点击上方“芋道源码”,选择“设为星标”
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 10:33 更新文章,每天掉亿点点头发...
源码精品专栏

三万字长文,建议收藏,方便查阅
来吧,缓存面试
问题:什么是redis?
答:Redis 是一个基于内存 的高性能key-value数据库 。
redis可以用在哪些业务上?答:redis可以做很多事情,比如:
以下是具体业务场景:
Redis有哪些数据结构?答:Redis是一种Key-Value的模型,key是字符串类型,而常说的数据结构一般是指value的数据结构,一般包含以下类型。最普通常见的,字符串(String),字典(Hash),列表(List),集合(Set),有序集合(SortedSet)。高级数据结构,HyperLogLog,Geo,bitmap更高级用户可能还知道Redis Module,像 BloomFilter,RedisSearch,Redis-ML。
Redis有哪些好处?答:
HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)string,list,set,sorted set,hashredis相比memcached有哪些优势?答:优势如下
memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型redis的速度比memcached快很多redis可以持久化其数据redis有什么缺点?答:主要有以下四点缺点:
Redis提供了几种数据淘汰策略?该怎么选择?答:
volatile-lru:从已经设置过期时间的数据集中,挑选最近最少使用的数据淘汰。volatile-ttl:从已经设置过期时间的数据集中,挑选即将要过期的数据淘汰。volatile-random:从已经设置过期时间的数据集中,随机挑选数据淘汰。volatile-lfu:从已经设置过期时间的数据集中,会使用LFU算法选择设置了过期时间的键值对。allkeys-lru:从所有的数据集中,挑选最近最少使用的数据淘汰。allkeys-random:从所有的数据集中,随机挑选数据淘汰。no-enviction:禁止淘汰数据,如果redis写满了将不提供写请求,直接返回错误。注意这里的6种机制,volatile和allkeys规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。

使用策略规则 :
allkeys-lruallkeys-randomredis需要把所有数据放到内存中?答:Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。
Redis是单线程的吗?答:Redis是单线程的,主要是指Redis的网络IO和键值对读写 是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程。但Redis的其他功能,比如持久化、异步删除、集群数据同步 等,其实是由额外的线程执行的。
Redis6.0之前为什么一直不使用多线程?答:官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。
使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。
Redis6.0为什么要引入多线程呢?答:Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。
但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。
从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:
协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因:
答:Redis6.0的多线程默认是禁用的,只使用主线程。如需开启需要修改redis.conf配置文件:io-threads-do-reads yes
答:开启多线程后,还需要设置线程数,否则是不生效的。同样修改redis.conf配置文件 关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。
答:Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试,GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。
测试环境:Redis Server: 阿里云 Ubuntu 18.04,8 CPU 2.5 GHZ, 8G 内存,主机型号 ecs.ic5.2xlarge Redis Benchmark Client: 阿里云 Ubuntu 18.04,8 2.5 GHZ CPU, 8G 内存,主机型号 ecs.ic5.2xlarge 测试结果:

详见:zhuanlan.zhihu.com/p/76788470
答:如图

流程简述如下:

该设计有如下特点:
答:从上面的实现机制可以看出,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的做法就是增加副本冗余量 ,将一份数据同时保存在多个实例上。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
redis的并发竞争问题如何解决?答:Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。
对此有两种解决方法:
Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。setnx实现锁。注:对于第一种,需要应用程序自己处理资源的同步,可以使用的方法比较通俗,可以使用synchronized也可以使用lock;第二种需要用到Redis的setnx命令,但是需要注意一些问题。
redis过期键的删除策略?答:
timer,让定时器在键的过期时间到达时,立即执行对键的删除操作。(主动删除) 对内存友好,但是对cpu时间不友好,有较多过期键的而情况下,删除过期键会占用相当一部分cpu时间。cpu时间友好,程序只会在取出键的时候才会对键进行过期检查,这不会在删除其他无关过期键上花费任何cpu时间,但是如果一个键已经过期,而这个键又保留在数据库中,那么只要这个过期键不被删除,他所占用的内存就不会释放,对内存不友好。cpu时间折中的方法,每个一段时间执行一次删除过期键操作,并通过限制操作执行的时长和频率来减少对cpu时间的影响。难点在于,选择一个好的策略来设置删除操作的时长和执行频率。答:哨兵是对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是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
答:
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持久化机制的优缺点?答:优缺点如下。
RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒AOF持久化机制的优缺点?答:优缺点如下:
AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据AOF日志文件通常比RDB数据快照文件更大AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的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。redis在快照时数据不能修改,那无疑是给业务造成损失。也许你会说可以使用bgsave来避免阻塞,但是注意,避免阻塞和正常处理写操作并不是一回事 。如果数据不能修改,虽然没有阻塞,但是主线程只能接受读请求,而写请求不能执行。这肯定不能接受,所以,Redis就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
如图所示,如果主线程读取数据A,主线程和bgsave互不影响。如果修改数据C,这个数据就复制一份,生成数据副本。bgsave子进程把副本写入RDB文件,主线程修改数据C不影响。
bgsave执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。
所以,我们要做增量快照 ,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。那如何记住后续的修改操作呢?redis4.0提出混合使用AOF日志和内存快照的方法 。就是内存快照以一定的频率执行,在两次快照之间,使用AOF日志记录这期间的所有命令操作。这样快照不需要频繁执行,AOF只需要记录两次快照间的操作。所以AOF文件也不会过大。
bgsave子进程需要通过fork操作从主线程创建出来。fork这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。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上复制数据。
如上图,可以分为以下步骤:
注意:FULLRESYNC响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。
主库执行bgsave命令,生成RDB文件,接着将文件发给从库,从库接收RDB文件后清空数据库,然后加载RDB文件。主库数据同步过程中,仍然可以接收请求,这些请求没有记录在刚刚生成的RDB文件中,所以主库在内存中有一个replication buffer用于记录RDB文件生成后收到的所有写操作。
replication buffer中的命令发送给从库,从库重新执行操作,这样主从就一致了。psync命令表示数据同步,主库根据psync命令的参数来启动复制。参数主要包含主库的runID 和复制进度offset 。主库收到psync命令后,会用FULLRESYNC 响应命令带上两个参数:主库runID 和主库目前的复制进度offset ,返回给从库。从库收到响应后,会记录下这两个参数。redis就忙这fork子进程生成RDB文件了,fork操作会阻塞主线程的,而且传输RDB文件也会占用主库网络带宽。所以就有了另一种办法:主从级联模式分担全量复制时的主库压力 ,什么意思呢?就是再选择一个从库,将这个从库作为其他从库的主库。那么就由这个从库为其他从库同步数据。
主库同步数据给从库,从库又同步数据给其他从库,这个过程也称为基于长连接的命令传播 ,可以避免频繁建立连接的开销。
repl_backlog_buffer是环形的,所以它会覆盖掉之前写的。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。这种情况可以调整repl_backlog_size参数大小。计算公式是 :缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即repl_backlog_size = 缓冲空间大小 * 2,这也就是repl_backlog_size的最终值。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。
回顾下增量复制的流程

综上,整个redis数据同步问题就解释清楚了。
答:主从集群方式一般是一主多从的模式,主库可接收读/写请求,并把数据同步给从库,从库只能接收读请求。那么这里就有个问题,如果主库挂了呢?那么就无法接收写请求了。

答:因为主从集群模式,如果主库挂了,就无法提供正常的写功能了。这里涉及了三个问题:
所以,哨兵机制就是解决上面这三个问题而出现的,哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。就围绕这三个任务来看一下哨兵机制流程。

PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态 。如果发现PING命令响应超时,哨兵就会把它标记为主观下线 。为了防止哨兵误判
哨兵误判就是主库并没有故障,误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下,而选主的代价比较高。
一般都会会采用多实例组成的集群模式进行部署,这也就是哨兵集群 。当多数哨兵标记主库为主观下线时,那么主库就会被标记为客观下线 ,也就是表示主库下线是客观事实的。判断的原则就是少数服从多数 。下面再给张图帮助理解:
down-after-milliseconds * 10配置,down-after-milliseconds是我们认定主从库断连的最大连接超时时间。如果在down-after-milliseconds毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了10次,就说明这个从库的网络状况不好,不适合作为新主库。
总的来说,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库。slave-priority配置从库优先级,如果你有台从库实例,配置比较高,你就可以手动设置它优先级,那么在哨兵选主的时候,它就会胜出。repl_backlog_buffer这里的数据同步程度,可以通过从库slave_repl_offset与旧主库的master_repl_offset值是否相近来判断接近程度。如下图,从库2就胜出称为新主库。
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实例会和以下对象发生交互。
redis使用了IO多路复用,避免了主线程一直等待网络连接或者请求到来的状态。所以网络IO它不是一个阻塞点。redis和客户端的主要交互对象,那么复杂度高的操作一定会阻塞redis主线程,所以要留意时间复杂度为O(n)的操作,所以涉及集合的操作通常都是O(n),以及集合间的聚合操作。例如:HGETALL、SMEMBERS、LRANGE。所以集合全量查询和聚合操作:可以使用SCAN命令,分批读取数据,再在客户端进行聚合计算;bigKey,阻塞点删除操作是要释放键值对占用的内存空间,把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程很费时,所以在删除大量元素的集合时肯定会阻塞,也就是bigkey删除也是一个阻塞点。** 可以异步处理来优化。**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日志写操作、键值对删除以及文件关闭的异步执行。主线程通过一个链表形式的任务队列和子线程交互。主线程收到客户端请求时,比如删除命令,主线程会把删除操作封装成任务放入任务队列,然后回复客户端完成,但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后再删除。执行机制如下图

答:redis释放的内存空间由内存分配器管理,不会立刻返回给操作系统。
答:redis内存碎片化和JVM内存碎片化是一样的,就是虽然操作系统剩余的内存总量足够,但是应用申请一块连续的空间,发现操作系统中无法申请一块连续的空间,因为没有这么大的连续空间,那么这些就是内存碎片化了。redis出现内存碎片化主要是两个方面。
redis键值对删除之后会释放部分空间带来的内存碎片。可以通过INFO memory来判断是否有内存碎片
INFO memory
> 基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。
>
> 项目地址:<https://github.com/YunaiV/onemall>
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_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集群增加多个副本,分摊请求,或者一台机器挂了,备用机器提供服务,保证缓存服务器的高可用所以,针对缓存雪崩,合理地设置数据过期时间,以及搭建高可靠缓存集群;
答:缓存击穿是指热点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没有标记,但是它算出来的也是这个位置,导致误判 。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主从同步与故障切换,有哪些坑?答:主要会遇到以下坑
Redis主从库部署在一起)Redis的INFO replication命令可以查看主库接收写命令的进度信息(master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),所以可以开发程序监控),一旦从库复制进度超过阈值(主库的master_repl_offset减去从库的slave_repl_offset所得到的值),不让客户端连接从库。
Redis的过期数据删除策略引起,redis提供两种策略,惰性删除策略和定期删除策略。这两种策略都会使得删除的数据不会立刻从redis中删除,而是还会在缓存中。那么从库还是会同步这个数据的,但是从库不会触发删除操作的,所以当客户端读取过期的数据时,如果redis是3.2之前的版本,那么从库就会返回过期数据,如果是3.2以后的版本,从库会判断是否过期了,过期了就会返回空值。那么redis3.2版本就一定安全了呢?不是的,还有一种情况,这种情况就和主从复制有延迟有关了,如果我们设置过期时间使用EXPIRE和PEXPIRE命令,表示多长时间以后过期。由于主从延迟导致读取过期数据。下面举个例子:比如,主库执行60s以后过期,此时是9.03分,那么过期时间是9.04,由于延迟从库获取命令时间是9.04分,执行之后,过期时间是9.05分,这样这个数据就延迟了1分钟。所以建议使用EXPIREAT和PEXPIREAT命令,将过期时间设置某个时间点。
protected-mode和cluster-node-timeout。protected-mode 配置项
这个配置项的作用是限定哨兵实例能否被其他服务器访问。当这个配置项设置为yes时,哨兵实例只能在部署的服务器本地进行访问。当设置为no时,其他服务器也可以访问这个哨兵实例。
所以,如果protected-mode被设置为yes,而其余哨兵实例部署在其它服务器,那哨兵节点就无法通信了。protected-mode这个配置项应该是no,让其他服务器都能访问,但是要将bind配置项设置为其它哨兵实例的IP地址。这样只有绑定了IP的哨兵才可以访问,这样保证了安全又保证了哨兵间的通信。cluster-node-timeout 配置项 这个配置项设置了Redis Cluster中实例响应心跳消息的超时时间。当我们在Redis Cluster集群中为每个实例配置了“一主一从”模式时,如果主实例发生故障,从实例会切换为主实例,受网络延迟和切换操作执行的影响,切换时间可能较长,就会导致实例的心跳超时(超出cluster-node-timeout)。实例超时后,就会被Redis Cluster判断为异常。而Redis Cluster正常运行的条件就是,有半数以上的实例都能正常运行。所以,如果执行主从切换的实例超过半数,而主从切换时间又过长的话,就可能有半数以上的实例心跳超时,从而可能导致整个集群挂掉。所以,我建议你将cluster-node-timeout调大些(例如10到20秒)。综上,遇到的坑总结:

答:直接引用网上文章 不懂Redis Cluster原理,我被同事diss了!、深度图解Redis Cluster原理、
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的风险,建议把这些统计数据与在线业务数据拆分开,实例单独部署,防止在做统计操作时影响到在线业务
答:先说第一个问题,签到打卡场景其实只有两种行为,签到或者未签到。这是典型的二值状态 。在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中。以按位与操作为例,下图可以看到,三个Bitmap bm1、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 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。XPENDING和XACK:XPENDING命令可以用来查询每个消费组内所有消费者已读取但尚未确认的消息,而XACK命令用于向消息队列确认消息处理已完成。为了保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息,Streams会自动使用内部队列(也称为PENDING List)留存消费组里每个消费者读取的消息,直到消费者使用XACK命令通知Streams“消息已经处理完成”。如果消费者没有成功处理消息,它就不会给Streams发送XACK命令,消息仍然会留存。此时,消费者可以在重启后,用XPENDING命令查看已读取、但尚未确认处理完成的消息。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 >。XADD:插入消息,消息格式是键值对形式,保证有序,可以自动生成全局唯一ID;例如,执行以下命令,往消息队列名称为mqstream 中插入数据,键为test,值hello 。其中消息队列名称后面的*,表示让Redis为插入的数据自动生成一个全局唯一的ID,也可以自己设置把*替换成自己的ID。
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次,那么就报错,JedisClusterMaxRedirectionException
jedis老版本,可能会出现在集群某个节点故障还没完成自动切换恢复时,频繁更新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的操作就会变慢一点。
redis会做rehash操作。Redis是如何做rehash操作的?答:Redis中rehash是一种渐进式rehash 。为了使rehash操作更高效,Redis默认使用了两个全局哈希表:哈希表1和哈希表2。一开始,当你刚插入数据时,默认使用哈希表1,此时的哈希表2并没有被分配空间。随着数据逐步增多,Redis开始执行rehash,这个过程分为三步:
而redis为了让rehash的时候阻塞客户端请求,所以采用渐进式rehash。也就是在rehash的时候也可以处理客户端请求,每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有entries拷贝到哈希表2中;等处理下一个请求时,再顺带拷贝哈希表1中的下一个索引位置的entries。

这样把一次性拷贝的开销分担到多次请求上,避免了耗时。
Redis集合数据结构(List、Set、Hash...)数据操作效率和String类型数据结构操作效率有何不同?答:Redis键值存储是通过哈希表实现的,那么字符串类型的操作效率就是相当于获取哈希桶中元素的效率,也就是O(1) + O(n) ,O(1)是通过key找到哈希桶的效率,O(n)是找到哈希桶后获取元素的效率。而集合类型 的值,第一步是通过全局哈希表找到对应的哈希桶位置 ,第二步是在集合中再增删改查。
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。

这就是GeoHash编码,就可以使用最后编码值表示浮点数的权重分数了。
答:
RDB,写入一些数据,然后kill -9杀掉redis进程,接着重启redis,发现数据没了,因为RDB快照还没生成AOF的开关,启用AOF持久化AOF文件中的日志内容 其实你在appendonly.aof文件中,可以看到刚写的日志,它们其实就是先写入os cache的,然后1秒后才fsync到磁盘中,只有fsync到磁盘中了,才是安全的,要不然光是在os cache中,机器只要重启,就什么都没了kill -9杀掉redis进程,重新启动redis进程,发现数据被恢复回来了,就是从AOF文件中恢复回来的redis进程启动的时候,直接就会从appendonly.aof中加载所有的日志,把内存中的数据恢复回来。
AOF rewrite重写如何配置?答:redis 2.4之前,还需要手动,开发一些脚本,比如定时任务crontab,通过BGREWRITEAOF命令去执行AOF rewrite,但是redis 2.4之后,会自动进行rewrite操作 在redis.conf中,可以配置rewrite策略
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb比如说上一次AOF rewrite之后,是128mb
然后就会接着128mb继续写AOF的日志,如果发现增长的比例,超过了之前的100%,已经达到256mb,就可能会去触发一次rewrite。但是此时还要去跟min-size,64mb去比较,因为256mb > 64mb,所以才会去触发rewrite。重写步骤 :
redis fork一个子进程。AOF文件中写入日志。redis主进程,接收到client新的写操作之后,在内存中写入日志,同时新的日志也继续写入旧的AOF文件。redis主进程将内存中的新日志再次追加到新的AOF文件中。欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:

已在知识星球更新源码解析如下:




最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
文章有帮助的话,在看,转发吧。谢谢支持哟 (*^__^*)