-
1. Redis主从
-
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
-
1.1.主从集群结构
-
如果是写操作,应该访问master节点,master会自动将数据同步给两个slave节点.
-
如果是读操作,建议访问各个slave节点(现在叫replica),从而分担并发压力
-
-
1.2搭建主从集群
-
利用3个docker 容器来创建主从集群
-
1.2.1.启动多个Redis实例
-
创建 docker-compose.yaml文件
-
内容如下:
-
version: "3.2" services: r1: image: redis container_name: r1 network_mode: "host" entrypoint: ["redis-server", "--port", "7001"] r2: image: redis container_name: r2 network_mode: "host" entrypoint: ["redis-server", "--port", "7002"] r3: image: redis container_name: r3 network_mode: "host" entrypoint: ["redis-server", "--port", "7003"]
-
- 将文件传入虚拟机 然后再该文件所在目录执行命令:
docker compose up -d
-
-
1.2.2.建立集群
- 通过以下命令来形成主从关系:
# Redis5.0以前 slaveof <masterip> <masterport>
# Redis5.0以后 replicaof <masterip> <masterport>
-
有临时和永久两种模式:
-
永久生效:在redis.conf文件中利用
slaveof
命令指定master
节点 -
临时生效:直接利用redis-cli控制台输入
slaveof
命令,指定master
节点
-
- 通过以下命令来形成主从关系:
-
1.3.主从同步原理
-
1.3.1.全量同步
-
主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点,流程:
-
master判断一个节点是否是第一次同步的依据,就是看replid是否一致
-
-
1.3.2.增量同步
-
全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。
-
1.3.3.repl_baklog原理
-
这就要说到全量同步时的
repl_baklog
文件了。这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。 -
repl_baklog
中会记录Redis处理过的命令及offset
,包括master当前的offset
,和slave已经拷贝到的offset
:-
-
slave与master的offset之间的差异,就是salve需要增量拷贝的数据了
-
-
-
-
1.4.主从同步优化
-
主从同步可以保证主从数据的一致性,非常重要。
-
可以从以下几个方面来优化Redis主从就集群:
- 在master中配置repl-diskless-sync yes 启用无磁盘复制,避免全量同步时的磁盘IO。
-
Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
-
适当提高
repl_baklog
的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步 -
限制一个master上的slave节点数量,如果实在是太多slave,则可以采用
主-从-从
链式结构,减少master压力 -
主-从-从
架构图: -
简述全量同步和增量同步区别?
-
全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
-
增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
-
什么时候执行全量同步?
-
slave节点第一次连接master节点时
-
slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
-
什么时候执行增量同步?
-
slave节点断开又恢复,并且在
repl_baklog
中能找到offset时
-
-
2. Redis哨兵
-
2.1.哨兵工作原理
Redis提供了
哨兵
(Sentinel
)机制来监控主从集群监控状态,确保集群的高可用性。 -
2.1.1.哨兵作用
-
哨兵集群作用原理图:
-
哨兵的作用如下:
-
状态监控:
Sentinel
会不断检查您的master
和slave
是否按预期工作 -
故障恢复(failover):如果
master
故障,Sentinel
会将一个slave
提升为master
。当故障实例恢复后会成为slave
-
状态通知:
Sentinel
充当Redis
客户端的服务发现来源,当集群发生failover
时,会将最新集群信息推送给Redis
的客户端
-
-
2.1.2.状态监控
-
Sentinel
基于心跳机制监测服务状态,每隔1秒向集群的每个节点发送ping命令,并通过实例的响应结果来做出判断: -
主观下线(sdown):如果某sentinel节点发现某Redis节点未在规定时间响应,则认为该节点主观下线。
-
客观下线(odown):若超过指定数量(通过
quorum
设置)的sentinel都认为该节点主观下线,则该节点客观下线。quorum值最好超过Sentinel节点数量的一半,Sentinel节点数量至少3台。 -
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
-
首先会判断slave节点与master节点断开时间长短,如果超过
down-after-milliseconds * 10
则会排除该slave节点 -
然后判断slave节点的
slave-priority
值,越小优先级越高,如果是0则永不参与选举(默认都是1)。 -
如果
slave-prority
一样,则判断slave节点的offset
值,越大说明数据越新,优先级越高 -
最后是判断slave节点的
run_id
大小,越小优先级越高(通过info server可以查看run_id
)。
-
-
-
2.1.3.选举leader
-
问题来了,当选出一个新的master后,该如何实现身份切换呢?
大概分为两步:
-
在多个
sentinel
中选举一个leader
-
由
leader
执行failover
-
- 第一个确认master客观下线的人会立刻发起投票,一定会成为leader。
-
-
2.1.4.failover
-
我们举个例子,有一个集群,初始状态下7001为
master
,7002和7003为slave
: -
假如master发生故障,slave1当选。则故障转移的流程如下:
1)
sentinel
给备选的slave1
节点发送slaveof no one
命令,让该节点成为master
-
-
sentinel
给所有其它slave
发送slaveof 192.168.150.101 7002
命令,让这些节点成为新master
,也就是7002
的slave
节点,开始从新的master
上同步数据。 -
最后,当故障节点恢复后会接收到哨兵信号,执行
slaveof 192.168.150.101 7002
命令,成为slave
: -
2.2.总结
-
Sentinel的三个作用是什么?
-
集群监控
-
故障恢复
-
状态通知
-
-
Sentinel如何判断一个redis实例是否健康?
-
每隔1秒发送一次ping命令,如果超过一定时间没有相向回复则认为是主观下线(
sdown
) -
如果大多数sentinel都认为实例主观下线,则判定服务客观下线(
odown
)
-
-
故障转移步骤有哪些?
-
首先要在
sentinel
中选出一个leader
,由leader执行failover
-
选定一个
slave
作为新的master
,执行slaveof no one
,切换到master模式 -
然后让所有节点都执行
slaveof
新master -
修改故障节点配置,添加
slaveof
新master
-
-
sentinel选举leader的依据是什么?
-
票数超过sentinel节点数量1半
-
票数超过quorum数量
-
一般情况下最先发起failover的节点会当
-
-
sentinel从slave中选取master的依据是什么?
-
首先会判断slave节点与master节点断开时间长短,如果超过
down-after-milliseconds * 10
则会排除该slave节点 -
然后判断slave节点的
slave-priority
值,越小优先级越高,如果是0则永不参与选举(默认都是1)。 -
如果
slave-prority
一样,则判断slave节点的offset
值,越大说明数据越新,优先级越高 -
最后是判断slave节点的
run_id
大小,越小优先级越高(通过info server可以查看run_id
)。
-
-
3. Redis分片集群
- 可以用来解决 :
-
海量数据存储
-
高并发写
-
-
要解决这两个问题就需要用到分片集群了。分片的意思,就是把数据拆分存储到不同节点,这样整个集群的存储数据量就更大了。
-
分片集群特征:
-
集群中有多个master,每个master保存不同分片数据 ,解决海量数据存储问题
-
每个master都可以有多个slave节点 ,确保高可用
-
master之间通过ping监测彼此健康状态 ,类似哨兵作用
-
客户端请求可以访问集群任意节点,最终都会被转发到数据所在节点
-
- 可以用来解决 :
-
3.2.散列插槽
-
数据要分片存储到不同的Redis节点,肯定需要有分片的依据,这样下次查询的时候才能知道去哪个节点查询。很多数据分片都会采用一致性hash算法。而Redis则是利用散列插槽(
hash slot
)的方式实现数据分片。 -
Redis分片集群如何判断某个key应该在哪个实例?
-
将16384个插槽分配到不同的实例
-
根据key计算哈希值,对16384取余
-
余数作为插槽,寻找插槽所在实例即可
-
-
如何将同一类数据固定的保存在同一个Redis实例?
-
Redis计算key的插槽值时会判断key中是否包含
{}
,如果有则基于{}
内的字符计算插槽 -
数据的key中可以加入
{类型}
,例如key都以{typeId}
为前缀,这样同类型数据计算的插槽一定相同
-
-
-
4 .Redis数据结构
-
我们常用的Redis数据类型有5种,分别是:
-
String
-
List
-
Set
-
SortedSet
-
Hash
还有一些高级数据类型,比如Bitmap、HyperLogLog、GEO等,其底层都是基于上述5种基本数据类型。因此在Redis的源码中,其实只有5种数据类型。
-
-
4.1.RedisObject
- redisObjec 结构体:
-
仅仅是对象头信息,内存占用的大小为4+4+24+32+64 = 128bit
也就是16字节,然后指针
ptr
指针指向的才是真实数据存储的内存地址。
-
-
属性中的
encoding
就是当前对象底层采用的数据结构或编码方式,可选的有11种之多: -
Redis中的5种不同的数据类型采用的底层数据结构和编码方式如下:
- redisObjec 结构体:
-
4.2.SkipList
-
SkipList(跳表)首先是链表,但与传统链表相比有几点差异:
-
元素按照升序排列存储
-
节点可能包含多个指针,指针跨度不同。
-
-
4.3.SortedSet
-
Redis的
SortedSet
底层的数据结构是怎样的?-
SortedSet是有序集合,底层的存储的每个数据都包含element和score两个值。score是得分,element则是字符串值。SortedSet会根据每个element的score值排序,形成有序集合。
它支持的操作很多,比如:
-
根据element查询score值
-
按照score值升序或降序查询element
-
不过,
ZipList
存在连锁更新问题,因此而在Redis7.0版本以后,ZipList
又被替换为Listpack(紧凑列表)。要实现根据element查询对应的score值,就必须实现element与score之间的键值映射。SortedSet底层是基于HashTable来实现的。
要实现对score值排序,并且查询效率还高,就需要有一种高效的有序数据结构,SortedSet是基于跳表实现的。
加分项:因为SortedSet底层需要用到两种数据结构,对内存占用比较高。因此Redis底层会对SortedSet中的元素大小做判断。如果元素大小小于128且每个元素都小于64字节,SortedSet底层会采用ZipList,也就是压缩列表来代替HashTable和SkipList
-
-
内存结构图
-
5. Redis内存回收
-
可以通过修改redis.conf文件,添加下面的配置来配置Redis的最大内存:
maxmemory 1gb -
5.1.内存过期处理
-
5.1.1.过期命令
-
Redis中通过
expire
命令可以给KEY设置TTL
(过期时间) -
5.1.2.过期策略
-
Redis如何判断KEY是否过期呢?
-
在Redis中会有两个Dict,也就是HashTable,其中一个记录KEY-VALUE键值对,另一个记录KEY和过期时间。要判断一个KEY是否过期,只需要到记录过期时间的Dict中根据KEY查询即可。
-
Redis的过期KEY删除策略有两种:
-
惰性删除:
-
Redis会在每次访问KEY的时候判断当前KEY有没有设置过期时间,如果有,过期时间是否已经到期。如果过期则删除。
-
-
周期删除 :
-
顾明思议是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。
执行周期有两种:
-
SLOW模式:Redis会设置一个定时任务
serverCron()
,按照server.hz
的频率来执行过期key清理 -
FAST模式:Redis的每个事件循环前执行过期key清理(事件循环就是NIO事件处理的循环)。
-
-
-
-
5.2.内存淘汰策略
1. 当内存使用达到阈值时就会主动挑选部分KEY删除以释放更多内存。这叫做内存淘汰机制2.Redis每次执行任何命令时,都会判断内存是否达到阈值 -
5.2.2.淘汰策略
-
noeviction
: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。 -
volatile-ttl
: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰 -
allkeys-random
:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选 -
volatile-random
:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。 -
allkeys-lru
: 对全体key,基于LRU算法进行淘汰 -
volatile-lru
: 对设置了TTL的key,基于LRU算法进行淘汰 -
allkeys-lfu
: 对全体key,基于LFU算法进行淘汰 -
volatile-lfu
: 对设置了TTL的key,基于LFI算法进行淘汰
-
-
比较容易混淆的有两个算法:
-
LRU(
L
east
R
ecently
U
sed
),最近最久未使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。 -
LFU(
L
east
F
requently
U
sed
),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
-
-
这就要聊起Redis的逻辑访问次数算法了,LFU的访问次数之所以叫做逻辑访问次数,是因为并不是每次key被访问都计数,而是通过运算:
-
① 生成
[0,1)
之间的随机数R
-
② 计算
1/(旧次数 * lfu_log_factor + 1)
,记录为P
,lfu_log_factor
默认为10 -
③ 如果
R
<P
,则计数器+1
,且最大不超过255 -
④ 访问次数会随时间衰减,距离上一次访问时间每隔
lfu_decay_time
分钟(默认1) ,计数器-1
-
-
总结
-
Redis如何判断KEY是否过期呢?
答:在Redis中会有两个Dict,也就是HashTable,其中一个记录KEY-VALUE键值对,另一个记录KEY和过期时间。要判断一个KEY是否过期,只需要到记录过期时间的Dict中根据KEY查询即可。
-
Redis何时删除过期KEY?如何删除?
答:Redis的过期KEY处理有两种策略,分别是惰性删除和周期删除。
惰性删除是指在每次用户访问某个KEY时,判断KEY的过期时间:如果过期则删除;如果未过期则忽略。
周期删除有两种模式:
-
SLOW模式:通过一个定时任务,定期的抽样部分带有TTL的KEY,判断其是否过期。默认情况下定时任务的执行频率是每秒10次,但每次执行不能超过25毫秒。如果执行抽样后发现时间还有剩余,并且过期KEY的比例较高,则会多次抽样。
-
FAST模式:在Redis每次处理NIO事件之前,都会抽样部分带有TTL的KEY,判断是否过期,因此执行频率较高。但是每次执行时长不能超过1ms,如果时间充足并且过期KEY比例过高,也会多次抽样
-
当Redis内存不足时会怎么做?
答:这取决于配置的内存淘汰策略,Redis支持很多种内存淘汰策略,例如LRU、LFU、Random. 但默认的策略是直接拒绝新的写入请求。而如果设置了其它策略,则会在每次执行命令后判断占用内存是否达到阈值。如果达到阈值则会基于配置的淘汰策略尝试进行内存淘汰,直到占用内存小于阈值为止。
-
那你能聊聊LRU和LFU吗?
答:
LRU
是最近最久未使用。Redis的Key都是RedisObject,当启用LRU算法后,Redis会在Key的头信息中使用24个bit记录每个key的最近一次使用的时间lru
。每次需要内存淘汰时,就会抽样一部分KEY,找出其中空闲时间最长的,也就是now - lru
结果最大的,然后将其删除。如果内存依然不足,就重复这个过程。由于采用了抽样来计算,这种算法只能说是一种近似LRU算法。因此在Redis4.0以后又引入了
LFU
算法,这种算法是统计最近最少使用,也就是按key的访问频率来统计。当启用LFU算法后,Redis会在key的头信息中使用24bit记录最近一次使用时间和逻辑访问频率。其中高16位是以分钟为单位的最近访问时间,后8位是逻辑访问次数。与LFU类似,每次需要内存淘汰时,就会抽样一部分KEY,找出其中逻辑访问次数最小的,将其淘汰。 -
逻辑访问次数是如何计算的?
答:由于记录访问次数的只有
8bit
,即便是无符号数,最大值只有255,不可能记录真实的访问次数。因此Redis统计的其实是逻辑访问次数。这其中有一个计算公式,会根据当前的访问次数做计算,结果要么是次数+1
,要么是次数不变。但随着当前访问次数越大,+1
的概率也会越低,并且最大值不超过255.除此以外,逻辑访问次数还有一个衰减周期,默认为1分钟,即每隔1分钟逻辑访问次数会
-1
。这样逻辑访问次数就能基本反映出一个key
的访问热度了。 -
6 .缓存问题
-
如何保证缓存的双写一致性?
答:缓存的双写一致性很难保证强一致,只能尽可能降低不一致的概率,确保最终一致。我们项目中采用的是
Cache Aside
模式。简单来说,就是在更新数据库之后删除缓存;在查询时先查询缓存,如果未命中则查询数据库并写入缓存。同时我们会给缓存设置过期时间作为兜底方案,如果真的出现了不一致的情况,也可以通过缓存过期来保证最终一致。追问:为什么不采用延迟双删机制?
答:延迟双删的第一次删除并没有实际意义,第二次采用延迟删除主要是解决数据库主从同步的延迟问题,我认为这是数据库主从的一致性问题,与缓存同步无关。既然主节点数据已经更新,Redis的缓存理应更新。而且延迟双删会增加缓存业务复杂度,也没能完全避免缓存一致性问题,投入回报比太低。
-
如何解决缓存穿透问题?
答:缓存穿透也可以说是穿透攻击,具体来说是因为请求访问到了数据库不存在的值,这样缓存无法命中,必然访问数据库。如果高并发的访问这样的接口,会给数据库带来巨大压力。
我们项目中都是基于布隆过滤器来解决缓存穿透问题的,当缓存未命中时基于布隆过滤器判断数据是否存在。如果不存在则不去访问数据库。
当然,也可以使用缓存空值的方式解决,不过这种方案比较浪费内存。
-
如何解决缓存雪崩问题?
答:缓存雪崩的常见原因有两个,第一是因为大量key同时过期。针对问这个题我们可以可以给缓存key设置不同的TTL值,避免key同时过期。
第二个原因是Redis宕机导致缓存不可用。针对这个问题我们可以利用集群提高Redis的可用性。也可以添加多级缓存,当Redis宕机时还有本地缓存可用。
-
如何解决缓存击穿问题?
答:缓存击穿往往是由热点Key引起的,当热点Key过期时,大量请求涌入同时查询,发现缓存未命中都会去访问数据库,导致数据库压力激增。解决这个问题的主要思路就是避免多线程并发去重建缓存,因此方案有两种。
第一种是基于互斥锁,当发现缓存未命中时需要先获取互斥锁,再重建缓存,缓存重建完成释放锁。这样就可以保证缓存重建同一时刻只会有一个线程执行。不过这种做法会导致缓存重建时性能下降严重。
第二种是基于逻辑过期,也就是不给热点Key设置过期时间,而是给数据添加一个过期时间的字段。这样热点Key就不会过期,缓存中永远有数据。
查询到数据时基于其中的过期时间判断key是否过期,如果过期开启独立新线程异步的重建缓存,而查询请求先返回旧数据即可。当然,这个过程也要加互斥锁,但由于重建缓存是异步的,而且获取锁失败也无需等待,而是返回旧数据,这样性能几乎不受影响。
需要注意的是,无论是采用哪种方式,在获取互斥锁后一定要再次判断缓存是否命中,做dubbo check. 因为当你获取锁成功时,可能是在你之前有其它线程已经重建缓存了。