Redis Cluster:哈希槽、客户端路由与故障转移
Redis Sentinel可以让单个主库在故障后自动切换,但所有写入和全部键空间仍然集中在一个主节点。当数据量、网络带宽或写吞吐超过单机上限时,仅增加副本无法继续扩展写能力。
Redis Cluster 同时引入两件事:把键空间分片到多个主节点,并为每个主节点配置副本。它解决的是水平扩展和分片高可用问题,代价是客户端、数据模型和运维过程都变得更复杂。
Cluster 与 Sentinel 的根本区别
Sentinel 维护的是一个逻辑主库:任何时刻只有一个节点承载全部写入。Cluster 则有多个主节点,每个主节点只负责一部分键。
| 能力 | Sentinel | Redis Cluster |
|---|---|---|
| 写主节点数量 | 1 个 | 多个 |
| 数据分片 | 不支持 | 支持 |
| 故障转移 | 支持 | 支持,每个分片独立主从切换 |
| 客户端要求 | 支持 Sentinel 服务发现 | 支持 Cluster 路由和重定向 |
| 多键限制 | 普通 Redis 规则 | 相关键通常必须位于同一槽位 |
如果数据和写流量仍适合单机,Sentinel 往往更简单。Cluster 不应该因为“节点更多”而被默认选择。
为什么是 16384 个哈希槽
Redis Cluster 不直接把某个键永久绑定到某台服务器,而是增加了一层固定数量的哈希槽:
key ── CRC16(key) mod 16384 ──> slot ──> 当前负责该槽位的主节点
全部键空间被划分为 0 到 16383,总计 16384 个槽位。每个主节点负责若干槽位,例如:
Master-A:0 - 5460
Master-B:5461 - 10922
Master-C:10923 - 16383
固定的是“键到槽位”的算法,变化的是“槽位由哪个节点负责”。扩容时只需要迁移一部分槽位及其键,而不需要改变所有键的哈希规则。
16384 个槽位也不是 16384 个独立进程或数据文件。槽位只是路由和管理单位,节点仍然使用普通 Redis 数据结构保存键值。
客户端如何找到正确节点
Cluster 没有代理层。客户端可以先连接任意节点,但命令最终必须到达负责目标槽位的主节点。
Cluster 感知客户端通常会缓存一份“槽位到节点”的映射。第一次请求或拓扑变化时,Redis 通过重定向响应帮助客户端修正路由。
MOVED:稳定归属已经改变
当客户端访问了错误节点,而目标槽位已经明确属于另一个节点时,Redis 返回:
MOVED 12182 192.168.10.12:7000
这表示客户端应把请求发送到新地址,并更新本地槽位缓存。redis-cli -c 会自动跟随这种重定向;普通 redis-cli 只会显示错误。
ASK:槽位正在迁移
槽位迁移期间,一部分键可能已经到达新节点,另一部分仍在旧节点。此时旧节点可能返回:
ASK 12182 192.168.10.12:7000
ASK 是一次性指引。客户端应先向目标节点发送 ASKING,再执行当前命令,但不应立即永久修改整个槽位缓存。迁移完成后,稳定重定向才会变成 MOVED。
两者的差异可以概括为:MOVED 表示归属已改变,ASK 表示迁移中的临时访问路径。
哈希标签解决什么
默认情况下,键名的全部内容参与槽位计算。多个键落在不同槽位时,Cluster 无法在单个节点上原子执行涉及这些键的事务、Lua 脚本或多键命令。
哈希标签允许只使用键名中第一组有效花括号内容计算槽位:
order:{1001}:header
order:{1001}:items
两个键都使用 1001 计算槽位,因此会落到同一个节点。哈希标签应围绕确实需要共同操作的数据设计,而不是把所有键都放进同一个标签;后者会让流量重新集中到一个分片,失去水平扩展价值。
节点之间怎样交换状态
每个 Cluster 节点除了客户端端口,还需要一个 Cluster Bus 端口,默认是数据端口加 10000。节点通过二进制集群总线传播:
- 节点存活和疑似故障信息。
- 槽位归属及配置纪元。
- 主从关系和故障转移状态。
- 发布订阅等集群内部消息。
节点周期性交换 PING、PONG 和 Gossip 信息。每条 Gossip 消息会携带发送者已知的部分节点状态,因此拓扑变化可以逐渐扩散到整个集群。
客户端端口可达不代表集群一定健康。如果 Bus 端口被防火墙阻断,客户端可能还能连接单个 Redis,但节点之间无法可靠完成故障判断和配置传播。
从疑似故障到确认故障
Cluster 的故障判断也分为本地观察和集体确认:
- 某节点长时间无法联系另一个节点,会在本地把对方标记为
PFAIL。 - 这些疑似故障信息通过 Gossip 传播。
- 负责槽位的主节点中有多数认为目标主节点不可达时,目标被标记为
FAIL。 - 故障主节点的副本发起选举,争取其他主节点授权。
- 获得多数派支持的副本晋升,并接管原主节点的槽位。
多数派按“有投票权的主节点”计算,不按容器总数或副本数量计算。副本能够保存数据和参与晋升,但故障授权依赖主节点投票域。
3 主 3 从能承受什么故障
一个合理分布的 3 主 3 从拓扑可以让每个主节点的副本位于另一台物理机:
node-1 node-2 node-3
Master-A Master-B Master-C
Replica-B Replica-C Replica-A
任意一台物理机离线时,另外两台仍保留三个分片的数据副本,并且三个主节点投票域通常还能形成多数派完成晋升。
但“3 主 3 从”不等于可以任意失去三个节点:
- 如果一个主节点及其唯一副本同时丢失,对应槽位就没有可服务的数据副本。
- 如果剩余主节点无法形成多数派,集群不能安全授权故障转移。
- 如果主从被错误地放在同一台物理机,整机故障会同时带走一个分片的两份数据。
节点数量只是基础,主从对应关系和故障域分布才决定实际可用性。
Cluster 仍然是异步复制
每个 Cluster 主节点与副本之间仍使用 Redis 主从复制。主节点确认写入时,副本可能尚未收到复制流。
网络分区时,旧主节点可能在一小段时间内继续接受客户端写入,随后因无法联系多数派而停止服务。如果多数派一侧已经提升新主,那么旧主尚未复制的写入可能在重新加入时被丢弃。
Cluster 优先提供可用性和水平扩展,不提供跨分片强一致事务,也不能保证故障时零数据丢失。
扩容不是增加容器就结束
向集群添加一个空主节点后,它还没有负责任何槽位,也就不会自然承担业务数据。扩容通常包含:
- 新节点加入集群。
- 为新主节点安排副本。
- 从现有主节点迁移一部分槽位。
- 迁移槽位内的键,并观察
ASK到MOVED的状态变化。 - 验证槽位完整性和负载分布。
缩容则需要先迁走目标节点负责的全部槽位,再把节点安全移出集群。普通开源 Redis Cluster 不会因为新增节点就自动把槽位均匀重排。
三服务器 3 主 3 从最小部署
下面在三台 Linux 服务器上各运行两个 Redis 实例:
| 服务器 | 地址 | Redis 端口 | Bus 端口 |
|---|---|---|---|
| node-1 | 192.168.10.11 | 7000、7001 | 17000、17001 |
| node-2 | 192.168.10.12 | 7000、7001 | 17000、17001 |
| node-3 | 192.168.10.13 | 7000、7001 | 17000、17001 |
每个实例都必须有独立的 data/ 和 nodes.conf。以 node-1 的 7000/redis.conf 为例:
bind 0.0.0.0
protected-mode yes
port 7000
requirepass RedisBlog_ChangeMe_2026!
masterauth RedisBlog_ChangeMe_2026!
appendonly yes
appendfsync everysec
dir /data
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 192.168.10.11
cluster-announce-port 7000
cluster-announce-bus-port 17000
daemonize no
7001/redis.conf 把端口改为 7001/17001。node-2 和 node-3 使用相同结构,但 cluster-announce-ip 分别改为自己的服务器地址。
三台服务器使用相同的 compose.yml:
services:
redis-7000:
image: redis:8.8.0
container_name: redis-cluster-7000
restart: unless-stopped
network_mode: host
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
- ./7000/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7000/data:/data
redis-7001:
image: redis:8.8.0
container_name: redis-cluster-7001
restart: unless-stopped
network_mode: host
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
volumes:
- ./7001/redis.conf:/usr/local/etc/redis/redis.conf:ro
- ./7001/data:/data
在三台服务器启动节点:
docker compose up -d
六个端点全部可达后,只执行一次建群命令:
export REDISCLI_AUTH='RedisBlog_ChangeMe_2026!'
docker exec -e REDISCLI_AUTH="$REDISCLI_AUTH" redis-cluster-7000 \
redis-cli --cluster create \
192.168.10.11:7000 192.168.10.12:7000 192.168.10.13:7000 \
192.168.10.11:7001 192.168.10.12:7001 192.168.10.13:7001 \
--cluster-replicas 1 --cluster-yes
redis-cli 会尝试避免把主节点和副本放在同一 IP,但建群后仍必须检查实际主从关系,不能只相信创建顺序。
验证槽位与客户端路由
查看集群概要:
docker exec -e REDISCLI_AUTH="$REDISCLI_AUTH" redis-cluster-7000 \
redis-cli -p 7000 CLUSTER INFO
关键结果应为:
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_known_nodes:6
cluster_size:3
再检查节点角色和槽位覆盖:
docker exec -e REDISCLI_AUTH="$REDISCLI_AUTH" redis-cluster-7000 \
redis-cli -p 7000 CLUSTER NODES
docker exec -e REDISCLI_AUTH="$REDISCLI_AUTH" redis-cluster-7000 \
redis-cli --cluster check 192.168.10.11:7000
验收时确认有 3 个主节点、3 个副本、16384 个槽位全部覆盖,并且每组主从位于不同物理机。
使用 -c 验证客户端重定向:
docker exec -e REDISCLI_AUTH="$REDISCLI_AUTH" redis-cluster-7000 \
redis-cli -c -p 7000 SET article:cluster ok
docker exec -e REDISCLI_AUTH="$REDISCLI_AUTH" redis-cluster-7001 \
redis-cli -c -p 7001 GET article:cluster
停止某个已确认是主节点的实例后,观察它的跨主机副本是否晋升、槽位是否仍完整。故障实验必须依据 CLUSTER NODES 的实际关系选择节点,不能仅凭端口猜测角色。
什么时候选择 Cluster
适合选择 Redis Cluster 的情况:
- 数据量无法稳定放入一台 Redis。
- 写吞吐或网络带宽需要多个主节点共同承担。
- 客户端支持 Cluster 协议,数据模型能够适应槽位限制。
- 团队具备管理槽位迁移、跨故障域副本和扩缩容的能力。
如果容量仍在单机范围,只需要自动主从切换,Redis Sentinel通常更简单。如果还没有建立复制、偏移量和异步一致性的基础认识,先阅读 Redis 主从复制:从全量同步到人工故障切换。