首页 文章 精选 留言 我的

精选列表

搜索[面试],共4916篇文章
优秀的个人博客,低调大师

上次阿里面试问到Redis主从复制原理,这次终于搞明白了!

1.前言 Redis单节点存在单点故障,为解决单点问题,需要对Redis节点配置从节点。使用哨兵来监听主节点存活状态,若主节点挂掉,从节点能继续提供缓存功能。从节点怎样和主节点间完成数据传递?就是Redis的主从复制。 2. 主从配置及作用 临时配置:redis-cli进入redis从节点后,使用 --slaveof [masterIP] [masterPort]永久配置:进入从节点的配置文件redis.conf,增加slaveof [masterIP] [masterPort]作用:1)主从配置结合哨兵模式能解决单点故障问题,提高redis可用性2)从节点仅提高读的操作,主节点提供写操作。对于读多写少的状况,可给主节点配置多个从节点,从而提供响应效率补充:主从复制并不是redis的横向拓展,集群模式才是 3. 复制过程 1)从节点执行slaveof [masterIP] [masterPort],保存主节点信息2)从节点中的定时任务发现主节点信息,建立和主节点的socket连接3)从节点发送Ping信号,主节点返回Pong,两边能互相通信4)连接建立后,主节点将所有数据发送给从节点(数据同步)5)主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性 4. 数据同步 redis 2.8 之前使用sync [runId] [offset]同步命令,redis2.8之后使用psync [runId] [offset]命令。两者不同在于,sync命令仅支持全量复制过程,psync支持全量和部分复制;介绍同步之前先介绍几个概念:runId:每个redis节点启动都会生成唯一的runId,每次redis重启后,runId也会发生变化offset:主节点和从节点都各自维护自己的主从复制偏移量offset,当主节点有写入命令时,offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的offset,并把自己的offset发送给主节点。这样,主节点同时保存自己的offset,从节点的offset,通过对比offset来判断主从节点数据是否一致repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小为1MB1)主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区。从节点同步主节点数据完成后,主节点将缓冲区的数据继续发送给从节点,用于部分复制;2)主节点(master)响应写命令时,不但会把命名发送给从节点,还会写入复制积压缓冲区,用于复制命令丢失的数据补救; psync执行流程 从节点发送psync [runId] [offset]命令,主节点有如下响应FULLRESYNC:第一次连接,进行全量复制CONTINUE:进行部分复制ERR:不支持psync命令,进行全量复制 全量复制流程 1)从节点发送psync ? -1命令,因为第一次发送,不知道主节点的runId,所以为?,因为是第一次复制,所以offset = -1。2)主节点发现从节点是第一次复制,变返回FULLRESYNC {runId} {offset},runId是主节点的runId,offset是主节点目前的offset。3)从节点接收主节点信息后,保存到info中。4)主节点在发送FULLRESYNC后,启动bgsave命令,生成RDB文件(数据持久化)。5)6)主节点发送RDB文件给从节点。到从节点加载数据完成这段期间主节点的写命令放入缓冲区。7)从节点清理自己的数据库数据。8)从节点加载RDB文件,将数据保存的自己的数据库中。9)如果从节点开启了AOF(另一种持久化方案),从节点会异步重写aof文件。 部分复制流程 1)部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施,使用psync {runId}{offset}命令实现。当从节点(slave)正在复制主节点(master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存将这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据。2)主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB。当从节点网络恢复后,从节点会再次连上主节点。3)当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当做psync参数发送个主节点,要求进行部分复制操作。4)主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+COUTINUE响应,表示可以进行部分复制。因为缓冲区大小固定,若发生缓存溢出,则要进行全量复制。5)主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。欢迎大家关注我的公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。 5. 补充 Redis故障处理若主节点挂掉后,再次重启,runid的值会变。此时从节点的发送psync命令,会提示找不到原runid,则会再进行一次全量复制。为避免这种状况,使用Redis故障转移机制,主节点挂掉后,从节点升级为主节点。如哨兵模式。 最后 欢迎大家一起交流,喜欢文章记得点个赞哟,感谢支持!

优秀的个人博客,低调大师

浅入浅出MongonDB,教你轻松应对面试中遇到的MongonDB索引问题

前言 索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中( 索引存储在特定字段或字段集的值),而且是使用了B-tree结构。索引可以极大程度提升MongoDB查询效率。 如果没有索引,MongoDB必须执行全集合collections扫描,即扫描集合中的每个文档,选取符合查询条件的文档document。 如果查询时存在适当的索引,MongoDB可以使用索引来限制它必须查询的文档document的数量,特别是在处理大量数据时,所以选择正确的索引是很关键的、重要的。 创建索引,需要考虑的问题: 每个索引至少需要数据空间为8kb;添加索引会对写入操作会产生一些性能影响。 对于具有高写入率的集合Collections,索引很昂贵,因为每个插入也必须更新任何索引;索引对于具有高读取率的集合Collections很有利,不会影响没索引查询;处于索引处于action状态时,每个索引都会占用磁盘空间和内存,因此需要对这种情况进行跟踪检测。索引限制: 索引名称长度不能超过128字段;复合索引不能超过32个属性;每个集合Collection不能超过64个索引;不同类型索引还具有各自的限制条件。 1. 索引管理 1.1 索引创建 索引创建使用createIndex()方法,格式如下: createIndex() 接收可选参数,可选参数列表如下: **1.2 查看索引**查看Collection中所有索引,格式如下: 1.3 删除索引 删除Collection中的索引:格式如下: 1.4 索引名称 索引的默认名称是索引键和索引中每个键的value1或-1,形式index_name+1/-1,比如: 也可以指定索引名称: 1.5 查看索引创建过程以及终止索引创建 1.6 索引使用情况 **1.7 MongoDB度量标准**MongoDB提供了许多索引使用和操作的度量标准,在分析数据库的索引使用时可能需要考虑这些度量标准,如下所示: 1.8 后台索引操作 在密集(快达到数据库最大容量)Collection创建索引:在默认情况下,在密集的Collection(快达到数据库最大容量)时创建索引,会阻止其他操作。在给密集的Collection(快达到数据库最大容量)创建索引时, 索引构建完成之前,保存Collection的数据库不可用于读取或写入操作。 任何需要对所有数据库(例如listDatabases)进行读或写锁定的操作都将等待不是后台进程的索引构建完成。 因此可以使用background属性进行设置后台索引创建,操作如下: 2. 索引类型 2.1 单字段索引(Single Field Indexes) MongoDB可以在任何一个字段中创建索引,默认情况下,所有的集合(collections)会在_id字段中创建索引。_id索引是为防止客户端插入具有相同value的_id字段的文档Document,而且不能删除_id字段索引。 在分片群集中使用_id索引,如果不使用_id字段作为分片键,则应用程序必须确保_id字段中值的唯一性以防止出错,解决方法为使用标准的自动生成的ObjectId来完成。 一般单字段索引的value中,“1”指定按升序对项目进行排序的索引,“-1”指定按降序对项目进行排序的索引。如下所示: 在单个字段创建索引,示例如下: 在嵌入式文档Document中的字段创建索引,示例如下: 在嵌入式文档Document创建索引,示例如下: 2.2 复合索引(Compound Index) 复合索引指的是将多个key组合到一起创建索引,这样可以加速匹配多个键的查询。特性如下: MongoDB对任何复合索引都限制了32个字段; 无法创建具有散列索引(hash index)类型的复合索引。如果尝试创建包含散列索引字段的复合索引,则会报错; 复合索引创建字段索引的顺序是很重要的。因为索引以升序(1)或降序(-1)排序顺序存储对字段的引用; 对于单字段索引,键的排序顺序无关紧要,因为MongoDB可以在任一方向上遍历索引。 但是,对于复合索引,排序顺序可以决定索引是否可以支持排序操作; 除了支持在所有索引字段上匹配的查询之外,复合索引还可以支持与索引字段的前缀匹配的查询。 创建复合索引的格式: 排序顺序,两个字段的复合索引示例,index{userid:1,score:-1},先userid的value排序,然后再userid排序基础下进行score排序。如下图: 创建复合索引,示例如下: 复合索引中的前缀查询,示例如下: **2.3 多键索引**MongoDB使用多键索引为数组的每个元素都创建索引,多键索引可以建立在字符串、数字等key或者内嵌文档(document)的数组上,如果索引字段包含数组值,MongoDB会自动确定是否创建多键索引; 您不需要手动指定多键类型。 其中创建方式: 索引边界 使用多键索引,会出现索引边界(索引边界即是查询过程中索引能查找的范围)的计算,并计算必须遵循一些规则。即当多个查询的条件中字段都存在索引中时,MongoDB将会使用交集或者并集等来判断这些条件索引字段的边界最终产生一个最小的查找范围。可以分情况: 1).交集边界 交集边界即为多个边界的逻辑交集,对于给定的数组字段,假定一个查询使用了数组的多个条件字段并且可以使用多键索引。如果使用了$elemMatch连接了条件字段,则MongoDB将会相交多键索引边界,示例如下: 查询条件分别为大于等于3、小于等于6,其中 (1)中使用了$elemMatch连接查询条件,会产生一个交集ratings:[[3,6]。在(2)查询中,没使用$elemMatch,则不会产生交集,只要满足任何一个条件即可。 2).并集边界 并集边界常常用在确定多键组合索引的边界,例如:给定的组合索引{a:1,b:1},在字段a上有一个边界:[3,+∞),在字段b上有一个边界:(-∞,6],相并这两个边界的结果是:{ a: [ [ 3, Infinity ] ], b: [ [ -Infinity, 6 ] ] }。 而且如果MongoDB没法并集这两个边界,MongoDB将会强制使用索引的第一个字段的边界来进行索引扫描,在这种情况下就是: a: [ [ 3, Infinity ] ]。 3)、数组字段的组合索引 一个组合索引的索引字段是数组,例如一个survey collection集合document文档中含有item字段和ratings数组字段,示例如下: 分别处理查询条件: MongoDB使用并集边界来组合这两个边界: 4).内嵌文档document的数组上建立组合索引 如果数组中包含内嵌文档document,想在包含的内嵌文档document字段上建立索引,需要在索引声明中使用逗号“,” 来分隔字段名,示例如下: 则score字段名称就是:ratings.score。 5).混合不是数组类型的字段和数组类型字段的并集 分别对查询条件进行处理: MongoDB可以组合 item键的边界与 ratings.score和ratings.by两个边界中的一个,到底是score还是by索引边界这取决于查询条件和索引键的值。MongoDB不能确保哪个边界和item字段进行并集。 但如果想组合ratings.score和ratings.by边界,则查询必须使用$elemMatch。 6).数组字段索引的并集边界 在数组内部并集索引键的边界, 除了字段名称外,索引键必须有相同的字段路径, 查询的时候必须在路径上使用$elemMatch进行声明 对于内嵌的文档,使用逗号分隔的路径,比如a.b.c.d是字段d的路径。为了在相同的数组上并集索引键的边界,需要$elemMatch必须使用在a.b.c的路径上。 比如:在ratings.score和ratings.by字段上创建组合索引: 字段ratings.score和ratings.by拥有共同的路径ratings。下面的查询使用$elemMatch则要求ratings字段必须包含一个元素匹配这两个条件: 分别对查询条件进行处理: MongoDB可以使用并集边界来组合这两个边界: 7). 还有不使用$elemMatch进行查询以及不完整的路径上使用$elemMatch,想要了解更多可以查看《官方文档-Multikey Index Bounds》。 限制: 对于一个组合多键索引,每个索引文档最多只能有一个索引字段的值是数组。如果组合多键索引已经存在了,不能在插入文档的时候违反这个限制; 不能声明一个多键索引作为分片键索引; 哈希索引不能拥有多键索引; 多键索引不能进行覆盖查询; 当一个查询声明把数组整体作为精确匹配的时候,MongoDB可以使用多键索引来查找这个查询数组的第一个元素,但是不能使用多键索引扫描来找出整个数组。代替方案是当使用多键索引查询出数组的第一个元素之后,MongoDB再对过滤之后的文档再进行一次数组匹配。 2.4 全文索引(text index) MongoDB提供了一种全文索引类型,支持在Collection中搜索字符串内容,对字符串与字符串数组创建全文可搜索的索引 。 这些全文索引不存储特定于语言的停用词(例如“the”,“a”,“或”),并且阻止document集合中的单词仅存储根词。创建方式如下: 而且MongoDB提供权重以及通配符的创建方式。查询方式多个字符串空格隔开,排除查询使用“-”如下所示: 要删除全本索引,需要将索引的名称传递给db.collection.dropIndex()方法, 而要获取索引的名称,使用db.collection.getIndexes()方法。 还可以指定全文索引的语言,通过default_language属性 在创建时指定, 或者使用language_override属性 覆盖掉创建document文档时默认的语言,如下所示: 权重 每个全文索引可以通过设置权重来分配不同的搜索程度,默认权重为1,对于文档中的每个索引字段,MongoDB将匹配数乘以权重并将结果相加。 使用此总和,MongoDB然后计算文档的分数,示例如下: content权重为10,keywords为5,about为默认权重1,因此可以得出content对于keywords查询频率高于2倍,而对于about字段则是10倍。 通配符全文索引 在多个字段上创建全文索引时,还可以使用通配符说明符($**)。 使用通配符全文索引,MongoDB会为包含Collection中每个Document的字符串数据。例如: 通配符全本索引是多个字段上的全本索引。 因此,可以在创建索引期间为特定字段指定权重,以控制结果的排名。 限制 每个Collection一个全文索引:一个collection最多只有一个全文索引, Text Search 和Hints函数,如果查询包含$ text查询表达式,则不能使用hint(); Text Index and Sort,排序操作无法从文本索引获取排序顺序,即使是复合文本索引也是如此; 即排序操作不能使用文本索引中的排序; 复合索引:复合索引可以包括文本索引键与升序/降序索引键的组合。 但是,这些复合索引具有以下限制: 复合文本索引不能包含任何其他特殊索引类型,例如多键或地理空间索引字段。 如果复合文本索引包括文本索引键之前的键,则执行$ text搜索时,查询谓词必须包含前面键上的相等匹配条件。 创建复合文本索引时,必须在索引规范文档中相邻地列出所有文本索引键。 **2.5 Hash 索引**散列索引使用散列函数来计算索引字段值的散列值。 散列函数会折叠嵌入的文档并计算整个值的散列值,但不支持多键(即数组)索引。 生成hash索引key使用了convertShardKeyToHashed()方法。创建方式如下: 而且散列索引支持使用散列分片键进行分片。 基于散列的分片使用字段的散列索引作为分片键来分割整个分片群集中的数据。 3. 索引属性 索引属性有TTL索引、惟一性索引、部分索引、稀疏索引以及区分大小写索引。 **3.1 TTL索引(TTL Indexes)**TTL索引是特殊的单字段索引,并且字段类型必须是date类型或者包含有date类型的数组,MongoDB可以使用它在一定时间后或在特定时钟时间自动从集合中删除文档。 数据到期对于某些类型的信息非常有用,例如机器生成的事件数据,日志和会话信息,这些信息只需要在数据库中持续有限的时间。 创建TTL索引方法,和普通索引的创建方法一样,只是会多加一个expireAfterSeconds的属性,格式如下: 例子: 指定过期时间 首先在保存BSON日期类型值或BSON日期类型对象数组的字段上创建TTL索引,并指定expireAfterSeconds值为0.对于集合中的每个文档,设置 索引日期字段为与文档到期时间对应的值。示例操作如下: 第一步: 第二步: 数据过期类型: 当指定时间到了过期的阈值数据就会过期并删除; 如果字段是数组,并且索引中有多个日期值,MongoDB使用数组中最低(即最早)的日期值来计算到期阈值; 如果文档(document)中的索引字段不是日期或包含日期值的数组,则文档(document)将不会过期; 如果文档(document)不包含索引字段,则文档(document)不会过期。 TTL索引特有限制: TTL索引是单字段索引。 复合索引不支持TTL并忽略expireAfterSeconds选项; _id属性不支持TTL索引; 无法在上限集合上创建TTL索引,因为MongoDB无法从上限集合中删除文档; 不能使用createIndex()方法来更改现有索引的expireAfterSeconds值。而是将collMod数据库命令与索引集合标志结合使用。 否则,要更改现有索引的选项的值,必须先删除索引并重新创建; 如果字段已存在非TTL单字段索引,则无法在同一字段上创建TTL索引,因为无法在相同key创建不同类型的的索引。 要将非TTL单字段索引更改为TTL索引,必须先删除索引并使用expireAfterSeconds选项重新创建。 3.2 惟一性索引(Unique Indexes) 唯一索引可确保索引字段不存储重复值; 即强制索引字段的唯一性。 默认情况下,MongoDB在创建集合期间在_id字段上创建唯一索引。创建方式如下: 单个字段创建方式,示例如下: 唯一性复合索引: 还可以对复合索引强制执行唯一约束。 如果对复合索引使用唯一约束,则MongoDB将对索引键值的组合强制实施唯一性。示例如下: 唯一多键索引: 创建唯一索引到副本或者分片中: 对于副本集和分片集群,使用滚动过程创建唯一索引需要在过程中停止对集合的所有写入。 如果在过程中无法停止对集合的所有写入,请不要使用滚动过程。 相反,通过以下方式在集合上构建唯一索引: 在主服务器上为副本集发出db.collection.createIndex() 在mongos上为分片群集发出db.collection.createIndex() NOTE:详细解析可以看 限制: 如果集合已经包含超出索引的唯一约束的数据(即有重复数据),则MongoDB无法在指定的索引字段上创建唯一索引。 不能在hash索引上创建唯一索引 唯一约束适用于Collection中的一个Document。由于约束适用于单文档document,因此对于唯一的多键索引,只要该文档document的索引键值不与另一个文档document的索引键值重复,文档就可能具有导致重复索引键值的数组元素。 在这种情况下,重复索引记录仅插入索引一次。 分片Collection唯一索引只能如下:1).分片键上的索引2).分片键是前缀的复合索引3).默认的_id索引; 但是,如果_id字段不是分片键或分片键的前缀,则_id索引仅对每个分片强制执行唯一性约束。如果_id字段不是分片键,也不是分片键的前缀,MongoDB希望应用程序在分片中强制执行_id值的唯一性。3.3 部分索引(Partial Indexes) 部分索引通过指定的过滤表达式去达到局部搜索。通过db.collection.createIndex()方法中增加partialFilterExpression属性创建,过滤表达式如下: 等式表达式(即 file:value或使用$eq运算符) $exists表达式 $gt,$gte,$lt,$lte 表达式 $type表达式 $and 示例如下: 其中: (1)查询: 查询条件{ $gte: 8 }于创建索引条件{ $gt: 5 }可以构成一个完整集(查询条件是创建索引条件的子集,即大于5可以包含大于等于 8),可以使用部分索引查询。(2)查询: 条件达不到完整集,MongoDB将不会将部分索引用于查询或排序操作。(3)查询: 次查询没有使用过滤表达式,也不会使用部分索引,因为要使用部分索引,查询必须包含过滤器表达式(或指定过滤器表达式子集的已修改过滤器表达式)作为其查询条件的一部分限制: 不可以仅通过过滤表达式创建多个局部索引;不可以同时使用局部索引和稀疏索引(sparse index);_id索引不能使用局部索引,分片索引(shard key index)也不能使用局部索引;同时指定partialFilterExpression和唯一约束,则唯一约束仅适用于满足过滤器表达式的文档。 如果Document不符合筛选条件,则具有唯一约束的部分索引是允许插入不符合唯一约束的Document。3.4 稀疏索引(Sparse Indexes) 稀疏索只引搜索包含有索引字段的文档的条目,跳过索引键不存在的文档,即稀疏索引不会搜索不包含稀疏索引的文档。默认情况下, 2dsphere (version 2), 2d, geoHaystack, 全文索引等总是稀疏索引。创建方式db.collection.createIndex()方法增加sparse属性,如下所示: 稀疏索引不被使用的情况: 如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引,除非hint()示显式指定索引。 稀疏复合索引: 对于包含上升/下降排序的稀疏复合索引,只要复合索引中的一个key 索引存在都会被检测出来 对于包含上升/下降排序的包含地理空间可以的稀疏复合索引,只有存在地理空间key才能被检测出来 对于包含上升/下降排序的全文索引的稀疏复合索引,只有存在全文索引索引才可以被检测 稀疏索引与唯一性: 一个既包含稀疏又包含唯一的索引避免集合上存在一些重复值得文档,但是允许多个文档忽略该键。满足稀疏索引和唯一性操作其两个限制都要遵循。 整合示例如下: 其中: (1)查询: 可以使用稀疏索引查询,返回完整集:{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }(2)查询: 即使排序是通过索引字段进行的,MongoDB也不会选择稀疏索引来完成查询以返回完整的结果:{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }(3)查询: 使用hint()返回所需完整集:{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 } 4. 其他事项 4.1 索引策略 索引策略: 应用程序的最佳索引必须考虑许多因素,包括期望查询的类型,读取与写入的比率以及系统上的可用内存量。 在开发索引策略时,您应该深入了解应用程序的查询。在构建索引之前,映射将要运行的查询类型,以便您可以构建引用这些字段的索引。索引具有性能成本,但是对于大型数据集上的频繁查询而言,它们的价值更高。考虑应用程序中每个查询的相对频率以及查询是否证明索引是合理的。 设计索引的最佳总体策略是使用与您将在生产中运行的数据集类似的数据集来分析各种索引配置,以查看哪些配置性能最佳。检查为集合创建的当前索引,以确保它们支持您当前和计划的查询。如果不再使用索引,请删除索引。 通常,MongoDB仅使用一个索引来完成大多数查询。但是,$或查询的每个子句可能使用不同的索引,从2.6开始,MongoDB可以使用多个索引的交集。

优秀的个人博客,低调大师

Javascript前端开发:阿里JS面试题让你深入了解原型与继承

题目如下: var F = function(){} Object.prototype.a = function(){ console.log('a()') } Function.prototype.b = function(){ console.log('b()') } var f = new F() F.a() F.b() f.a() f.b() 主要考查的技术点: 1.、原型与原型链 2、实例对象、构造函数、Object、 Function的关系 分析: F是个构造函数,而f是构造函数F的一个实例。 因为F instanceof Object == true、F instanceof Function == true 由此我们可以得出结论:F是Object 和 Function两个的实例,即F既能访问到a,也能访问到b。 所以F.a() 输出 a() F.b() 输出 b() 对于f,我们先来看下下面的结果: f并不是Function的实例,因为它本来就不是构造函数,所以就调用Function原型链上的相关属性和方法了,只能访问Object原型链。 所以f.a() 输出 a() 而f.b()就报错了。 接下来,我们具体分析下,它们是如何按路径查找的: f.a的查找路径: f自身: 没有 ---> f.__proto__(Object.prototype): 输出a() f.b的查找路径: f自身: 没有 ---> f.__proto__(Object.prototype): 没有 ---> f.__proto__.__proto__ (Object.prototype.__proto__): 因为找不到,所以报错 F.a的查找路径: F自身: 没有 ---> F.__proto__(Function.prototype): 没有 ---> F.__proto__.__proto__(Object.prototype): 输出 a() F.b的查找路径: F自身: 没有 ---> F.__proto__(Function.prototype): b()

优秀的个人博客,低调大师

突破Java面试(23-4) - 再深入Redis Replication的完整执行流程及原理

0 Github 1 复制的完整流程 slave node启动,仅仅保存master node的信息,包括master node的host和ip,但复制流程尚未开始master host和ip配置在 redis.conf 中的 slaveof slave node内部有个定时任务,每s 检查是否有新的master node要连接和复制,若发现,就跟master node建立socket网络连接 slave node发送ping命令给master node 口令认证 - 若master设置了requirepass,那么salve node必须同时发送masterauth的口令认证 master node第一次执行全量复制,将所有数据发给slave node master node后续持续将写命令,异步复制给slave node 完整复制的基本流程图

优秀的个人博客,低调大师

【漫画】以后在有面试官问你AVL树,你就把这篇文章扔给他。

背景 西天取经的路上,一样上演着编程的乐趣..... 1、若它的左子树不为空,则左子树上所有的节点值都小于它的根节点值。 2、若它的右子树不为空,则右子树上所有的节点值均大于它的根节点值。 3、它的左右子树也分别可以充当为二叉查找树。 例如: 例如,我现在想要查找数值为14的节点。由于二叉查找树的特性,我们可以很快着找到它,其过程如下: 1、和根节点9比较 2、由于 14 > 9,所以14只可能存在于9的右子树中,因此查看右孩子13 3、由于 14 > 13,所以继续查看13的右孩子15 4、由于 14 < 15,所以14只可能存在于15的左孩子中,因此查找15的左孩子14 5、这时候发现14正是自己查找的值,于是查找结束。 这种查找二叉树的查找正是二分查找的思想,可以很快着找到目的节点,查找所需的最大次数等同于二叉查找树的高度。 在插入的时候也是一样,通过一层一层的比较,最后找到适合自己的位置。 初始的二叉查找树只有三个节点: 然后我们按照顺序陆续插入节点 4,3,2,1,0。插入之后的结构如下: 这是一种比查找二叉树还特别的树哦,这种树就可以帮助我们解决二叉查找树刚才的那种所有节点都倾向一边的缺点的。具有如下特性: 具有二叉查找树的全部特性。 每个节点的左子树和右子树的高度差至多等于1。 例如:图一就是一颗AVL树了,而图二则不是(节点右边标的是这个节点的高度)。 对于图二,因为节点9的左孩子高度为2,而右孩子高度为0。他们之间的差值超过1了。 这种树就可以保证不会出现大量节点偏向于一边的情况了。 听起来这种树还不错,可以对于图1,如果我们要插入一个节点3,按照查找二叉树的特性,我们只能把3作为节点4的左子树插进去,可是插进去之后,又会破坏了AVL树的特性,那我们那该怎么弄? 右旋 我们在进行节点插入的时候,可能会出现节点都倾向于左边的情况,例如: 我们把这种倾向于左边的情况称之为左-左型。这个时候,我们就可以对节点9进行右旋操作,使它恢复平衡。 即:顺时针旋转两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子 再举个例子: 节点4和9高度相差大于1。由于是左孩子的高度较高,此时是左-左型,进行右旋。 这里要注意,节点4的右孩子成为了节点6的左孩子了 我找了个动图,尽量这个动图和上面例子的节点不一样。 左旋 左旋和右旋一样,就是用来解决当大部分节点都偏向右边的时候,通过左旋来还原。例如: 我们把这种倾向于右边的情况称之为右-右型。 我也找了一张动图。 例子讲解 初始状态如下: 然后我们主键插入如下数值:1,4,5,6,7,10,9,8 插入 1 左-左型,需要右旋调整。 插入4 继续插入 5 右-右型,需要左旋转调整。 继续插入6 右-右型,需要进行左旋 继续插入7 右-右型,需要进行左旋 继续插入10 继续插入9 出现了这种情况怎么办呢?对于这种右-左型的情况,单单一次左旋或右旋是不行的,下面我们先说说如何处理这种情况。 这种我们就把它称之为右-左 型吧。处理的方法是先对节点10进行右旋把它变成右-右型。 然后在进行左旋。 所以对于这种右-左型的,我们需要进行一次右旋再左旋。 同理,也存在左-右型的,例如: 对于左-右型的情况和刚才的 右-左型相反,我们需要对它进行一次左旋,再右旋。 回到刚才那道题 对它进行右旋再左旋。 到此,我们的插入就结束了。 总结一下 在插入的过程中,会出现一下四种情况破坏AVL树的特性,我们可以采取如下相应的旋转。 1、左-左型:做右旋。 2、右-右型:做左旋转。 3、左-右型:先做左旋,后做右旋。 4、右-左型:先做右旋,再做左旋。 不知道大家发现规律没,这个规则还是挺好记。 代码实现 //定义节点class AvlNode {intdata; AvlNode lchild;//左孩子AvlNode rchild;//右孩子intheight;//记录节点的高度}//在这里定义各种操作publicclass AVLTree{//计算节点的高度staticintheight(AvlNode T) {if(T ==null) {return-1; }else{returnT.height; } }//左左型,右旋操作staticAvlNode R_Rotate(AvlNode K2) { AvlNode K1;//进行旋转K1 = K2.lchild; K2.lchild = K1.rchild; K1.rchild = K2;//重新计算节点的高度K2.height= Math.max(height(K2.lchild),height(K2.rchild)) +1; K1.height= Math.max(height(K1.lchild),height(K1.rchild)) +1;returnK1; }//进行左旋staticAvlNode L_Rotate(AvlNode K2) { AvlNode K1; K1 = K2.rchild; K2.rchild = K1.lchild; K1.lchild = K2;//重新计算高度K2.height= Math.max(height(K2.lchild),height(K2.rchild)) +1; K1.height= Math.max(height(K1.lchild),height(K1.rchild)) +1;returnK1; }//左-右型,进行右旋,再左旋staticAvlNode R_L_Rotate(AvlNode K3) {//先对其孩子进行左旋K3.lchild = R_Rotate(K3.lchild);//再进行右旋returnL_Rotate(K3); }//右-左型,先进行左旋,再右旋staticAvlNode L_R_Rotate(AvlNode K3) {//先对孩子进行左旋K3.rchild = L_Rotate(K3.rchild);//在右旋returnR_Rotate(K3); }//插入数值操作staticAvlNode insert(intdata, AvlNode T) {if(T ==null) { T =newAvlNode(); T.data = data; T.lchild = T.rchild =null; }elseif(data < T.data) {//向左孩子递归插入T.lchild = insert(data, T.lchild);//进行调整操作//如果左孩子的高度比右孩子大2if(height(T.lchild) -height(T.rchild) ==2) {//左-左型if(data < T.lchild.data) { T = R_Rotate(T); }else{//左-右型T = R_L_Rotate(T); } } }elseif(data > T.data) { T.rchild = insert(data, T.rchild);//进行调整//右孩子比左孩子高度大2if(height(T.rchild) -height(T.lchild) ==2)//右-右型if(data > T.rchild.data) { T = L_Rotate(T); }else{ T = L_R_Rotate(T); } }//否则,这个节点已经在书上存在了,我们什么也不做//重新计算T的高度T.height= Math.max(height(T.lchild),height(T.rchild)) +1;returnT; }} 提示:可以左右拉动 希望通过这种漫画的形式,能够让你们更加容易读懂一些算法或数据结构。 欢迎工作一到五年的Java工程师朋友们加入Java填坑之路:860113481 群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

优秀的个人博客,低调大师

面试必备】透过源码角度一步一步带你分析 ArrayList 扩容机制

该文已加入开源文档:JavaGuide(一份涵盖大部分Java程序员所需要掌握的核心知识)。地址:https://github.com/Snailclimb/JavaGuide. 一 先从 ArrayList 的构造函数说起 ArrayList有三种方式来初始化,构造方法源码如下: /** * 默认初始容量大小 */ private static final int DEFAULT_CAPACITY = 10; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** *默认构造函数,使用初始容量10构造一个空列表(无参数构造) */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 带初始容量参数的构造函数。(用户自己指定容量) */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) {//初始容量大于0 //创建initialCapacity大小的数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) {//初始容量等于0 //创建空数组 this.elementData = EMPTY_ELEMENTDATA; } else {//初始容量小于0,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回 *如果指定的集合为null,throws NullPointerException。 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[](see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 细心的同学一定会发现 :以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10。 下面在我们分析 ArrayList 扩容时会降到这一点内容! 二 一步一步分析 ArrayList 扩容机制 这里以无参构造函数创建的 ArrayList 为例分析 1. 先来看 add 方法 /** * 将指定的元素追加到此列表的末尾。 */ public boolean add(E e) { //添加元素之前,先调用ensureCapacityInternal方法 ensureCapacityInternal(size + 1); // Increments modCount!! //这里看到ArrayList添加元素的实质就相当于为数组赋值 elementData[size++] = e; return true; } 2. 再来看看 ensureCapacityInternal() 方法 可以看到 add 方法 首先调用了ensureCapacityInternal(size + 1) //得到最小扩容量 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 获取默认的容量和传入参数的较大值 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } 当 要 add 进第1个元素时,minCapacity为1,在Math.max()方法比较后,minCapacity 为10。 3. ensureExplicitCapacity() 方法 如果调用 ensureCapacityInternal() 方法就一定会进过(执行)这个方法,下面我们来研究一下这个方法的源码! //判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); } 我们来仔细分析一下: 当我们要 add 进第1个元素到 ArrayList 时,elementData.length 为0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity 此时为10。此时,minCapacity - elementData.length > 0 成立,所以会进入 grow(minCapacity) 方法。 当add第2个元素时,minCapacity 为2,此时e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时,minCapacity - elementData.length > 0 不成立,所以不会进入 (执行)grow(minCapacity) 方法。 添加第3、4···到第10个元素时,依然不会执行grow方法,数组容量都为10。 直到添加第11个元素,minCapacity(为11)比elementData.length(为10)要大。进入grow方法进行扩容。 4. grow() 方法 /** * 要分配的最大数组大小 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * ArrayList扩容的核心方法。 */ private void grow(int minCapacity) { // oldCapacity为旧容量,newCapacity为新容量 int oldCapacity = elementData.length; //将oldCapacity 右移一位,其效果相当于oldCapacity /2, //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1); //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE, //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍! 记清楚了!不是网上很多人说的 1.5 倍+1! ">>"(移位运算符):>>1 右移一位相当于除2,右移n位相当于除以 2 的 n 次方。这里 oldCapacity 明显右移了1位所以相当于oldCapacity /2。对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源 我们再来通过例子探究一下grow() 方法 : 当add第1个元素时,oldCapacity 为0,经比较后第一个if判断成立,newCapacity = minCapacity(为10)。但是第二个if判断不会成立,即newCapacity 不比 MAX_ARRAY_SIZE大,则不会进入 hugeCapacity 方法。数组容量为10,add方法中 return true,size增为1。 当add第11个元素进入grow方法时,newCapacity为15,比minCapacity(为11)大,第一个if判断不成立。新容量没有大于数组最大size,不会进入hugeCapacity方法。数组容量扩为15,add方法中return true,size增为11。 以此类推······ 这里补充一点比较重要,但是容易被忽视掉的知识点: java 中的 length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法. java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看! 5. hugeCapacity() 方法。 从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。 private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); //对minCapacity和MAX_ARRAY_SIZE进行比较 //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小 //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小 //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } 三 System.arraycopy() 和 Arrays.copyOf()方法 阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)、toArray() 等方法中都用到了该方法! 3.1 System.arraycopy() 方法 /** * 在此列表中的指定位置插入指定的元素。 *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 */ public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! //arraycopy()方法实现数组自己复制自己 //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } 我们写一个简单的方法测试以下: public class ArraycopyTest { public static void main(String[] args) { // TODO Auto-generated method stub int[] a = new int[10]; a[0] = 0; a[1] = 1; a[2] = 2; a[3] = 3; System.arraycopy(a, 2, a, 3, 3); a[2]=99; for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } } } 结果: 0 1 99 2 3 0 0 0 0 0 3.2 Arrays.copyOf()方法 /** 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 */ public Object[] toArray() { //elementData:要复制的数组;size:要复制的长度 return Arrays.copyOf(elementData, size); } 个人觉得使用 Arrays.copyOf()方法主要是为了给原有数组扩容,测试代码如下: public class ArrayscopyOfTest { public static void main(String[] args) { int[] a = new int[3]; a[0] = 0; a[1] = 1; a[2] = 2; int[] b = Arrays.copyOf(a, 10); System.out.println("b.length"+b.length); } } 结果: 10 3.3 两者联系和区别 联系: 看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy() 方法 区别: arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组。 四 ensureCapacity方法 ArrayList 源码中有一个 ensureCapacity 方法不知道大家注意到没有,这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的,那么这个方法有什么作用呢? /** 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。 * * @param minCapacity 所需的最小容量 */ public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } 最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量从新分配的次数 我们通过下面的代码实际测试以下这个方法的效果: public class EnsureCapacityTest { public static void main(String[] args) { ArrayList<Object> list = new ArrayList<Object>(); final int N = 10000000; long startTime = System.currentTimeMillis(); for (int i = 0; i < N; i++) { list.add(i); } long endTime = System.currentTimeMillis(); System.out.println("使用ensureCapacity方法前:"+(endTime - startTime)); list = new ArrayList<Object>(); long startTime1 = System.currentTimeMillis(); list.ensureCapacity(N); for (int i = 0; i < N; i++) { list.add(i); } long endTime1 = System.currentTimeMillis(); System.out.println("使用ensureCapacity方法后:"+(endTime1 - startTime1)); } } 运行结果: 使用ensureCapacity方法前:4637 使用ensureCapacity方法前:241 通过运行结果,我们可以很明显的看出向 ArrayList 添加大量元素之前最好先使用ensureCapacity 方法,以减少增量从新分配的次数

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。