1. Redis主从复制的原理
【主从复制的原理】
- 同步:从节点向主节点发送
psync
命令进行同步,从节点保存主节点返回的runid
和offset
- 全量复制:如果是第一次连接或者连接失败且
repl_backlog_buffer
缓存区不包含slave_repl_offset
, 则生成主节点的数据快照(RDB文件)发给从节点 - 增量复制:全量复制完毕后,主从节点之间会保持长连接。如果连接没有断开或者
slave_repl_offset
仍然在repl_backlog_buffer
中,则将后续的写操作传递给从节点,让数据保持一致。
【全量复制细节】
全量复制的过程是基于TCP长连接的,主要流程如下
- 从节点发送
psync ? -1
表示需要建立连接进行同步,主节点返回主节点IDrunid
和 复制进度offset
(第一次同步用 -1 表示)。从节点接受之后,保存主节点的信息。 - 主节点执行
bgsave
命令生成数据快照RDB文件,然后将RDB文件发送给从节点。从节点接受文件后,清除现有的所有数据,然后加载RDB文件 - 如果在制作数据快照RDB文件的过程当中,主节点接收到了新的写操作,主节点会将其记录在
repl buffer
里面。然后将repl buffer
当中的写操作发给从节点,让其数据保持一致。
【增量复制细节】
如果主从节点意外断开连接,为了保持数据的一致性,必须重新同步数据。如果使用全量复制来保持一致性的话,开销太大,所以采用增量复制。
增量复制的具体流程如下:
- 连接恢复后,从节点会发送
psync {runid} {offset}
, 其中主节点IDrunid
和 复制进度offset
用于标识是哪一个服务器主机和复制进度。 - 主节点收到
psync
命令之后,会用conitnue
响应告知从节点,采用增量复制同步数据 - 最后,主节点根据
offset
查找对应的进度,将短线期间未同步的写命令,发送给从节点。同时,主节点将所有的写命令写入repl_backlog_buffer
, 用于后续判断是采用增量复制还是全量复制。
【注意】从节点 psync
携带的 offset
为 slave_repl_offset
。如果 repl_backlog_buffer
包含slave_repl_offset
对应的部分,则采用增量复制,否则采用全量复制。repl_backlog_buffer
的默认缓冲区大小为1M
【为什么要主从复制】
- 备份数据:主从复制实现了数据的热备份,是持久化之外的数据冗余方式
- 故障恢复:当主节点宕机之后,可以采用从节点提供服务。
- 负载均衡: 主从复制实现了读写分离,只有主节点支持读写操作,从节点只有都操作。在读多写少的场景下,可以提高Redis服务器的并发量。
2. Redis集群的实现原理是什么?
【Redis集群基本知识】
- 定义: Redis集群由多个实例组成,每个实例存储部分数据 (每个实例之间的数据不重复) 。
【注】集群和主从节点不是一个东西,集群的某一个实例当中可能包含一个主节点 + 多个从节点
- 为什么用
问题 | 解决方案 |
---|---|
容量不足 | 数据分片,将数据分散不存到不同的主节点 |
高并发写入 | 数据分片,将写入请求分摊到多个主节点 |
主机宕机问题 | 自动切换主从节点,避免影响服务, 不需要手动修改客户端配置 |
- 节点通信协议:Redis集群采用Gossip协议, 支持分布式信息传播、延迟低、效率高。采用去中心化思想,任意实例(主节点)都可以作为请求入口,节点间相互通信。
- 分片原理: 采用哈希槽(Hash Slot)机制来分配数据,整个空间可以划分为16384 (16 * 1024)个槽。 每个Redis负责一定范围的哈希槽,数据的key经过哈希函数计算之后对16384取余可定位到对应的节点。
【集群节点之间的交互协议】
- 为什么用Gossip协议
- 分布式信息传播:每个节点定期向其他节点传播状态信息,确保所有节点对集群的状态有一致视图 (采用
ping
发送 和pong
接受,就像检查心跳一样 ) - 低延迟、高效率:轻量级通信方式,传递信息很快
- 去中心化:没有中心节点,任意实例(主节点)都可以作为请求入口,节点间相互通信。
- Gossip协议工作原理
- 状态报告和信息更新:特定时间间隔内,向随机的其他节点报告自身情况 (主从关系、槽位分布)。其他节点接收到之后,会相应的更新对应的节点状态信息
- 节点检测:通过周期性交换状态信息,可以检测到其他节点的存活状态。预定时间内未响应,则标记为故障节点。
- 容错处理:如果某个节点故障之后,集群中的其他节点可以重新分配槽位,保持系统的可用性
【哈希槽的相关机制】
假定集群中有三个节点,Node1 (0 - 5460)、Node2(5461-10922)、Node3(10923-16383)
集群使用哈希槽的流程如下:
- 计算哈希槽
- 使用CRC16哈希算法计算
user:0001
的CRC16的值 - 将CRC16的值对16384进行取余 (哈希槽 = CRC16 % 16383)
- 假如CRC16为12345,哈希槽 = 12345 % 16383 = 12345
- 确定目标节点 :查询到12345为Node3的存储的键,向该节点发送请求
- 当前非对应节点 :假设当前连接的节点为Node1,Node1将返回
MOVED
错误到客户端,并让客户端根据MOVED
携带的Node3的信息(ip
和端口)重新进行连接,最后从新发送GET user:0001
请求,获得结果。
3. Redis的哨兵机制(Sentinel)是什么?
【哨兵作用】
- 监控:哨兵不断监控主从节点的运行状态,定时发送ping进行检测
- 故障转移: 当主节点发生故障时, 哨兵会先踢出所有失效的节点, 然后选择一个有效的从节点作为新的主节点, 并通知客户端更新主节点的地址
- 通知: 哨兵可以发送服务各个节点的状态通知,方便观察Redis实例的状态变化。(比如主节点g了,已经更换为新的主节点)
【哨兵机制的主观下线和客观下线】
-
主观下线:哨兵在监控的过程中,每隔1s会发送
ping
命令给所有的节点。如果哨兵超过down-after-milliseconds
所配置的时间,没有收到pong
的响应,就会认为节点主观下线。 -
客观下线:某个哨兵发现节点主线下线后,不能确认节点是否真的下线了(可能是网络不稳定),就询问其他的哨兵是否主观下线了。等待其他哨兵的确认,进行投票,如果超过半数+1 (总哨兵数/2 + 1),就认定为客观下线。
【注】客观下线只对主节点适用,因为从节点也没必要这样子判断,g了就g了呗。
【哨兵leader如何选举】
哨兵leader是采用分布式算法raft选出来的。具体流程如下:
- 候选人:当哨兵判断为主观下线,则可以当选候选人
- 投票:每个哨兵都可以投票,但是只能投一票。候选者会优先投给自己。
- 选举:选取投票结果半数以上的候选人作为leader (哨兵一般设置为奇数,防止平票)
【主节点如何选举】
哨兵判断主节点客观下线之后,会踢出所有下线的节点,然后从有效的从节点选新的主节点。选取依据如下:
- 优先级:按照从节点的优先级
slave-priority
,优先级的值越小越高。 - 主从复制offset值:如果优先级相同,则判断主从复制的offset值哪一个大,表明其同步的数据越多,优先级就越高。
- 从节点ID:如果上述条件均相同,则选取ID较小的从节点作为主节点。
4. Redis Cluster 集群模式与 Sentinel 哨兵模式的区别是什么?
- Cluster集群模式:集群模式用于对数据进行分片,主要用于解决大数据、高吞吐量的场景。将数据自动分不到多个Redis实例上,支持自动故障转移(如果某个实例失效,集群会自动重写配置和平衡,不需要手动进行调整,因为内置了哨兵逻辑)
- Sentinel哨兵模式: 哨兵模式用于保证主从节点的高可用,读写分离场景。如果主节点宕机,哨兵会将从节点升为主节点。
5. Redis 在生成 RDB 文件时如何处理请求?
首先,Redis生成RDB文件的操作是异步的,由fork
子线程进行,主线程用于处理客户端的请求。下面具体说明生成RDB文件的流程
【生成RDB文件原理】
- 使用
bgsave
命令,开启fork
子线程进行操作 fork
子线程会复制主线程对应的页表(里面包含了需要操作数据的物理地址)- 如果过程中,主线程接收到写命令,需要修改数据。主线程会将对应数据的所在页面复制一份,子线程仍然指向老的页面。(老的数据才叫数据快照)
【注意事项】
RDB处理的时间比较长,过程中会发生大量的磁盘I/O和CPU负载。如果RDB生成的时间过长,并且Redis的写并发高,就可能出现系统抖动的现象,应该选取Redis使用频率较低的时间段生成RDB文件。
6. Redis集群会出现脑裂问题吗?
-
脑裂定义: 在网络分区的情况下,Redis同一个集群的实例当中出现多个主节点,导致数据不一致。
-
脑裂发生的原因:比如当前集群实例是一主+两从的模式,当网络发送分区,分为A区和B区。主节点(原)被分到A区,其他节点和哨兵集群都在B区。哨兵机制无法检测到A区的原主节点, 只能重新选取新的主节点(新)。此时,集群当中就有两个主节点,A区的主节点(原)被写入的新数据不会同步到B区的节点上。会出现数据不一致的情况。
-
如何避免脑裂:
min-slaves-to-write
主节点写操作所要求有效从节点个数、min-slaves-max-lag
从节点的最大延迟。比如min-slaves-to-write = 3
和min-slaves-max-lag = 10
表明需要至少3个延时低于10s的从节点才可以接受写操作。【注意】脑裂并不能够完全避免,比如说在选举主节点的过程中,主节点(原)突然恢复了,然后发现主节点和从节点的延迟都不超过10s,客户端正常在主节点(原)进行写操作。等选举完毕,选出新的主节点,让主节点(原) slaveof 为从节点。选举时间写入的数据会被覆盖,就出现了数据不一致的现象。
7. Redis如何实现分布式锁?
-
分布式锁原理:Redis分布式锁由
set ex nx
和lua
脚本组成,分别对应加锁和解锁操作 -
为什么用
set ex nx
:某个进程对指定key执行set nx
之后, 返回值为1,其他进程想要对相同的key获取锁,会发现key已存在,返回值为0。这样就是实现了上锁的操作。但是,如果A进程上完锁突然挂了,其他进程就永远不可能拿到锁。所以,设置一个ex
过期时间,让其不要一直占用着锁。【注意】
set ex nx
设置value的时候,必须采用唯一值,比如uuid
。 不然可能出现如下情况:- A进程正常申请锁,值设为1。
- A进程上锁后, 执行过程时间比较长, 以至于锁已经过期了, A进程还没执行完.
- 此时,B进程申请锁,值也设为1. 同时,A进程执行完毕, 使用
lua
脚本把锁删除了 - B进程此时还在执行程序,一脸懵逼。(不是,哥们儿,我锁呢?谁偷了我的锁!!!)
-
为什么用
lua
进行解锁:如上述注意事项所说的一样,A进程执行完毕之后, 会删除锁. 假如他们的值都采用了uuid
保证了唯一性。可能会出现下面的情况- A进程先判断key和其值是否为对应的
uuid
,然后再删除锁. - A进程准备删除锁之前, 锁过期了. B进程同时获取了锁
- A进程再删除了该锁 (B进程申请的锁),发生了误删的现象
所以需要用
lua
脚本保证解锁的原子性,就可以避免上述问题 - A进程先判断key和其值是否为对应的
8. Redis的Red Lock是什么?你了解吗?
1. **Red Lock定义**: 一种分布式锁的实现方案,主要用于解决分布式环境中使用Redis分布式锁的安全性问题
2. **为什么用Red Lock**: 假如我们采用一主+两从+哨兵方式部署Redis,如果有A进程在使用分布式锁的过程当中,主节点发送了主从更换,但是原主节点的锁信息不一定同步到新主节点上。所以当前新主节点可能没有锁信息,此时另外的B进程去获取锁,发现锁没被占,成功拿到锁并执行业务逻辑。此时两个竞争者(A和B进程)会同时操作临界资源,会出现数据不一致的情况。
3. **Red Lock实现原理** : 假如当前有五个实例,不需要部署从节点和哨兵,只需要主节点。注意当前的五个实例之间没有任何关系,不进行任何的信息交互 (不同于Redis Cluster集群模式)。对五个实例依次申请上锁,如果最终申请成功的数量超过半数(大于总数/2 + 1),则表明红锁申请成功。按照下面的流程进行操作:
1. 客户端获取当前时间 `t1`
2. 客户端依次对五个实例进行`set ex nx` 操作,锁的过期时间为 `t_lock` (远小于锁的总过期事件)。如果当前节点请求超时,则立马请求下一个节点。
3. 当获取的锁超过半数,则获取当前的时间 `t2`。获取锁的过程总耗时`t = t2 - t1`。如果`t`小于锁的过期时间 `t_lock`,则可以判断为加锁成功,否则加锁失败。
4. 加锁成功,则执行业务逻辑。若加锁失败,则依次释放所有节点的锁。
-
Red Lock是否安全:先说结论,不一定安全
当前有两个客户端(
Client1
和Client2
),首先Client1
正常获取锁,然后突然被GC
执行垃圾回收机制了。在GC
的过程当中,Client1
的锁超时释放了,Client2
开始申请并获得锁。然后Client2
写入数据并释放锁。 后面Client1
在GC
结束之后又写入数据, 此时就出现了数据不一致的情况。