1. Redis主从复制的原理

主从复制的原理

  1. 同步:从节点向主节点发送psync命令进行同步,从节点保存主节点返回的 runid offset
  2. 全量复制:如果是第一次连接或者连接失败且repl_backlog_buffer 缓存区不包含slave_repl_offset, 则生成主节点的数据快照(RDB文件)发给从节点
  3. 增量复制:全量复制完毕后,主从节点之间会保持长连接。如果连接没有断开或者slave_repl_offset仍然在repl_backlog_buffer中,则将后续的写操作传递给从节点,让数据保持一致。

【全量复制细节】

全量复制的过程是基于TCP长连接的,主要流程如下

  1. 从节点发送psync ? -1表示需要建立连接进行同步,主节点返回主节点ID runid 和 复制进度offset (第一次同步用 -1 表示)。从节点接受之后,保存主节点的信息。
  2. 主节点执行bgsave命令生成数据快照RDB文件,然后将RDB文件发送给从节点。从节点接受文件后,清除现有的所有数据,然后加载RDB文件
  3. 如果在制作数据快照RDB文件的过程当中,主节点接收到了新的写操作,主节点会将其记录在repl buffer 里面。然后将repl buffer当中的写操作发给从节点,让其数据保持一致。

Redis主从全量复制

【增量复制细节】

如果主从节点意外断开连接,为了保持数据的一致性,必须重新同步数据。如果使用全量复制来保持一致性的话,开销太大,所以采用增量复制。

增量复制的具体流程如下:

  1. 连接恢复后,从节点会发送psync {runid} {offset}, 其中主节点ID runid 和 复制进度offset用于标识是哪一个服务器主机和复制进度。
  2. 主节点收到psync 命令之后,会用conitnue响应告知从节点,采用增量复制同步数据
  3. 最后,主节点根据offset查找对应的进度,将短线期间未同步的写命令,发送给从节点。同时,主节点将所有的写命令写入repl_backlog_buffer, 用于后续判断是采用增量复制还是全量复制。

【注意】从节点 psync 携带的 offsetslave_repl_offset。如果 repl_backlog_buffer包含slave_repl_offset 对应的部分,则采用增量复制,否则采用全量复制。repl_backlog_buffer的默认缓冲区大小为1M

Redis主从增量复制

为什么要主从复制

  • 备份数据:主从复制实现了数据的热备份,是持久化之外的数据冗余方式
  • 故障恢复:当主节点宕机之后,可以采用从节点提供服务。
  • 负载均衡: 主从复制实现了读写分离,只有主节点支持读写操作,从节点只有都操作。在读多写少的场景下,可以提高Redis服务器的并发量。

Redis主从读写分离

2. Redis集群的实现原理是什么?

Redis集群基本知识

  • 定义: Redis集群由多个实例组成,每个实例存储部分数据 (每个实例之间的数据不重复) 。

【注】集群和主从节点不是一个东西,集群的某一个实例当中可能包含一个主节点 + 多个从节点

  • 为什么用
问题 解决方案
容量不足 数据分片,将数据分散不存到不同的主节点
高并发写入 数据分片,将写入请求分摊到多个主节点
主机宕机问题 自动切换主从节点,避免影响服务, 不需要手动修改客户端配置
  • 节点通信协议:Redis集群采用Gossip协议, 支持分布式信息传播、延迟低、效率高。采用去中心化思想,任意实例(主节点)都可以作为请求入口,节点间相互通信。
  • 分片原理: 采用哈希槽(Hash Slot)机制来分配数据,整个空间可以划分为16384 (16 * 1024)个槽。 每个Redis负责一定范围的哈希槽,数据的key经过哈希函数计算之后对16384取余可定位到对应的节点。

Redis集群架构图

集群节点之间的交互协议

  • 为什么用Gossip协议
  1. 分布式信息传播:每个节点定期向其他节点传播状态信息,确保所有节点对集群的状态有一致视图 (采用ping 发送 和 pong 接受,就像检查心跳一样 )
  2. 低延迟、高效率:轻量级通信方式,传递信息很快
  3. 去中心化:没有中心节点,任意实例(主节点)都可以作为请求入口,节点间相互通信。
  • Gossip协议工作原理
  1. 状态报告和信息更新:特定时间间隔内,向随机的其他节点报告自身情况 (主从关系、槽位分布)。其他节点接收到之后,会相应的更新对应的节点状态信息
  2. 节点检测:通过周期性交换状态信息,可以检测到其他节点的存活状态。预定时间内未响应,则标记为故障节点。
  3. 容错处理:如果某个节点故障之后,集群中的其他节点可以重新分配槽位,保持系统的可用性

哈希槽的相关机制

假定集群中有三个节点,Node1 (0 - 5460)、Node2(5461-10922)、Node3(10923-16383)

集群使用哈希槽的流程如下:

  • 计算哈希槽
  1. 使用CRC16哈希算法计算user:0001的CRC16的值
  2. 将CRC16的值对16384进行取余 (哈希槽 = CRC16 % 16383)
  3. 假如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选出来的。具体流程如下:

  1. 候选人:当哨兵判断为主观下线,则可以当选候选人
  2. 投票:每个哨兵都可以投票,但是只能投一票。候选者会优先投给自己。
  3. 选举:选取投票结果半数以上的候选人作为leader (哨兵一般设置为奇数,防止平票)

【主节点如何选举】

哨兵判断主节点客观下线之后,会踢出所有下线的节点,然后从有效的从节点选新的主节点。选取依据如下:

  1. 优先级:按照从节点的优先级 slave-priority,优先级的值越小越高。
  2. 主从复制offset值:如果优先级相同,则判断主从复制的offset值哪一个大,表明其同步的数据越多,优先级就越高。
  3. 从节点ID:如果上述条件均相同,则选取ID较小的从节点作为主节点。

4. Redis Cluster 集群模式与 Sentinel 哨兵模式的区别是什么?

  1. Cluster集群模式:集群模式用于对数据进行分片,主要用于解决大数据、高吞吐量的场景。将数据自动分不到多个Redis实例上,支持自动故障转移(如果某个实例失效,集群会自动重写配置和平衡,不需要手动进行调整,因为内置了哨兵逻辑
  2. Sentinel哨兵模式: 哨兵模式用于保证主从节点的高可用,读写分离场景。如果主节点宕机,哨兵会将从节点升为主节点。

5. Redis 在生成 RDB 文件时如何处理请求?

首先,Redis生成RDB文件的操作是异步的,由fork子线程进行,主线程用于处理客户端的请求。下面具体说明生成RDB文件的流程

【生成RDB文件原理】

  1. 使用bgsave命令,开启fork子线程进行操作
  2. fork子线程会复制主线程对应的页表(里面包含了需要操作数据的物理地址)
  3. 如果过程中,主线程接收到写命令,需要修改数据。主线程会将对应数据的所在页面复制一份,子线程仍然指向老的页面。(老的数据才叫数据快照)

【注意事项】

RDB处理的时间比较长,过程中会发生大量的磁盘I/O和CPU负载。如果RDB生成的时间过长,并且Redis的写并发高,就可能出现系统抖动的现象,应该选取Redis使用频率较低的时间段生成RDB文件。

6. Redis集群会出现脑裂问题吗?

  1. 脑裂定义: 在网络分区的情况下,Redis同一个集群的实例当中出现多个主节点,导致数据不一致。

  2. 脑裂发生的原因:比如当前集群实例是一主+两从的模式,当网络发送分区,分为A区和B区。主节点(原)被分到A区,其他节点和哨兵集群都在B区。哨兵机制无法检测到A区的原主节点, 只能重新选取新的主节点(新)。此时,集群当中就有两个主节点,A区的主节点(原)被写入的新数据不会同步到B区的节点上。会出现数据不一致的情况。

  3. 如何避免脑裂min-slaves-to-write 主节点写操作所要求有效从节点个数、min-slaves-max-lag 从节点的最大延迟。比如 min-slaves-to-write = 3min-slaves-max-lag = 10 表明需要至少3个延时低于10s的从节点才可以接受写操作。

    【注意】脑裂并不能够完全避免,比如说在选举主节点的过程中,主节点(原)突然恢复了,然后发现主节点和从节点的延迟都不超过10s,客户端正常在主节点(原)进行写操作。等选举完毕,选出新的主节点,让主节点(原) slaveof 为从节点。选举时间写入的数据会被覆盖,就出现了数据不一致的现象。

7. Redis如何实现分布式锁?

  1. 分布式锁原理:Redis分布式锁由set ex nxlua 脚本组成,分别对应加锁和解锁操作

  2. 为什么用 set ex nx:某个进程对指定key执行 set nx 之后, 返回值为1,其他进程想要对相同的key获取锁,会发现key已存在,返回值为0。这样就是实现了上锁的操作。但是,如果A进程上完锁突然挂了,其他进程就永远不可能拿到锁。所以,设置一个ex过期时间,让其不要一直占用着锁。

    【注意】set ex nx设置value的时候,必须采用唯一值,比如uuid。 不然可能出现如下情况:

    1. A进程正常申请锁,值设为1。
    2. A进程上锁后, 执行过程时间比较长, 以至于锁已经过期了, A进程还没执行完.
    3. 此时,B进程申请锁,值也设为1. 同时,A进程执行完毕, 使用lua脚本把锁删除了
    4. B进程此时还在执行程序,一脸懵逼。(不是,哥们儿,我锁呢?谁偷了我的锁!!!)
  3. 为什么用 lua 进行解锁:如上述注意事项所说的一样,A进程执行完毕之后, 会删除锁. 假如他们的值都采用了uuid保证了唯一性。可能会出现下面的情况

    • A进程先判断key和其值是否为对应的uuid,然后再删除锁.
    • A进程准备删除锁之前, 锁过期了. B进程同时获取了锁
    • A进程再删除了该锁 (B进程申请的锁),发生了误删的现象

    所以需要用lua脚本保证解锁的原子性,就可以避免上述问题

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. 加锁成功,则执行业务逻辑。若加锁失败,则依次释放所有节点的锁。

Redis的RedLock结构图

  1. Red Lock是否安全:先说结论,不一定安全

    当前有两个客户端(Client1Client2),首先Client1 正常获取锁,然后突然被GC执行垃圾回收机制了。在GC的过程当中,Client1 的锁超时释放了,Client2开始申请并获得锁。然后Client2 写入数据并释放锁。 后面Client1GC 结束之后又写入数据, 此时就出现了数据不一致的情况。

Redis的RedLock安全问题