学最好的别人,做最好的我们来吧,缓存面试习惯性的来张知识图谱
详见:zhuanlan.zhihu.com/p/76788470说明1:这些性能验证的测试并没有针对严谨的延时控制和不同并发的场景进行压测。数据仅供验证参考而不能作为线上指标。说明2:如果开启多线程,至少要4核的机器,且Redis实例已经占用相当大的CPU耗时的时候才建议采用,否则使用多线程没有意义。所以估计80%的公司开发人员看看就好。问题:Redis6.0多线程的实现机制?答:如图
流程简述如下:主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程主线程阻塞等待 IO 线程读取 socket 完毕主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行主线程阻塞等待 IO 线程将数据回写 socket 完毕解除绑定,清空等待队列
该设计有如下特点:IO 线程要么同时在读 socket,要么同时在写,不会同时读或写IO 线程只负责读写 socket 解析命令,不负责命令处理问题:开启多线程后,是否会存在线程并发安全问题?答:从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。问题:Redis线程中经常提到IO多路复用,如何理解?这是IO模型的一种,即经典的Reactor设计模式,有时也称为异步阻塞IO。
多路指的是多个socket连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
问题:redis如何做到高可用?答:redis具备的高可用,其实包含两层含义:一是数据尽量少丢失,二是服务尽量少中断。对于前者redis**使用AOF和RDB两种持久化方式保证,对于后者Redis的做法就是增加副本冗余量**,将一份数据同时保存在多个实例上。高级内容问题:redis的并发竞争问题如何解决?答:Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。对此有两种解决方法:客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。服务器角度,利用setnx实现锁。注:对于第一种,需要应用程序自己处理资源的同步,可以使用的方法比较通俗,可以使用synchronized也可以使用lock;第二种需要用到Redis的setnx命令,但是需要注意一些问题。问题:redis过期键的删除策略?答:定时删除:在设置键的过期时间的同时,创建一个timer,让定时器在键的过期时间到达时,立即执行对键的删除操作。(主动删除)对内存友好,但是对cpu时间不友好,有较多过期键的而情况下,删除过期键会占用相当一部分cpu时间。惰性删除:放任过期键不管,但是每次从键空间中获取键时,都检查取到的键是否过去,如果过期就删除,如果没过期就返回该键。(被动删除)对cpu时间友好,程序只会在取出键的时候才会对键进行过期检查,这不会在删除其他无关过期键上花费任何cpu时间,但是如果一个键已经过期,而这个键又保留在数据库中,那么只要这个过期键不被删除,他所占用的内存就不会释放,对内存不友好。定期删除:每隔一段时间就对数据库进行一次检查,删除里面的过期键。(主动删除)采用对内存和cpu时间折中的方法,每个一段时间执行一次删除过期键操作,并通过限制操作执行的时长和频率来减少对cpu时间的影响。难点在于,选择一个好的策略来设置删除操作的时长和执行频率。问题:简述redis的哨兵模式?答:哨兵是对redis进行实时的监控,主要有两个功能。监测主数据库和从数据库是否正常运行。当主数据库出现故障的时候,可以自动将一个从数据库转换为主数据库,实现自动切换。问题:redis的哨兵的监控机制是怎样的?答:哨兵监控也是有集群的,会有多个哨兵进行监控,当判断发生故障的哨兵达到一定数量的时候才进行修复。一个健壮的部署至少需要三个哨兵实例。每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他Sentinel实例发送一个PING命令如果一个实例(instance)距离最后一次有效回复PING命令的时间超过down-after-milliseconds选项所指定的值, 则这个实例会被Sentinel标记为主观下线。如果一个Master被标记为主观下线,则正在监视这个Master的所有Sentinel要以每秒一次的频率确认Master的确进入了主观下线状态。当有足够数量的Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线在一般情况下, 每个Sentinel会以每 10 秒一次的频率向它已知的所有Master,Slave发送INFO命令当Master被Sentinel标记为客观下线时,Sentinel向下线的Master的所有Slave发送INFO命令的频率会从 10 秒一次改为每秒一次若没有足够数量的Sentinel同意Master已经下线,Master的客观下线状态就会被移除。若Master重新向Sentinel的PING命令返回有效回复,Master的主观下线状态就会被移除。问题:redis常见性能问题和解决方案?答:Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内问题:为什么Redis是单线程的?答:官方给出答案是因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。问题:简述Redis的高并发和快速原因?答:Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。再说一下IO,Redis使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。问题:Redis持久化方式是什么?答:两种持久化方式即AOF日志和RDB快照。问题:简单介绍一下AOF和RDB,如果同时使用AOF和RDB,redis重启会使用哪个构建数据?答:AOF和RDB都是redis持久化方案。RDB持久化机制,对redis中的数据执行周期性的持久化。AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止RDB和AOF所有的持久化机制。如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整。问题:RDB持久化机制的优缺点?答:优缺点如下。优点:(1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据(2)RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可(3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速缺点:(1)如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据(2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒问题:AOF持久化机制的优缺点?答:优缺点如下:优点:(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据(2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据缺点:(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。问题:AOF日志是如何实现的?答:AOF日志是一种写后日志,“写后”的意思是Redis是先执行命令,把数据写入内存,然后才记录日志,如下图所示:
AOF里记录的是Redis收到的每一条命令,这些命令是以文本形式保存的。我们以Redis收到“set testkey testvalue”命令后记录的日志为例。
其中,”*3”表示当前命令有三个部分,每部分都是由”$ 数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,”$3 set”表示这部分有3个字节,也就是”set”命令。 这种写后日志的好处就是:先执行命令,如果命令出错了就报错,而写入日志的命令肯定是正确的,避免出现记录错误命令的情况。写日志是在命令执行后才记录日志,所以不会阻塞当前命令的写操作。AOF的日志何时写入磁盘呢?AOF日志写入磁盘是比较影响性能的,为了平衡性能与数据安全,开发了三种机制,也就是AOF配置项appendfsync的三个可选值:Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;同步写回可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能。Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;每秒写回采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。操作系统控制的写回在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在Redis手中了,只要AOF记录没有写回磁盘,一旦宕机对应的数据就丢失了;所以想要获得高性能,就选择No策略;如果想要得到高可靠性保证,就选择Always策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择Everysec策略。问题:AOF写日志方式会有什么问题?答:随着接收的写命令越来越多,AOF文件会越来越大,这事就会带来性能问题,性能问题主要是三个方面:文件系统本身对文件大小有限制,无法保存过大的文件。如果文件太大,之后再往里面追加命令记录的话,效率也会变低。如果发生宕机,AOF中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到Redis的正常使用。所以就需要使用AOF重写机制重写日志。问题:请你谈谈AOF重写机制?答:AOF在重写时,**Redis根据数据库现状创建一个新的AOF文件,假如数据库中有键值对:“test”:”hello”,那么重写机制会这样记录:set test hello。由上可知AOF重写机制可以把日志变少。因为在旧日志多条命令,在重写之后就变成一条命令了。以下画图解释:
由上图可知重写前6条命令,重写后只有一条命令,因为重写机制是基于数据库当前数据的,之前这个数据经历怎样的变化我都不关心,只关心结果。重写时还有个问题就是重写会不会阻塞主线程?和AOF日志由主线程写回不同,重写过程是由后台线程bgrewriteaof来完成的,这也是为了避免阻塞主线程**。整个重写过程是这样的:主线程fork出后台的bgrewriteaof子进程,fork会把主线程内存拷贝一份给bgrewriteaof子进程,这就是数据库此时最新数据,bgrewriteaof子进程会在不影响主线程的情况下逐一把拷贝的数据写成操作命令,记录在重写日志里。(拷贝)主线程未阻塞,可以处理新命令,处理之后将命令写入AOF日志缓冲区,保证AOF日志完整。(确保AOF日志完整)主线程处理的新命令也需要写入AOF重写日志,保证AOF重写日志不丢新数据。(确保AOF重写日志完整)总的来说,整个过程就是:一处拷贝,两处日志完整。
问题:AOF何时重写日志呢?或者说什么时候会触发重写AOF重写?答:主要有两种触发方式:手动执行bgrewriteaof触发AOF重写在redis.conf文件中配置重写的条件,如:auto-aof-rewrite-min-size 64MB // 当文件小于64M时不进行重写auto-aof-rewrite-min-percenrage 100 // 当文件比上次重写后的文件大100%时进行重写问题:AOF日志重写的时候,是由bgrewriteaof子进程来完成的,不用主线程参与,非阻塞是指子进程的执行不阻塞主线程。但是,你觉得,这个重写过程有没有其他潜在的阻塞风险呢?如果有的话,会在哪里阻塞?问题:AOF重写也有一个重写日志,AOF本身也有一个日志,它为什么不把两个日志共享呢?问题:请你谈谈redis持久化机制RDB是怎么样的?答:首先,RDB出现的原因由于使用AOF方法进行故障恢复的时候,如果日志特别多,Redis就会恢复得很缓慢,影响到正常使用。所以出现了RDB。RDB叫做内存快照,就是Redis DataBase的缩写。它记录的是某一时刻的数据,保存在磁盘的dump.rdb文件中。所以,在做数据恢复时,我们可以直接把RDB文件读入内存,很快地完成恢复。对于使用RDB来说有几个关键的地方:它给哪些数据做快照?为了提供所有数据可靠性,它执行的是全量快照。也就是所有数据做快照。redis提供了两个命令,save和bgsave。save在主线程中执行,会导致阻塞;bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配置。快照时数据能修改吗?如果redis在快照时数据不能修改,那无疑是给业务造成损失。也许你会说可以使用bgsave来避免阻塞,但是注意,避免阻塞和正常处理写操作并不是一回事。如果数据不能修改,虽然没有阻塞,但是主线程只能接受读请求,而写请求不能执行。这肯定不能接受,所以,Redis就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
如图所示,如果主线程读取数据A,主线程和bgsave互不影响。如果修改数据C,这个数据就复制一份,生成数据副本。bgsave子进程把副本写入RDB文件,主线程修改数据C不影响。多久做一次快照? 如果想要数据尽可能少丢失,那快照间隔的时间就要变得很短。虽然bgsave执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。另一方面,bgsave子进程需要通过fork操作从主线程创建出来。fork这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。所以,我们要做增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。那如何记住后续的修改操作呢?redis4.0提出混合使用AOF日志和内存快照的方法。就是内存快照以一定的频率执行,在两次快照之间,使用AOF日志记录这期间的所有命令操作。这样快照不需要频繁执行,AOF只需要记录两次快照间的操作。所以AOF文件也不会过大。
问题:redis主从库如何实现数据一致?答:如何实现数据一致,这里分以下几个问题说明。多个实例如何形成主从?当启动多个redis实例时,它们通过replicaof命令(Redis 5.0之前使用slaveof)形成主从关系。比如有两个实例:实例1(ip: 172.16.19.3)和实例2(ip: 172.16.19.5),在实例2上执行replicaof 172.16.19.3 6379命令,实例2变成实例1的重库,并从实例1上复制数据。主从之间第一次数据同步过程
如上图,可以分为以下步骤:第一阶段,段建立连接,协商同步。从库给主库发送psync命令表示数据同步,主库根据psync命令的参数来启动复制。参数主要包含主库的runID和复制进度offset。runID,是每个Redis实例启动时都会自动生成的一个随机ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的runID,所以将runID设为“?”。offset,此时设为-1,表示第一次复制。主库收到psync命令后,会用FULLRESYNC响应命令带上两个参数:主库runID和主库目前的复制进度offset,返回给从库。从库收到响应后,会记录下这两个参数。注意:FULLRESYNC响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。主库执行bgsave命令,生成RDB文件,接着将文件发给从库,从库接收RDB文件后清空数据库,然后加载RDB文件。主库数据同步过程中,仍然可以接收请求,这些请求没有记录在刚刚生成的RDB文件中,所以主库在内存中有一个replication buffer用于记录RDB文件生成后收到的所有写操作。第三阶段,主库会把replication buffer中的命令发送给从库,从库重新执行操作,这样主从就一致了。主-从-从模式下,数据如何同步?如果存在多个从数据库都需要从主数据库中同步数据,那主redis就忙这fork子进程生成RDB文件了,fork操作会阻塞主线程的,而且传输RDB文件也会占用主库网络带宽。所以就有了另一种办法:主从级联模式分担全量复制时的主库压力,什么意思呢?就是再选择一个从库,将这个从库作为其他从库的主库。那么就由这个从库为其他从库同步数据。
主库同步数据给从库,从库又同步数据给其他从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。如果主从库之间网路断了怎么办?redis2.8之前,重新全量复制。redis2.8之后采用增量复制,下面说说增量复制过程。主从断了之后,主库会把这段时间收到的写命令写入replication buffer和repl_backlog_buffer。增量复制所有操作都是基于repl_backlog_buffer的。它是一个环形缓冲区,主库会记录自己写到的位置(偏移量master_repl_offset),从库则会记录自己已经读到的位置(偏移量slave_repl_offset)。刚开始,master_repl_offset和slave_repl_offset在一起,然后主库写,从库读,所以一般slave_repl_offset要小于master_repl_offset。
回顾下增量复制的流程
这里有个注意的地方,repl_backlog_buffer是环形的,所以它会覆盖掉之前写的。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。这种情况可以调整repl_backlog_size参数大小。计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 – 主从库间网络传输命令速度 * 操作大小。在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即repl_backlog_size = 缓冲空间大小 * 2,这也就是repl_backlog_size的最终值。综上,整个redis数据同步问题就解释清楚了。问题:主从库间的数据复制同步使用的是RDB文件,AOF记录的操作命令更全,相比于RDB丢失的数据更少。那么,为什么主从库间的复制不使用AOF呢?问题:主从集群方式有什么缺点?答:主从集群方式一般是一主多从的模式,主库可接收读/写请求,并把数据同步给从库,从库只能接收读请求。那么这里就有个问题,如果主库挂了呢?那么就无法接收写请求了。
问题:为啥需要哨兵?哨兵机制是什么样的?答:因为主从集群模式,如果主库挂了,就无法提供正常的写功能了。这里涉及了三个问题:主库真的挂了吗?(是否误判)该选择哪个从库作为主库?(如何选主)怎么把新主库的相关信息通知给从库和客户端呢?(怎么通知)所以,哨兵机制就是解决上面这三个问题而出现的,哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。就围绕这三个任务来看一下哨兵机制流程。
监控监控是指哨兵进程会周期性的使用PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果发现PING命令响应超时,哨兵就会把它标记为主观下线。为了防止哨兵误判哨兵误判就是主库并没有故障,误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下,而选主的代价比较高。一般都会会采用多实例组成的集群模式进行部署,这也就是哨兵集群。当多数哨兵标记主库为主观下线时,那么主库就会被标记为客观下线,也就是表示主库下线是客观事实的。判断的原则就是少数服从多数。下面再给张图帮助理解:
选主 哨兵选主的过程可以概括为筛选 打分。筛选筛选时,哨兵除了要检查从库的当前在线状态,还要判断它之前的网络连接状态(因为如果它刚选上就网络不好挂了,那这次选主不就白费了么)。如果从库总是和主库断连而且超过了阈值,那这个从库就被排除了。具体可以通过down-after-milliseconds * 10配置,down-after-milliseconds是我们认定主从库断连的最大连接超时时间。如果在down-after-milliseconds毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了10次,就说明这个从库的网络状况不好,不适合作为新主库。总的来说,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库。打分打分按照三个规则依次进行三轮打分,主要某个库从在某一轮胜出,那么它就是主库,选举结束,规则分别是从库优先级、从库复制进度以及从库ID号。第一轮:优先级最高的从库得分高可以通过slave-priority配置从库优先级,如果你有台从库实例,配置比较高,你就可以手动设置它优先级,那么在哨兵选主的时候,它就会胜出。第二轮:和旧主库同步程度最接近的从库得分高同步接近程度,也就是repl_backlog_buffer这里的数据同步程度,可以通过从库slave_repl_offset与旧主库的master_repl_offset值是否相近来判断接近程度。如下图,从库2就胜出称为新主库。
第三轮:ID号小的从库得分高。每个实例都有一个ID,在优先级和复制进度都相同的情况下,ID号最小的从库得分最高,会被选为新主库通知在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。问题:通过哨兵机制,可以实现主从库的自动切换,这是实现服务不间断的关键支撑,同时,主从库切换是需要一定时间的。所以,在这个切换过程中,客户端能否正常地进行请求操作呢?如果想要应用程序不感知服务的中断,还需要哨兵或需要客户端再做些什么吗?问题:哨兵集群是如何自动发现的?答:哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往sentinel:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在,每隔两秒钟,每个哨兵都会往自己监控的某个master slaves对应的sentinel:hello channel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置,每个哨兵也会去监听自己监控的每个master slaves对应的sentinel:hello channel,然后去感知到同样在监听这个master slaves的其他哨兵的存在,每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。问题:使用redis要注意哪些影响性能的潜在因素?答:主要要注意下面这些因素:Redis内部的阻塞式操作;CPU核和NUMA架构的影响;Redis关键系统配置;Redis内存碎片;Redis缓冲区问题:redis实例有哪些阻塞点?答:可从redis实例的交互对象来分析。redis实例会和以下对象发生交互。客户端网络IO,不是阻塞点网络IO比较慢,但是redis使用了IO多路复用,避免了主线程一直等待网络连接或者请求到来的状态。所以网络IO它不是一个阻塞点。集合全量查询和聚合操作,阻塞点键值对的操作是redis和客户端的主要交互对象,那么复杂度高的操作一定会阻塞redis主线程,所以要留意时间复杂度为O(n)的操作,所以涉及集合的操作通常都是O(n),以及集合间的聚合操作。例如:HGETALL、SMEMBERS、LRANGE。所以集合全量查询和聚合操作:可以使用SCAN命令,分批读取数据,再在客户端进行聚合计算;删除bigKey,阻塞点删除操作是要释放键值对占用的内存空间,把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程很费时,所以在删除大量元素的集合时肯定会阻塞,也就是bigkey删除也是一个阻塞点。** 可以异步处理来优化。**清空数据库,阻塞点FLUSHDB和FLUSHALL操作也是会阻塞的,因为它需要删除和释放所有的键值对。可以异步处理来优化。磁盘生成RDB快照文件采用fork子进程的方式不会阻塞主线程,不是阻塞点**AOF日志重写操作也是采用子进程方式不会阻塞,不是阻塞点****AOF日志同步写,阻塞点**AOF日志同步写,会根据不同的写回策略对数据做落盘保存。一个同步写磁盘的操作的耗时大约是1~2ms,如果有大量的写操作需要记录在AOF日志中,并同步写回的话,就会阻塞主线程了。所以从库加载RDB文件:把主库的数据量大小控制在2~4GB左右,以保证RDB文件能以较快的速度加载。主从节点主库生成RDB快照文件,并把文件传输给从库,这是fork子进程做的不会阻塞主线程,但是从库收到主库快照文件时,需要清空本地数据,加载RDB文件这个过程对于从库来说是阻塞的。切片集群实例 当部署redis集群切片时,redis实例上分配的哈希槽信息需要在不同实例之间传递,以及负载均衡,实例增删时,数据会在不同实例间传递。不过哈希槽信息不大,而数据迁移是渐进式的,所以说不会阻塞。综上,redis实例的阻塞点在集合全量查询和聚合操作、bigkey删除、清空数据库、AOF日志同步写、从库加载RDB文件。问题:对于redis阻塞点中哪些可以异步操作的?怎么来异步操作呢?答:redis实例的阻塞点在集合全量查询和聚合操作、bigkey删除、清空数据库、AOF日志同步写、从库加载RDB文件。异步操作,也就是它不是主线程的关键操作。集合全量查询和聚合操作,是客户端的读请求,客户端发送了读操作之后,就会等待读取的数据返回,以便进行后续的数据处理。所以它不能异步操作。bigkey删除,删除操作并不需要给客户端返回具体的数据结果,所以可以异步操作。清空数据库,同样删除操作可以异步。AOF日志同步写,为了保证数据可靠性,Redis实例需要保证AOF日志中的操作记录已经落盘,这个操作虽然需要实例等待,但它并不会返回具体的数据结果给实例,可以异步。从库加载RDB文件,从库要想对客户端提供数据读取服务,就必须把RDB文件加载完成。所以,这个操作也属于关键路径上的操作,必须让从库的主线程来执行。异步的子线程机制redis主线程启动后,会通过操作系统pthread_create函数创建3个子线程,分别负责AOF日志写操作、键值对删除以及文件关闭的异步执行。主线程通过一个链表形式的任务队列和子线程交互。主线程收到客户端请求时,比如删除命令,主线程会把删除操作封装成任务放入任务队列,然后回复客户端完成,但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后再删除。执行机制如下图
问题:明明做了数据删除,数据量已经不大了,为什么使用top命令查看时,还会发现Redis占用了很多内存呢?答:redis释放的内存空间由内存分配器管理,不会立刻返回给操作系统。问题:什么是内存碎片化?为什么redis会出现内存碎片化?答:redis内存碎片化和JVM内存碎片化是一样的,就是虽然操作系统剩余的内存总量足够,但是应用申请一块连续的空间,发现操作系统中无法申请一块连续的空间,因为没有这么大的连续空间,那么这些就是内存碎片化了。redis出现内存碎片化主要是两个方面。一个是内存分配器的分配策略造成的,内存分配器是按照固定大小分配的,而不是按照实际申请的大小分配的。比如申请20字节内存,但是分配器分配是按照8字节、16字节、32字节…分配,你申请的20字节,那么内存分配器会给你分配32字节的。那么多出部分就是碎片。另一个是redis键值对删除之后会释放部分空间带来的内存碎片。可以通过INFO memory来判断是否有内存碎片INFO memory# Memoryused_memory:1073741736used_memory_human:1024.00Mused_memory_rss:1997159792used_memory_rss_human:1.86G…mem_fragmentation_ratio:1.86这里有一个mem_fragmentation_ratio的指标,它表示的就是Redis当前的内存碎片率。这个指标就是上面的命令中的两个指标used_memory_rss(操作系统实际分配内存)和used_memory(redis申请的内存)相除的结果。对于这个指标:mem_fragmentation_ratio大于1但小于1.5。这种情况是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由Redis负载决定,也无法限制。所以,存在内存碎片也是正常的。mem_fragmentation_ratio大于 1.5 。这表明内存碎片率已经超过了50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。清理内存碎片方法:重启Redis实例开启redis自动内存碎片清理功能问题:缓存今典问题之如何解决缓存和数据库的数据不一致问题?问题:缓存今典问题之如何解决缓存雪崩难题?答:缓存雪崩是指的部分缓存节点不可以用,导致大量请求无法在redis中处理,进而大量请求打在数据库上,导致数据库层压力激增。缓存雪崩一般有两个原因:第一、缓存中大量数据同时过期,导致大量请求无法处理 针对这种情况导致缓存雪崩,一般有以下解决方案:1.对缓存Key的过期时间做微调,避免同时过期,假如需要同时过期的情况,可以在过期时间上加一点随机数(例如,增加个1~3分钟)这样避免大量缓存Key同时过期。2.服务降级,对于非核心业务,在发生缓存雪崩时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;对于核心业务允许查询缓存以及数据库,这样快速失败(failfast)处理牺牲部分功能,保全整个系统。3.应用中
第二、redis实例发生故障宕机了(比如缓存节点过载被打死了),一般也有两种解决方案:可以使用服务熔断或者请求限流机制 服务熔断是指缓存发生雪崩时,为了防止连锁的数据库崩溃,甚至是整个系统不可用情况,而关闭部分功能的方案。这种方案会使用部分应用不可用(一般时非重点业务)。也可以使用请求限流机制,将请求一点点的访问数据库,保证良好的运行。redis集群增加多个副本,分摊请求,或者一台机器挂了,备用机器提供服务,保证缓存服务器的高可用应用可以采用多级缓存的机制,当然这会带来多级缓存一致性的问题。所以,针对缓存雪崩,合理地设置数据过期时间,以及搭建高可靠缓存集群;问题:缓存今典问题之如何解决缓存击穿难题?答:缓存击穿是指热点Key在redis中过期,此时大量请求访问热点数据在缓存中无法命中,导致大量请求直接访问数据库,数据库压力激增。解决方式就是对于访问特别频繁的热点数据,我们就不设置过期时间了。 所以,针对缓存击穿,在缓存访问非常频繁的热点数据时,不要设置过期时间;问题:缓存今典问题之如何解决缓存穿透难题?答:缓存穿透是指访问了一个不存在的数据,缓存中没有,数据库中也没有,这样会每次请求都会打到数据库,如果大量请求访问不存在的Key,就会导致数据库压力激增。 应对这样的解决方案一般以下几种:缓存空值或缺省值。当没有这个数据时,可以缓存一个空值,那么下次请求就会在redis中命中。但是这种方式也有个问题,就是如果后面这个数据在数据库中有值,那么redis中还是空值,所以一般设置空值的过期时间短一点。使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。所以,针对缓存穿透,提前在入口前端实现恶意请求检测,或者规范数据库的数据删除操作,避免误删除。问题:布隆过滤器工作原理是什么?答:布隆过滤器在我看来其实使用的就是位图 哈希实现的。(在我看来和HashMap部分机制很像)它首先会初始化一个值都是0的bit的字节数组,当需要对某个数据X做标记时,先使用N个哈希函数计算得到哈希值,然后用哈希值对数组取模后找到对应的数组位置,将这个位置标记为1。这样布隆过滤器就设置好了。
所以当检查某个数据是否存在时,只要把数据通过哈希函数找到对应的数组位置,查看对应的位置是否是1就行,只要有一个不是1那么这个数据一定不存在,如果几个哈希函数计算得到的数组位置的值都是1,那么可能存在,因为存在哈希冲突的情况,这个标记为1的操作可能是别的数据计算之后正好也在该数组这个位置,然后标记1的。所以理论上来讲,哈希函数越多,冲突越少,越精准。正是基于布隆过滤器的快速检测特性,我们可以在把数据写入数据库时,使用布隆过滤器做个标记。当缓存缺失后,应用查询数据库时,可以通过查询布隆过滤器快速判断数据是否存在。如果不存在,就不用再去数据库中查询了。这样一来,即使发生缓存穿透了,大量请求只会查询Redis和布隆过滤器,而不会积压到数据库,也就不会影响数据库的正常运行。布隆过滤器可以使用Redis实现,本身就能承担较大的并发访问压力。举一个存储用户信息例子:
布隆过滤器主要有两个缺陷它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中。 主要是Hash算法的问题。因为布隆过滤器是由一个二进制数组和一个Hash算法组成的,Hash算法存在着一定的碰撞几率。Hash碰撞的含义是不同的输入值经过Hash运算后得到了相同的Hash结果。这样就导致了数据A和数据B运算之后是同一个结果,假如A把数组值标记为A,而B没有标记,但是它算出来的也是这个位置,导致误判。不支持删除元素。布隆过滤器不支持删除元素的缺陷也和 Hash 碰撞有关。举一个例子,假如两个元素 A 和 B 都是集合中的元素,它们有相同的 Hash 值,它们就会映射到数组的同一个位置。这时我们删除了 A,数组中对应位置的值也从 1 变成 0,那么在判断 B 的时候发现值是 0,也会判断 B 是不在集合中的元素,就会得到错误的结论。问题:redis哨兵主备切换时数据会丢失吗?答:在主备切换过程中有两种情况会丢失数据:异步复制导致的数据丢失发生原因因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。如何检查如果是这种情况的数据丢失,我们可以通过比对主从库上的复制进度差值来进行判断,也就是计算master_repl_offset和slave_repl_offset的差值。如果从库上的slave_repl_offset小于原主库的master_repl_offset,那么,我们就可以认定数据丢失是由数据同步未完成导致的。脑裂导致的数据丢失发生原因脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里就会有两个master,也就是所谓的脑裂,此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了,因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据,这样也就导致写给原master的数据丢失了。如何检查这个情况可以排查客户端的操作日志时,看看在主从切换后的一段时间内,有没有客户端仍然在和原主库通信,并没有和升级的新主库进行交互,如果有那就表示出现了脑裂。解决数据丢失问题redis提供了两个参数配置来限制主库的请求处理min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送ACK消息的最大延迟(以秒为单位)。min-slaves-to-write和min-slaves-max-lag这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为N和T。这两个配置项组合后的要求是,主库连接的从库中至少有N个从库,和主库进行数据复制时的ACK消息延迟不能超过T秒,否则,主库就不会再接收客户端的请求了。举个例子,假设我们将min-slaves-to-write设置为1,把min-slaves-max-lag设置为12s,把哨兵的down-after-milliseconds设置为10s,主库因为某些原因卡住了15s,导致哨兵判断主库客观下线,开始进行主从切换。同时,因为原主库卡住了15s,没有一个从库能和原主库在12s内进行数据复制,原主库也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主库能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。问题:Redis主从同步与故障切换,有哪些坑?答:主要会遇到以下坑1.主从数据不一致主要原因是主从库间的命令复制是异步进行的,那导致数据不一致主要是以下原因主从库间的网络可能会有传输延迟,所以从库不能及时地收到主库发送的命令,从库上执行同步命令的时间就会被延后。即使从库及时收到了主库的命令,但是,也可能会因为正在处理其它复杂度高的命令(例如集合操作命令)而阻塞。此时,从库需要处理完当前的命令,才能执行主库发送的命令操作,这就会造成主从数据不一致。解决方案1.建议在硬件环境配置方面,尽量保证主从库间的网络连接状况良好。(比如部署在同一个机房,或者是避免把网络通信密集的应用(例如数据分析应用)和Redis主从库部署在一起)2.开发一个外部程序监控从库复制进度(Redis的INFO replication命令可以查看主库接收写命令的进度信息(master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),所以可以开发程序监控),一旦从库复制进度超过阈值(主库的master_repl_offset减去从库的slave_repl_offset所得到的值),不让客户端连接从库。
2.读取过期数据这个问题主要是由于Redis的过期数据删除策略引起,redis提供两种策略,惰性删除策略和定期删除策略。这两种策略都会使得删除的数据不会立刻从redis中删除,而是还会在缓存中。那么从库还是会同步这个数据的,但是从库不会触发删除操作的,所以当客户端读取过期的数据时,如果redis是3.2之前的版本,那么从库就会返回过期数据,如果是3.2以后的版本,从库会判断是否过期了,过期了就会返回空值。那么redis3.2版本就一定安全了呢?不是的,还有一种情况,这种情况就和主从复制有延迟有关了,如果我们设置过期时间使用EXPIRE和PEXPIRE命令,表示多长时间以后过期。由于主从延迟导致读取过期数据。下面举个例子:比如,主库执行60s以后过期,此时是9.03分,那么过期时间是9.04,由于延迟从库获取命令时间是9.04分,执行之后,过期时间是9.05分,这样这个数据就延迟了1分钟。所以建议使用EXPIREAT和PEXPIREAT命令,将过期时间设置某个时间点。
问题:redis cluster 集群原理?答:直接引用网上文章不懂Redis Cluster原理,我被同事diss了!、深度图解Redis Cluster原理、Redis设计篇问题:我曾碰到过这么一个场景:我们使用一个2核CPU、4GB内存、500GB磁盘的云主机运行Redis,Redis数据库的数据量大小差不多是2GB,我们使用了RDB做持久化保证。当时Redis的运行负载以修改操作为主,写读比例差不多在8:2左右,也就是说,如果有100个请求,80个请求执行的是修改操作。你觉得,在这个场景下,用RDB做持久化有什么风险吗?你能帮着一起分析分析吗?问题:使用redis设计实现一个功能,统计每天网站的新客用户和第二日留存用户?(新客表示今天新增用户,留存表示老用户)答:redis知识分析,对于这题,可以使用redis**集合的统计模式,redis集合一般常见四种统计模式:聚合统计、排序统计、二值统计、基数统计。对于设计这个功能可以使用聚合统计。常见的聚合统计包括:统计多个集合的共有元素(交集统计);把两个集合相比,统计其中一个集合独有的元素(差集统计);统计多个集合的所有元素(并集统计)。题目分析,要获取新客用户和留存用户刚好可以使用聚合统计。我们可以用一个集合记录所有登录过网站的用户ID,同时,用另一个集合记录每一天登录过网站的用户ID。比如累计用户key使用user:login,value就是一个Set集合,记录所有用户ID,而每日用户登录也使用一个Set集合存储,假如key使用user:login:20110412,每日登录key**里面包含时间信息。那么新客用户就可以使用以下命令统计:SDIFFSTORE user:new:20110412 user:login:20110412 user:login第二日留存用户,就是第二天用户集合与前一天用户集合的交集:SINTERSTORE user:login:rem user:login:20110412 user:login:20110413但是注意,Set的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致Redis实例阻塞。所以,把数据读取到客户端,在客户端来完成聚合统计。还有一点注意,如果是在集群模式使用多个key聚合计算的命令,一定要注意,因为这些key可能分布在不同的实例上,多个实例之间是无法做聚合运算的,这样操作可能会直接报错或者得到的结果是错误的!当数据量非常大时,使用这些统计命令,因为复杂度较高,可能会有阻塞Redis的风险,建议把这些统计数据与在线业务数据拆分开,实例单独部署,防止在做统计操作时影响到在线业务问题:如何设计并统计一个用户一个月签到的次数?如果记录了1亿个用户10天的签到情况,你有办法统计出这10天连续签到的用户总数吗?答:先说第一个问题,签到打卡场景其实只有两种行为,签到或者未签到。这是典型的二值状态。在redis中有一种统计二值状态的数据结构,位图(Bitmap)。Bitmap本身是用String类型作为底层数据结构实现的一种统计二值状态的数据类型。String类型是会保存为二进制的字节数组,所以,Redis就把字节数组的每个bit位利用起来,用来表示一个元素的二值状态。你可以把Bitmap看作是一个bit数组。Bitmap提供了GETBIT/SETBIT操作,偏移值offset从0开始,对这一位的读写。还有一个BITCOUNT命令,用来统计这个bit数组中所有“1”的个数。那么一个月签到我们可以这么设计。用户ID:1000在2021年4月1号签到了SETBIT uid:sign:1000:202104 0 1用户ID:1000在2021年4月2号未签到SETBIT uid:sign:1000:202104 1 0统计签到次数BITCOUNT uid:sign:1000:202104对于第二个问题,统计10天连续签到。Bitmap支持用BITOP命令对多个Bitmap按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的Bitmap中。以按位与操作为例,下图可以看到,三个Bitmapbm1、bm2和bm3,对应bit位做“与”操作,结果保存到了一个新的Bitmap中。
回到刚刚的问题,在统计1亿个用户连续10天的签到情况时,可以把每天的日期作为key,每个key对应一个1亿位的Bitmap,每一个bit对应一个用户当天的签到情况。然后对10个Bitmap做按位与操作,每一位与操作结果表示一个用户10天签到的情况,得到一个新的Bitmap,这个Bitmap就代表了1亿用户10天签到情况了,1表示10天连续签到,0表示未连续签到,再使用BITCOUNT获取1的数量就可以了。内存消耗情况,每天使用1个1亿位的Bitmap,大约占12MB的内存(10^8/8/1024/1024),10天的Bitmap的内存开销约为120MB,内存压力不算太大。不过,在实际应用时,最好对Bitmap设置过期时间,让Redis自动删除不再需要的签到记录,以节省内存开销。问题:如何实现附近的加油站?答:reids提供了GEO数据类型,严格来说它不是一种新的数据类型,它的底层是使用Sort set数据结构的。在GEO中我们可以用到两个命令,分别是GEOADD和GEORADIUS。GEOADD命令:用于把一组经纬度信息和相对应的一个ID记录到GEO类型集合中;GEORADIUS命令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内的其他元素。当然,我们可以自己定义这个范围。可以把加油站经纬度位置是(116.034579,39.030452)通过GEOADD命令加入集合中,如GEOADD gasstation:locations 116.034579 39.030452假如要获取用户(经纬度信息:116.054579,39.030452 )附近5公里的加油站,就可以使用以下命令:GEORADIUS gasstation:locations 116.054579 39.030452 5 km ASC COUNT 10以上命令中可以使用ACS选项,让返回的加油站按照距离这个中心位置从近到远的方式来排序,以方便选择最近的加油站;还可以使用COUNT选项,指定返回的加油站的数量。问题:redis适合做消息队列吗?如何使用它做消息队列?答:分析redis是否适合做消息队列那就需要知道消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。那就看看redis的两种数据类型,List和Streams如何做消息队列。基于List的消息队列解决方案有序性保证List本身就是按照先进先出的顺序存取数据的,所以它本身就是有序的。生产者使用LPUSH把消息写入队列,消费者使用RPOP把消息从队列中读取出来。如下图,生产者发送消息5,3。消费者依次取出5,3。
但是要注意使用List方式有性能风险。因为List没有等待唤醒机制,也就是说生产者将消息写入队列,消息消费者并不能及时知晓,想要及时消费消息,那么消息消费者就要不停的获取消息,也就是在while循环里不停的使用RPOP获取消息。这会十分消耗CPU资源,所以,redis提供了BRPOP命令。**BRPOP**命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据,节省CPU开支。消息不重复处理保证保证消息不重复消费就是要保证幂等性。redis中的List并没有这种机制保证,所以需要生产者和消费者自己保证。比如,一方面生产者可以在每个消息上携带全局唯一的ID,另一方面,消费者需要记录自己消费了的全局唯一ID来判断下个消息是否重复。消息可靠性保证消费者从List中获取一条消息后,List不会再有这条消息的留存了,但是如果消费者获取了消息,但是还没来得及处理消费者就宕机了,那么这条消息就丢失了。为了解决留存问题。List类型提供了BRPOPLPUSH命令,这个命令的作用是让消费者程序从一个List中读取消息,同时,Redis会把这个消息再插入到另一个List(可以叫作备份List)留存。这样消息就不会丢失了。下面画图解释
所以,List是满足消息队列的三个条件的。但是它也有缺点,就是如果生产者消息发送很快,而消费者处理消息的速度比较慢,这就导致List中的消息越积越多,给Redis的内存带来很大压力,我们希望能启动多个消费者组成一个消费者组来消费数据,但是可惜List并没有提供。但是redis5.0提供了Streams。基于Streams的消息队列解决方案**Streams是Redis专门为消息队列设计的数据类型**,它提供了丰富的消息队列操作命令。XADD:插入消息,消息格式是键值对形式,保证有序,可以自动生成全局唯一ID;例如,执行以下命令,往**消息队列名称为mqstream**中插入数据,键为test,值hello。其中消息队列名称后面的*,表示让Redis为插入的数据自动生成一个全局唯一的ID,也可以自己设置把*替换成自己的ID。XADD mqstream * test hello “1599203861727-0″返回结果是1599203861727-0,分为两个部分,1599203861727表示插入数据时毫秒值,0表示这个时间点的第1条消息。XREAD:用于读取消息,可以按ID读取数据; 使用XREAD读取消息,命令:XREAD BLOCK 1000 STREAMS mqstream 1599203861727-0,获取消息IID:1599203861727-0的消息。消费者也可以在调用XRAED时设定block配置项,实现类似于BRPOP的阻塞读取操作。当消息队列中没有消息时,一旦设置了block配置项,XREAD就会阻塞,阻塞的时长可以在block配置项进行设置,上面那条命令中1000表示等待1秒钟,如果想读取最新消息,则把ID换成$,XREAD block 10000 streams mqstream $。XREADGROUP:按消费组形式读取消息; Streams特有的功能消费者组,可以使用XGROUP创建消费者组,使用XREADGROUP读取消费者组消息。如,XGROUP create mqstream group1 0,创建一个名为group1的消费组,这个消费组消费的消息队列是mqstream。如果让group1消费组里的消费者consumer1从mqstream中读取所有消息,其中,命令最后的参数“>”,如,XREADGROUP group group1 consumer1 streams mqstream >。使用消费者consumer1读取。需要注意的是,消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了。如果要让组中每个消费者读取一条消息,如,XREADGROUP group group2 consumer1 count 1 streams mqstream >,XREADGROUP group group2 consumer2 count 1 streams mqstream >。XPENDING和XACK:XPENDING命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而XACK命令用于向消息队列确认消息处理已完成。为了保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息,Streams会自动使用内部队列(也称为PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用XACK命令通知Streams“消息已经处理完成”。如果消费者没有成功处理消息,它就不会给Streams发送XACK命令,消息仍然会留存。此时,消费者可以在重启后,用XPENDING命令查看已读取、但尚未确认处理完成的消息。两种方式比较
问题: 如果一个生产者发送给消息队列的消息,需要被多个消费者进行读取和处理(例如,一个消息是一条从业务系统采集的数据,既要被消费者1读取进行实时计算,也要被消费者2读取并留存到分布式文件系统HDFS中,以便后续进行历史查询),你会使用Redis的什么数据类型来解决这个问题呢?问题:简单介绍一下JedisCluster的工作原理?答:在JedisCluster初始化的时候,就会随机选择一个node,初始化hashslot -> node映射表,同时为每个节点创建一个JedisPool连接池。每次基于JedisCluster执行操作,首先JedisCluster都会在本地计算key的hashslot,然后在本地映射表找到对应的节点。如果那个node正好还是持有那个hashslot,那么就ok; 如果说进行了reshard这样的操作,可能hashslot已经不在那个node上了,就会返回moved。如果JedisCluter API发现对应的节点返回moved,那么利用该节点的元数据,更新本地的hashslot -> node映射表缓存。重复上面几个步骤,直到找到对应的节点,如果重试超过5次,那么就报错,JedisClusterMaxRedirectionExceptionjedis老版本,可能会出现在集群某个节点故障还没完成自动切换恢复时,频繁更新hash slot,频繁ping节点检查活跃,导致大量网络IO开销。jedis最新版本,对于这些过度的hash slot更新和ping,都进行了优化,避免了类似问题。内部原理篇问题:Redis字符串内部实现是什么样的?答:Redis的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
问题:你了解Redis的底层数据结构?答:String(字符串)、List(列表)、Hash(哈希)、Set(集合)和Sorted Set(有序集合)分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。
由上面可以知道,只有String的底层是一种数据类型,而其他的底层都有两种数据结构实现。问题:Redis键和值本身是用什么数据结构呢?答:为了实现从键到值的快速访问,Redis使用了一个哈希表来保存所有键值对。一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。而且哈希桶中保存的也不是值数据,而是数据的指针。所以可以通过O(1)的时间复杂度很快找到键值对。
问题:既然Redis键值存储使用哈希表,时间复杂度为O(1),那为什么写入大量数据后,操作会变慢呢?答:那是因为写入大量数据后,会带来一些问题,那就是哈希表的冲突问题和rehash可能带来的操作阻塞。redis键值的存储和java中的Map类似,都是通过”数组 链表”的形式。哈希桶的个数通常要少于key的数量,这也就是说,难免会有一些key的哈希值对应到了同一个哈希桶中。
而当桶中的元素太多时,哈希表就会做一次rehash操作。所以,当数据量大是,redis的操作就会变慢一点。其一是由于冲突后每个桶元素过多,只能通过指针逐一查找,这个过程时间复杂度是O(n),也就是随着元素越多,越耗时。其二是当数据量特别多时redis会做rehash操作。问题:Redis是如何做rehash操作的?答:Redis中rehash是一种渐进式rehash**。 为了使rehash操作更高效,Redis默认使用了两个全局哈希表:哈希表1和哈希表2。一开始,当你刚插入数据时,默认使用哈希表1,此时的哈希表2并没有被分配空间。随着数据逐步增多,Redis开始执行rehash**,这个过程分为三步:给哈希表2分配更大的空间,例如是当前哈希表1大小的两倍;把哈希表1中的数据重新映射并拷贝到哈希表2中;释放哈希表1的空间。而redis为了让rehash的时候阻塞客户端请求,所以采用渐进式rehash。也就是在rehash的时候也可以处理客户端请求,每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有entries拷贝到哈希表2中;等处理下一个请求时,再顺带拷贝哈希表1中的下一个索引位置的entries。
这样把一次性拷贝的开销分担到多次请求上,避免了耗时。问题:Redis集合数据结构(List、Set、Hash…)数据操作效率和String类型数据结构操作效率有何不同?答:Redis键值存储是通过哈希表实现的,那么字符串类型的操作效率就是相当于获取哈希桶中元素的效率,也就是O(1) O(n),O(1)是通过key找到哈希桶的效率,O(n)是找到哈希桶后获取元素的效率。而集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。那么首先集合的操作效率就与集合的底层数据结构有关,例如,使用哈希表实现的集合(Hash),要比使用链表(List)实现的集合访问效率更高。第二,操作效率和这些操作本身的执行特点有关,比如读写一个元素的操作要比读写所有元素的效率高。问题:Redis底层数据结构时间复杂度对比?答:redis除了简单动态字符串之外,还有整数数组、双向链表、哈希表、压缩列表和跳表等底层数据结构。 整数数组、双向链表、哈希表这三种很常见,它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是O(N),操作效率比较低。压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段zlbytes、zltail和zllen,分别表示列表长度、列表尾的偏移量和列表中的entry个数;压缩列表在表尾还有一个zlend,表示列表结束。
所以访问第一个元素和最后一个元素通过前三个字段很快访问,时间复杂度O(1),其他元素O(n)。跳表是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。如下图:
以下看一下每种数据结构底层结构的时间复杂度:
问题:每种数据结构操作命令的时间复杂度?答:一般可以分为以下几种。单元素操作,是指每一种集合类型对单个数据实现的增删改查操作。这类操作一般与数据结构有关,也就是数据结构的时间复杂度,但是集合类型支持同时对多个元素进行增删改查,例如Set类型的SADD支持同时增加多个元素。此时,这些操作的复杂度,就是由单个元素操作复杂度和元素个数决定的。例如,HMSET增加M个元素时,复杂度就从O(1)变成O(M)了。范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据,比如Hash类型的HGETALL和Set类型的SMEMBERS,或者返回一个范围内的部分数据,比如List类型的LRANGE和ZSet类型的ZRANGE。这类操作的复杂度一般是O(N),比较耗时,我们应该尽量避免。统计操作,是指集合类型对集合中所有元素个数的记录,例如LLEN和SCARD。这类操作复杂度只有O(1)。例外情况,是指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于List类型的LPOP、RPOP、LPUSH、RPUSH这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有O(1),可以实现快速操作。问题:Redis中GEO结构是新的数据结构吗?它是如何实现的?答:Geo不是一种新的数据结构,它的底层是通过Sorted set和GeoHash编码来实现的,Sorted Set很熟悉就是有序集合,一般是这样使用的,Sorted Set的key存储地理位置的编号,比如某个公司的地理位置,那么这个key就是公司名称或者公司编号,总之可以定位一家公司的标识。而value也就是权重分数是经纬度信息。
这时问题来了,Sorted Set元素的权重分数是一个浮点数(float类型),而一组经纬度包含的是经度和纬度两个值,是没法直接保存为一个浮点数的,所以GeoHash编码就登上舞台了。GeoHash的编码方法基本原理就是二分区间,区间编码。对一组经纬度进行GeoHash编码时,要先对经度和纬度分别编码,然后再把经纬度各自的编码组合成一个最终编码。 经度范围是[-180,180],GeoHash编码会把一个经度值编码成一个N位的二进制值,我们来对经度范围[-180,180]做N次的二分区操作。如果当前的经度在范围的左区间,用0表示,在右区间用1表示。 举例: 假设对经纬度[116.37,39.86],做5次二分,先看经度116.37,第一次分出范围是[-180,0]和[0,180],而116.37在[0,180]区间,所以用1表示,继续对[0,180]做二分,第二次分出[0,90]和[90,180],很明显116.37还在右区间,所以还是用1表示,下面依次二分。如下图:
对纬度同理,只是纬度范围在[-90,90]可以分成如下图:
当经纬度各自编码之后再对他门组合,规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从0开始,奇数位从1开始。经纬度(116.37,39.86)的各自编码值是11010和10111,组合之后,第0位是经度的第0位1,第1位是纬度的第0位1,第2位是经度的第1位1,第3位是纬度的第1位0,以此类推,就能得到最终编码值1110011101。
BAT等大厂Java面试经验总结
想获取 Java大厂面试题学习资料
扫下方二维码回复「BAT」就好了
回复【加群】获取github掘金交流群
回复【电子书】获取2020电子书教程
回复【C】获取全套C语言学习知识手册
回复【Java】获取java相关的视频教程和资料
回复【爬虫】获取SpringCloud相关多的学习资料
回复【Python】即可获得Python基础到进阶的学习教程