Redis集群添加节点槽位迁移分析
第一部分:为什么要迁移槽位?
首先,要理解迁移的目的。Redis Cluster 共有 16384 个槽位。数据通过 CRC16(key) mod 16384 的算法被分配到这些槽中。每个主节点负责处理其中一部分槽位。
当你向一个已经平衡的集群添加新的主节点时,这个新节点默认是不负责任何槽位的(即它的槽位计数为 0)。这意味着它无法处理任何请求。为了让新节点分担负载,必须从现有节点中拿走一部分槽位及其对应的数据,并分配给新节点,这个过程就是槽位迁移。
第二部分:槽位迁移的详细步骤
整个迁移过程由集群管理员通过 redis-cli --cluster 工具触发,但背后是一系列精细的协议和命令。我们可以将其分解为几个核心阶段:
阶段一:准备新节点并加入集群
- 启动新节点:启动两个新的 Redis 实例(一个主节点,一个从节点),并以集群模式运行。
- 加入集群:使用
CLUSTER MEET <ip> <port>命令,让新节点与集群中任意一个已知节点取得联系,并最终成为集群的一份子。此时,新节点是空的,不持有任何槽位。
阶段二:规划迁移
- 触发重分配:使用集群管理命令启动迁移流程:
1
redis-cli --cluster reshard <host>:<port>
<host>:<port>可以是集群中任意节点的地址。
- 设置迁移参数:
- 需要迁移多少槽位?:系统会提示你输入要迁移的槽位总数(例如,计划从旧节点转移 1000 个槽到新节点)。
- 接收槽位的目标节点 ID:输入新主节点的节点 ID。
- 从哪些节点迁移出来?:
- 你可以输入一个或多个源节点的 ID,迁移会平均地从这些节点中抽取槽位。
- 或者输入
all,表示从所有现有主节点中按比例抽取槽位,这是最公平的再平衡方式。
- 生成迁移计划:
redis-cli工具会根据你的输入,计算出一个具体的迁移方案:从哪个源节点迁移哪些具体的槽(如 slot 1-500 从节点A,slot 501-1000 从节点B)到目标节点(新节点)。
阶段三:执行迁移 - 数据同步与原子切换
这是最核心和最复杂的阶段,涉及多个步骤,由 redis-cli 工具自动执行,但背后是 CLUSTER 命令族在协作。
- 对每个槽位,发起迁移流程:对于迁移计划中的每一个槽(例如 slot 1234):
- 向源节点发送准备迁移命令:
CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
这条命令告诉目标节点(新节点):你准备接收槽位 1234,数据正在从源节点迁移过来。 此时,目标节点会为这个槽位创建一个特殊的导入中状态,对于发往这个槽的请求,如果它发现自己没有数据,会触发ASK重定向(后面会详述)。 - 向目标节点发送迁移数据命令:
CLUSTER SETSLOT <slot> MIGRATING <target-node-id>
这条命令告诉源节点(旧节点):你准备将槽位 1234 迁移到目标节点去。 此时,源节点会为这个槽位创建一个特殊的迁移中状态。
- 向源节点发送准备迁移命令:
- 迁移键值数据:
- 批量获取键:向源节点发送
CLUSTER GETKEYSINSLOT <slot> <count>命令,获取该槽位中最多count个键名。 - 逐个迁移键:对获取到的每个键,向源节点发送
MIGRATE <target-host> <target-port> "" 0 5000 KEYS key1 key2 key3 ...命令。MIGRATE是一个原子操作。源节点会序列化指定的键,然后原子地传输到目标节点。- 目标节点接收到数据后,会反序列化并存入自身数据库。
- 一旦传输成功,源节点会原子地删除本地的这个键。
- 这个过程是同步的,在迁移单个键的期间,两个节点都会阻塞,直到操作完成。但每次迁移多个键(KEYS 参数)和较小的超时时间(5000ms)使得这个过程是高效且可控的。
- 批量获取键:向源节点发送
- 发布槽位变更,完成迁移:
- 当一个槽位的所有键都迁移完毕后,集群配置需要更新。
- 管理员工具会向目标节点和所有源节点发送最终命令:
CLUSTER SETSLOT <slot> NODE <target-node-id> - 这个命令会原子性地将槽位的所有权正式分配给目标节点。
- 所有节点在接收到这个命令后,都会在本地更新它们的集群配置(cluster state),记录下“槽位 1234 现在归目标节点负责”。
- 这个配置信息会通过 Gossip 协议逐渐传播到集群中的所有节点。
阶段四:清理与验证
- 重复过程:对迁移计划中的每一个槽位,重复步骤 6-8。
- 验证集群状态:使用
redis-cli --cluster check <host>:<port>检查集群状态是否健康,确保所有槽位都已分配,且没有错误。
第三部分:如何保证迁移过程中的数据安全性?
这是 Redis Cluster 设计的精髓所在。它通过以下几种机制确保在迁移这个动荡时期数据不会丢失,服务尽可能可用。
1. 原子性的键迁移 (MIGRATE 命令)
这是数据安全的第一重保障。如前述,MIGRATE 命令是原子操作:
- 数据一致性:它像事务一样,要么成功地将一个键从源节点移动到目标节点,要么失败(超时或错误),两个节点的数据都不会处于损坏或中间状态。绝不会出现一个键在两边都存在或都不存在的情况(除非网络分区等极端情况)。
2. 双写与重定向机制 (ASK & MOVED)
这是保证可用性和读写正确性的核心。
客户端行为:一个优秀的 Redis Cluster 客户端(如 Lettuce, Jedis 等)会缓存一份槽位-节点的映射关系。
情况一:请求未迁移的键
- 客户端请求的键还在源节点。请求直接发给源节点,正常处理。无任何影响。
情况二:请求已迁移的键(客户端映射未更新)
- 客户端根据缓存的旧映射,把请求发给了源节点。
- 源节点发现自己正在迁移这个槽,并且请求的键
key已经不在自己这里了。 - 源节点不会直接处理这个请求,而是向客户端返回一个
-ASK重定向错误,并告知目标节点的地址。 - 客户端收到
-ASK错误后:- 首先向目标节点发送一个
ASKING命令(这是一个临时许可)。 - 然后重新发送原本的命令(GET/SET等)到目标节点。
- 首先向目标节点发送一个
- 目标节点收到命令后,会检查自己是否处于导入中状态。因为收到了
ASKING命令,它会破例执行这个本不属于它的槽位的命令。 - 关键点:
-ASK重定向是一次性的,客户端不会更新本地缓存的映射表。下次请求同一个键,它可能还会先发到源节点,再被重定向。这保证了在迁移未最终完成时,配置不会混乱。
情况三:请求已迁移的键(客户端映射已更新)
- 当集群配置通过
CLUSTER SETSLOT NODE更新并通过 Gossip 协议传播后,客户端的缓存映射也会更新。 - 此后,客户端会直接将请求发送给目标节点(新节点)。
- 如果目标节点已经完成了数据导入并正式拥有该槽位,它会正常处理请求。
- 如果客户端在更新映射后,请求了一个尚未被目标节点完全接收的键(极端情况),目标节点会发现这个键不存在,并返回
-MOVED错误,指引客户端去正确的节点(此时可能是源节点)。但这种情况在规划良好的迁移中很少见。
- 当集群配置通过
-ASKvs-MOVED:-MOVED:表示槽位的所有权永久地转移了。客户端应该更新本地缓存,以后都将请求发到新节点。-ASK:表示槽位的数据正在临时迁移中。客户端应该不更新缓存,只是临时性地请求一次新节点。
3. 配置更新的原子性与传播
CLUSTER SETSLOT NODE 是改变槽位归属的最终命令。它的更新是原子的,并且通过 Gossip 协议最终在所有节点达成一致。这确保了集群从一个一致状态过渡到另一个一致状态。
4. 网络故障容忍
如果迁移过程 (MIGRATE 命令) 中发生网络故障或超时,操作会失败。由于 MIGRATE 的原子性,键要么完全在源节点,要么完全在目标节点,不会出现迁移了一半的脏数据。管理员或工具可以重试迁移操作。
总结
Redis Cluster 的槽位迁移是一个精心设计的过程,它通过:
- 原子性的
MIGRATE命令保证单个键移动的数据安全。 -ASK重定向机制保证在迁移过程中,对已移动数据的读写请求能被正确地引导到目标节点,而不会丢失或错误。- 分步的槽位状态设置(IMPORTING/MIGRATING) 让节点能智能地处理处于中间状态的槽位。
- 最终的原子配置更新(SETSLOT NODE) 和 Gossip 协议来高效、一致地扩散新的集群拓扑。
这些机制共同工作,使得 Redis Cluster 能够在几乎不影响服务的情况下(客户端会遇到少量重定向,感知为延迟小幅增加),安全地完成数据的在线迁移和集群扩容。


