目录
一、Redis主从
1. 主从集群结构
2. 主从同步原理
2.1 全量同步
2.2 增量同步
3. 主从同步优化
4. 总结
二、Redis哨兵
1. 哨兵工作原理
1.1 哨兵作用
1.2 状态监控
1.3 选举新的master节点
2. 总结
三、Redis分片集群
1. 散列插槽
2. 故障转移
四、Redis数据结构
1. RedisObject
2. SkipList
3. SortedSet
五、Redis内存回收
1. 内存过期处理
2. 内存淘汰策略
3. 总结
一、Redis主从
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
1. 主从集群结构
下图是一个简单的Redis主从集群结构:
集群中有一个master节点(主),两个slave节点(从)。当我们通过Redis的Java客户端访问主从集群时,应该做好路由:
-
如果是写操作,应该访问master节点,master会自动将数据同步给两个slave节点
-
如果是读操作,建议访问各个slave节点,从而分担并发压力
2. 主从同步原理
我们向master节点写入数据之后,在两个slave节点上也可以看到对应的数据,这说明主从之间完成了数据的同步。那么这个同步是如何完成的呢?
2.1 全量同步
当主从第一次建立连接的时候,会执行全量同步,将master节点的所有数据都拷贝给slave节点,流程如下:
但是主节点如何知道是不是第一次同步呢?
每一个节点在创建出来的时候,都会认为自己是master节点,因此每一个节点就会有一个唯一的ID,即replid。当该节点成为其他节点的从节点时,它就会继承master节点的ID。因此,如果请求数据同步的节点ID与master节点ID不同,就可以判断是不是第一次进行同步。
2.2 增量同步
全量同步需要先生成RDB文件,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步,流程如下:
做增量同步之前,需要知道一个重要的概念:偏移量(offset),随着记录在repl_baklog
中的数据增多而逐渐增大。slave
完成同步时也会记录当前同步的offset
。如果slave
的offset
小于master
的offset
,说明slave
数据落后于master
,需要更新。
repl_baklog文件:是一个固定大小的环形数组,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
repl_baklog文件中会记录Redis处理过的命令及offset
,包括master当前的offset
,和slave已经拷贝到的offset,slave与naster的offset之间的差异,就是slave需要增量拷贝的数据。
红色部分是需要进行同步的数据。
但是会有一种特殊情况,slave出现了网络阻塞,导致master的offset远远超过slave的offset,最终导致还没有进行数据同步,master就将slave的offset覆盖了。此时就只能进行全量同步。
3. 主从同步优化
主从同步可以保证主从数据的一致性,非常重要。
可以从下面几个方面来优化Redis主从集群:
-
在master中配置
repl-diskless-sync yes
启用无磁盘复制(即主节点直接通过网络向从节点同步数据,不会先保存到磁盘中,再从磁盘取),避免全量同步时的磁盘IO。 -
Redis单节点的内存占用不要太大,减少RDB导致过多的磁盘IO
-
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能的避免全量同步
-
限制一个master节点上的salve节点数量。
4. 总结
二、Redis哨兵
主从结构中master节点的作用非常重要,一旦故障就会导致集群不可用。Redis提供了哨兵
(Sentinel
)机制来监控主从集群监控状态,确保集群的高可用性。
1. 哨兵工作原理
1.1 哨兵作用
哨兵集群作用原理图:
哨兵的作用如下:
-
状态监控:
Sentinel
会不断检查master
和slave
是否按预期工作 -
故障恢复(failover):如果
master
故障,Sentinel
会将一个slave
提升为master
。当故障实例恢复后会成为slave
-
状态通知:
Sentinel
充当Redis
客户端的服务发现来源,当集群发生failover
时,会将最新集群信息推送给Redis
的客户端,客户端就会知道有了一个新的master,就不会向旧的master中写数据了。
1.2 状态监控
sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个节点发送ping命令,并通过实例的响应结果来做出判断:
-
主观下线(sdown):如果某sentinel节点发现某Redis节点未在规定时间响应,则认为该节点主观下线。
-
客观下线(odown):若超过指定数量(通过
quorum
设置)的sentinel都认为该节点主观下线,则该节点客观下线。quorum值最好超过Sentinel节点数量的一半,Sentinel节点数量至少3台。
一旦发现故障,sentinel需要在slave中选择一个作为新的master,选择依据如下:
-
首先会判断slave节点与master节点断开时间长短,如果超过一定时间则会排除该slave节点
-
然后判断slave节点的
slave-priority
值,越小优先级越高,如果是0则永不参与选举(默认都是1)。 -
如果
slave-prority
一样,则判断slave节点的offset
值,越大说明数据越新,优先级越高
1.3 选举新的master节点
首先sentinel集群会先选择出一个执行failover的节点,第一个确认master客观下线的人会立刻发起投票,一定会成为leader(执行failover的节点)。
执行failover的流程如下:
假设我们有一个集群,初始状态下7001为master
,7002和7003为slave
:
此时master发生故障,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. 总结
三、Redis分片集群
主从模式可以解决高可用、高并发读的问题。但依然有两个问题没有解决:
-
海量数据存储
-
高并发写
要解决这两个问题就需要用到分片集群了。分片的意思,就是把数据拆分存储到不同节点,这样整个集群的存储数据量就更大了。
可以将分片集群理解为多个主从集群集群到一起了。
结构如图:
分片集群特征:
-
集群中有多个master,每个master保存不同分片数据 ,解决海量数据存储问题
-
每个master都可以有多个slave节点 ,确保高可用
-
master之间通过ping监测彼此健康状态 ,类似哨兵作用
-
客户端请求可以访问集群任意节点,最终都会被转发到数据所在节点
1. 散列插槽
当使用分片集群时,数据要分片存储到不同的Redis节点,肯定需要有分片的依据,这样下次查询的时候才能知道去哪个节点查询。redis是利用散列插槽(hash slot)的方式实现数据分片的。
在Redis集群中,共有16384个hash slots
,集群中的每一个master节点都会分配一定数量的hash slots
。具体的分配在集群创建时就已经指定了:
redis中一共有16384个插槽,在分片集群中,会将这些插槽分配给不同的示例。例如上图,三个主从集群,每一个会被分配5461个插槽。然后根据key计算哈希值,对16384取余,余数作为插槽,寻找插槽所在的实例即可。
不过hash slot
的计算也分两种情况:
-
当
key
中包含{}
时,根据{}
之间的字符串计算hash slot
-
当
key
中不包含{}
时,则根据整个key
字符串计算hash slot
例如:
-
key是
user
,则根据user
来计算hash slot -
key是
user:{age}
,则根据age
来计算hash slot
2. 故障转移
分片集群的节点之间会互相通过ping的方式做心跳检测,超时未回应的节点会被标记为下线状态。当发现master下线时,会将这个master的某个slave提升为master。
例如某个分片集群master节点为7002,有个从节点7006。如果7002发生故障,那么7006就会变成主节点,7002恢复后就会变成7006的slave节点。
四、Redis数据结构
1. RedisObject
不管是任何一种数据类型,最终都会封装为RedisObject格式,它是一种结构体。
结构如下图所示:
属性中的encoding就是当前对象底层采用的数据结构或者编码方式。
下面要说的SkipList(跳表)就是一种encoding。
2. SkipList
SkipList(跳表)首先是链表,但与传统链表相比有几点差异:
-
元素按照升序排列存储
-
节点可能包含多个指针,指针跨度不同。
传统链表只有指向前后元素的指针,因此只能顺序依次访问。如果查找的元素在链表中间,查询的效率会比较低。而SkipList则不同,它内部包含跨度不同的多级指针,可以让我们跳跃查找链表中间的元素,效率非常高。
结构如图所示:
我们可以看到1号元素就有指向3、5、10的多个指针,查询时就可以跳跃查找。例如我们要找大小为14的元素,查找的流程是这样的:
3. SortedSet
SortedSet就是有序集合Zset。
SortedSet的结构体如下所示:
typedef struct zset {dict *dict; // dict,底层就是HashTablezskiplist *zsl; // 跳表
} zset;
Redis的SortedSet底层数据结构是怎么样的?
SortedSet是有序集合,底层的存储的每个数据都包含element和score两个值。score是得分,element则是字符串值。SortedSet会根据每个element的score值排序,形成有序集合。
它支持的操作很多,比如:
-
根据element查询score值
-
按照score值升序或降序查询element
要实现根据element查询对应的score值,就必须实现element与score之间的键值映射。SortedSet底层是基于HashTable来实现的。
要实现对score值排序,并且查询效率还高,就需要有一种高效的有序数据结构,SortedSet是基于跳表实现的。
五、Redis内存回收
1. 内存过期处理
过期处理指的就是存入Redis中的数据可以配置过期时间,到期后再次访问会发现这些数据都不存在了,也就是被过期清理了。
Redis是如何判断一个KEY是否过期呢?
在Redis中会有两个Dict,也就是HashTable,其中一个记录KEY-VALUE键值对,另一个记录KEY和过期时间。要判断一个KEY是否过期,只需要到记录过期时间的Dict中根据KEY查询即可。
Redis是何时删除过期KEY的呢?
Redis的过期KEY删除策略有两种:惰性删除、周期删除。
惰性删除顾名思义Redis不会定期去看内存中的KEY是否过期,而是在访问某个KEY的时候判断当前KEY是否过期,如果过期就直接删除。
周期删除就是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。
2. 内存淘汰策略
对于某些特别依赖于Redis的项目而言,仅仅依靠过期KEY清理是不够的,内存可能很快就达到上限。因此Redis允许设置内存告警阈值,当内存使用达到阈值时就会主动挑选部分KEY删除以释放更多内存。这叫做内存淘汰机制。
Redis支持多种内存淘汰策略,:
-
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算法进行淘汰
其中volatile-lru和volatile-lfu是比较常用的两种策略。
-
LRU(
L
east
R
ecently
U
sed
),最近最久未使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。 -
LFU(
L
east
F
requently
U
sed
),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
在RedisObject结构当中,其中的lru
就是记录最近一次访问时间和访问频率的。
当然,选择LRU
和LFU
时的记录方式不同:
-
LRU:以秒为单位记录最近一次访问时间,长度24bit
-
LFU:高16位以分钟为单位记录最近一次访问时间,低8位记录逻辑访问次数。
3. 总结
当Redis内存不足时会怎么做?
这取决于配置的内存淘汰策略,Redis支持很多种内存淘汰策略,例如LRU、LFU、Random. 但默认的策略是直接拒绝新的写入请求。而如果设置了其它策略,则会在每次执行命令后判断占用内存是否达到阈值。如果达到阈值则会基于配置的淘汰策略尝试进行内存淘汰,直到占用内存小于阈值为止。
逻辑访问次数是如何计算的?
由于记录访问次数的只有8bit
,即便是无符号数,最大值只有255,不可能记录真实的访问次数。因此Redis统计的其实是逻辑访问次数。这其中有一个计算公式,会根据当前的访问次数做计算,结果要么是次数+1
,要么是次数不变。但随着当前访问次数越大,+1
的概率也会越低,并且最大值不超过255.
除此以外,逻辑访问次数还有一个衰减周期,默认为1分钟,即每隔1分钟逻辑访问次数会-1
。这样逻辑访问次数就能基本反映出一个key
的访问热度了。