Redis集群是Redis提供的分布式数据库解决方案,集群通过分片来实行数据共享,并支持主从复制和故障转移功能。
1 节点
-
一个集群通常由多个节点组成,可以通过CLUSTER MEET命令来连接多个节点
CLUSTER MEET <ip> <port>
-
启动节点
将cluster-enabled配置为yes
-
集群数据结构
clusterLink保存了连接到这个节点需要的数据,其数据结构如下:
最后,每个节点都保存了一个clusterState,这个数据结构保存了整个集群的信息。
-
CLUSTER MEET命令的实现
以向A节点发送CLUSTER MEET命令,让其添加B节点到集群中为例:
- A为B创建一个clusterNode结构,添加到自己的clusterState.nodes字典里。
- A向B发送一条MEET消息,表示要与B进行握手。这与经典的TCP三次握手类似。
- B收到A的MEET后,为A创建clusterNode节点,然后向A回复一条PONG消息。
- A收到B的PONG消息后,就知道B已经收到它的MEET消息。接着他向B发送一条PING消息。B收到A的PING消息后,就知道A已经收到它的PONG消息。
2 槽指派
Redis集群通过分片的方式来保存数据库中的键值对:将整个集群数据库分为16384个槽(2^14),每个节点可以处理其中的0个或多个槽。
如果集群中的所有槽都被处理,集群就处于上线状态,否则,集群处于下线状态。
可以通过CLUSTER ADDSLOTS将一个或多个操指派给某节点负责。
CLUSTER ADDSLOTS [slot...]
-
记录节点的槽指派信息
clusterNode的slots属性记录了节点的槽信息。
-
传播节点的槽指派信息
- 被指派了槽的节点会将自己处理的槽告知集群中的其它节点。
- 接收到告知信息的节点会在自己clusterNode.nodes字典中更新对应节点的处理槽信息。
-
记录集群所有槽指派信息
clusterState结构中的slots[16384]保存了每个槽的指派信息。
- 其中的每个指针指向该槽处理节点的clusterNode。
- 如果为NULL表示该槽没有节点处理。
-
CLUSTER ADDSLOTS命令的实现
- 遍历所有槽,检查他们是否都未指派。
- 再次遍历所有槽,将它们都指派给当前节点。
- 发消息告知其它节点,自己在处理哪些槽。
这里提一下clusterNode.slots和clusterState.slots的关键区别。
- 使用clusterNode.slots数组,可以快速的将本届点处理的槽信息发送给其它节点。
- 使用clusterState.slots数组,可以快速的知道槽i是被哪个节点处理。
3 在集群中执行命令
在集群中的16384个槽都指派成功后,集群就进入上线状态,这时就可以通过客户端向集群发送命令。当客户端向服务器发送和键有关的命令时,服务器先计算出该键对应的槽,然后判断该槽是否由本节点处理:
- 如果是由本节点处理,则执行命令。
- 否则,向客户端返回MOVED错误,并告知客户端该槽由哪个节点处理。
- 收到MOVED错误信息的客户端重新向正确的节点发送命令。
-
计算节点属于哪个槽
节点使用以下算法来计算键属于哪个槽:
def slot_number(key): return CRC16(key) & 16383;
CRC16()用来计算key的校验和。
-
判断键是否由当前槽处理
计算出key的槽i之后,会执行以下步骤:
- 找到clusterState.slots[i],看是否指向自己。如果是自己,说明该槽由自己处理。
- 否则,取出处理该槽的clusterNode信息,并返回MOVED错误。
-
MOVED错误
MOVED错误的格式为
MOVEC <slot> <ip>:<port>
被隐藏的MOVED错误:
- 在集群模式下工作的client不会打印出MOVED错误,而是直接根据MOVED错误来重新发送命令。
- 在单机模式下工作的client无法识别MOVED错误,会直接打印出MOVED错误信息。
-
节点数据库的实现
集群节点数据库保存键值对及键值对过期时间的方式和单机数据库完全一样,不同点是:
- 节点数据库只能只用0号数据库。
- 节点数据库会使用跳表来记录每个槽下记录的所有键值对,这样可以方便的查找出某个槽下的所有键值对,在槽转移等批量操作时十分方便。
4 重新分片
Redis可以将已经分派给某个节点的槽重新分配给其它节点,叫做重新分片。
重新分片可以在线进行,集群无需下线,并且在重新分片时节点仍然可以处理命令。
这为Redis横向扩展提供了非常方便的途径。
-
重新分片的实现原理
Redis使用redis-trib来进行重新分片,步骤如下:
- 向目标节点发送命令,让其做好准备接收新的槽。
- 向源节点发送命令,让其准备好槽的转移。
- 从源节点一次最多获取count个key。
- 对获取的每个key,向源节点逐个发送命令,让其将该key转移至目标节点。
- 重复获取key和转移的步骤,直至转移完成。
- 向所有集群中其它节点发送命令,告知该槽已经被转移至目标节点。
5 ASK错误
在重新分片期间,可能会出现数据正在转移,此时客户端发来命令的情况。那么此时源节点会这样处理:
- 检查该key是否仍在自己的槽中,如果在则直接处理命令。
- 如果不在,则有可能已经转移到目标节点,返回ASK错误,指引客户端访问正确的节点。
ASK错误与MOVED错误的区别:
- MOVED错误是在集群正常运行下产生的错误,客户端接收到MOVED错误时,就会将该槽的目标节点设置为新的节点,以后每次对该槽查询时,都向新的节点查询。
- ASK错误只是在集群重新分片时产生的一种错误,处理措施也只是一种临时措施。也就是说客户端接到ASK错误时,它只是将此次查询重新定位到新的节点,但下次查询仍然会查询到之前的节点。这是因为,转移还在进行中,仍然有部分键值对存在源节点中。
6 复制与故障转移
Redis集群中的节点也可以设置主从节点,其中,主节点负责处理槽,而从节点从主节点复制数据,并在主节点下线时,接替主节点的工作。
-
设置主节点
向节点发送如下命令,可以让该节点成为指定node_id节点的从节点,并开始主从复制。
CLUSTER REPLICATE <node_id>
- 首先,在自己的clusterState.nodes中找到主节点,并将自己的clusterState.myself.slaveOf指向这个节点。
- 然后,将自己的clusterState.myself.flags标志位改为自己是个从节点。
- 最后,节点会调用复制代码,逻辑主从复制一样。相当于向从节点执行了SLAVEOF
命令 - 同时,在开始复制的时候,会发送一条命令,告诉集群中的其他节点,本节点已经成为另一个节点的从节点,那么在集群中的所有其它节点就会记录这个主从关系。
-
故障检测
- 集群中每个节点都定时向其它节点发送PING消息,来检测其它节点是否依然在线
- 如果某个节点在规定时间内没有回复PONG消息,发出PING的节点,就主观地认为该节点疑似下线,并在其对应地clusterNode中标记疑似下线。
- 节点之间会互通消息,来报告每个节点的下线状态。如果主节点A收到主节点B认为主节点C疑似下线的报告,主节点A会在自己的clusterState.nodes字典中,找到C节点,并将主节点B的下线报告添加在C节点中。
- 如果在一个集群中,超过半数的主节点都认为某个主节点已经下线,那么该主节点就会被认为客观下线。第一个发现该节点客观下线的节点,会向集群广播该节点已经下线,所有收到消息的节点,都会立即将该节点标记为下线。
-
故障转移
当一个从节点发现自己的主节点已经下线时,会启动故障转移流程。
- 复制同一个主节点的从节点中,会有一个被选为新的主节点。
- 新的主节点会接管原主节点的所有槽。
- 新的主节点会向集群中其它节点报告自己已经成为新的主节点。
- 新的主节点开始接管原主节点的工作,故障转移完成。
-
选举新的节点
- 所有已下线主节点的从节点都有选举权。
- 所有正在处理槽的主节点具有投票权。
- 选举按照集群的纪元来进行,一个纪元中只能进行一轮投票。
- 当一个从节点发现它的主节点已经下线时,会向所有正在处理槽的主节点发送一条命令,要求将票投给自己。
- 主节点会给收到的第一个投票要求的发送者投,并向其回复投票信息。
- 所有参选的从节点会统计自己的票数,如果得到超过半数的票,则当选成功。
- 若一个纪元中没有选举出新的从节点,就等下一个纪元重新投票。