Redis设计与实现(四)——复制与集群

Posted by 皮皮潘 on 12-17,2021

复制

在Redis中用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制另一个服务器

Redis的复制功能分为同步和命令传播两个操作:

  1. 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态
  2. 命令传播操作则用于在主服务器的数据库状态被修改时,将对应的命令发给从服务器

同步

当客户端向从服务器发送SLAVEOF命令后,从服务器首先需要执行同步操作,其中同步一共有两种同步:1. 全量同步 2. 部分同步

全量同步

  1. 从服务器向主服务器发送SYNC命令
  2. 收到SYNC命令的主服务器进行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
  3. 当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送个从服务器
  4. 主服务器将记录在缓冲区里面的所有写命令发送给从服务器

部分同步

从服务器对主服务器的复制主要可以分为两种情况:1. 初次复制 2. 断线后重新复制。对于除此复制来说,全量同步可以很好地完成任务,但是对于断线后重新复制来说,全量同步效率太低了,因此又有了部分同步功能PSYNC。

部分同步功能主要由以下三个部分构成:

  1. 主服务器的复制偏移量和从服务器的复制偏移量:执行复制的双方都分别维护一个复制偏移量,通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态
  2. 主服务器的复制积压缓冲区:它是由主服务器维护的一个固定长度的队列,默认大小为1MB,当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面,同时为队列中的每个字节记录响应的复制偏移量,当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会通过这个offset来决定执行何种同步操作:
    • offset之后的数据仍然存在于复制积压缓冲区中,则执行部分同步(主服务器回复+CONTINUE)
    • 反之则执行全量同步(主服务器回复+FULLRESYNC)
  3. 服务器的运行ID(runID):每个Redis服务器都会有自己的运行ID,它在启动时自动生成,有40个随机的十六机制字符组成,当从服务器断线并重新连上一个主服务器时,从服务器会发送之前保存的主服务器的运行ID,如果从服务器保存的运行ID与当前连接的主服务器相同,则会尝试使用部分重同步,反之则执行全量同步

命令传播

在完成了同步之后,主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行的对应的写命令就可以保证一致性了,另外这里需要注意命令传播是异步的不会等待从服务器执行完才返回,因为通过心跳检测时从服务器发来的offset也可以知道是否有命令丢失以及从服务器落后了多少,从而自发进行命令丢失补全

集群

Redis集群通过分片来进行数据共享,并提供复制和故障转移功能,其核心在于:节点、槽指派、命令执行、重新分片和故障转移。接下来将就以下方面进行介绍

节点

一个Redis集群通常由多个节点组成,在一开始的时候每个节点都是相互独立的,它们都处于一个只包含自己的集群中,通过使用CLUSTER MEET命令,可以将各个独立的节点连接起来,构成一个包含多个节点的集群

ClUSTER MEET的命令格式如下:

CLUSTER MEET <ip> <port>

通过CLUSTER MEET命令,两个节点之间会为彼此握手,然后创建一个对方clusterNode结构,并各自添加到自己的clusterState.nodes字典里面去,最后发起节点会再通过Gossip协议将加入的节点传播给集群中的其他节点,让其他节点也与加入节点进行握手。

槽指派

Redis集群通过分片的方式来保存数据库中的键值对:整个数据库被分为16384个槽,每个键都属于这个槽的其中一个(通过计算键key的CRC-16校验和然后&16383就可以了),每个节点分则处理一部分的槽。

当数据库中的16384个槽都有节点在处理时,集群处于上线状态,反之则处于下线状态。因此之前使用CLUSTER MEET只能将节点加入集群中,但是集群仍然处于下线状态。

需要通过向节点发送CLUSTER ADDSLOTS命令,将槽指派给节点负责,才可以让集群进入上线状态

clusterNode结构的slots属性记录了节点负责处理哪些槽,由于只需要一个bit就能记录是否负责对应的槽,因此对应的数据结构是char slots[16384/8]

一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,从而来告知其他节点目前负责哪些槽,所以说大部分的去中心化的实现,往往是每个节点都存了一个数据结构记录集群中全局的状态副本(Redis里面就是clusterState),然后通过Gossip协议或者其他协议来进行广播,最后达成各个节点的全局数据结构的一致性。

为了快速找到每个槽被哪个节点负责了,在clusterState结构中还使用了一个clusterNode *slots[16384]的slots数组记录了集群中所有槽的指派信息,从而在对应的槽不归当前节点处理时可以快速地告知客户端应该重定向到哪个真正的处理节点(MOVED错误),集群模式的客户端会自动重定向,而单机模式的则会报错MOVED错误

重新分片

Redis集群的重新分片操作由Redis的集群管理软件redis-trib负责执行,它对集群的单个槽slot进行重新分片的步骤如下:

  1. 对目标节点发送CLUSTER SETSLOT IMPORTING <source_id>命令,让目标节点准备好从源节点导入属于槽slot的键值对
  2. 对源节点发送CLUSTER SETSLOT MIGRATING <target_id>命令,让源节点主备好槽slot的键值对的迁移
  3. 向源节点发送CLUSTER GETKEYSINSLOT 命令获取最多count个属于槽slot的键值对的简明
  4. 对于上一步获得的每个简明,向源节点发送一个MIGRATE <target_ip> <target_port> <key_name>的命令,将被选中的键原子地(插入target+删除source)迁移到目标节点
  5. 重复3、4操作一直到所有的键值对都迁移完毕
  6. 向集群中任意一个节点发送CLUSTER SETSLOT NODE <target_id>将槽slot指派给目标节点,这一指派信息最后会通过消息机制发送至整个集群

ASK错误

如果节点收到一个关于键key的命令请求,并且键key所属的槽i正好就指派给了这个节点,那么节点会尝试在自己的数据库里查找键key,如果找到了,就执行对应的命令,反之节点会检查自己的clusterState.migrating_slots_to[i],查看键key所属的槽i是否正在进行迁移,如果是的话,那么节点会返回一个ASK错误,引导客户端到正在导入槽i的节点去查找键key

ASK错误和MOVED错误都会导致客户端专项,它们的区别在于:

  • MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点,或者本来就是在另外一个节点
  • ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施

故障转移

集群的故障转移底层是基于复制实现的,集群中的节点也可以是某些主节点的复制节点,这样在主节点宕机了以后,集群会基于类似RAFT算法的方式从该主节点的所有复制节点中推选出新的主节点,从而完成故障转移。