场景:一台 Redis 装不下、扛不住了
单实例 Redis 受两个硬约束限制:内存(单机内存有上限,且实例越大 fork 越慢)和吞吐(单线程处理命令,一个核的算力到顶)。当数据量或 QPS 突破单机天花板,就需要把数据水平拆分到多台机器,同时还要保证某台挂了服务不中断。Redis Cluster 就是官方的分布式方案,把"分片"与"高可用"做进了同一套协议里。
分片机制:16384 个哈希槽
最朴素的分片是按 key 取模:node = hash(key) % N。但这有致命缺陷——扩容时 N 变了,几乎所有 key 的归属都变,要搬几乎全部数据。Redis Cluster 引入一层间接:不直接把 key 映射到节点,而是先映射到固定的 16384 个哈希槽(hash slot),再把槽分配给节点。
1 | slot = CRC16(key) % 16384 # key 先定位到槽 |
这层间接的好处是:节点数变化时,只需迁移部分槽,而非重算所有 key。扩容就是把一些槽连同其数据从老节点搬到新节点,其余 key 归属不变。为什么是 16384 而不是更大?这是个工程权衡:节点间要交换"谁负责哪些槽"的位图,16384 位 = 2KB,心跳包里带得起;槽再多,心跳开销和内存都吃不消,而 16384 对常见集群规模(几百节点内)已经足够细。
hash tag:让相关 key 落在同一槽
Redis Cluster 不支持跨槽的多 key 操作(如 MGET、事务、Lua 操作多个 key),因为这些 key 可能分散在不同节点。如果你需要几个 key 一起操作,用 hash tag:只对 {} 内的部分计算槽。
1 | SET {user:1000}:profile "..." |
这是把"数据局部性"的控制权交给业务方:你自己决定哪些 key 该聚在一起。代价是用不好会造成热点槽——把太多 key 塞进一个 tag,该槽所在节点压力陡增。
请求路由:MOVED 与 ASK
集群没有中心代理,客户端直连节点。当客户端把请求发到不负责该槽的节点,节点不会代为转发,而是回一个重定向:
1 | GET foo |
智能客户端(smart client)会缓存"槽 -> 节点"的映射表,大多数请求一次命中,只在集群拓扑变化时靠 MOVED 纠正并刷新本地映射。这把路由智能下放到客户端,服务端不必做代理转发,省了一跳。
迁移槽的过程中还有个 ASK 重定向。当一个槽正从 A 迁往 B,部分 key 已在 B、部分还在 A:客户端访问该槽先问 A,A 若发现这个 key 已迁走,回 ASK(临时重定向,只对这一次请求生效,不刷新映射表),客户端带 ASKING 标记去 B 取。MOVED 是"槽永久归别人了,更新你的表";ASK 是"这个 key 暂时去那边问问,别改表"。区分这两者是理解平滑迁移的关键。
高可用:主从复制 + 故障转移
分片解决容量,但每个分片(master)单点故障会丢一片数据。所以每个 master 配若干 replica(从节点),异步复制主的数据。master 挂了,集群自动把它的一个 replica 提升为新 master——这就是故障转移。
故障如何被发现:PFAIL 到 FAIL
节点间通过 Gossip 协议互相发心跳(ping/pong),交换彼此的存活状态和槽分配信息。判定流程是两级的:
1 | 1. 节点 A 心跳超时联系不上 B -> A 把 B 标记为 PFAIL(疑似下线,主观判断) |
两级判定是为了避免误判:单个节点因网络抖动联系不上 B,不能就判 B 死;必须多数 master 达成共识(类似 quorum),才确认下线。这跟分布式系统里"避免脑裂下的误判"是一脉相承的。
故障转移:replica 的选举
master B 被判 FAIL 后,它的多个 replica 竞争上位,过程类似 Raft 的领导选举:
1 | 1. replica 发现自己的 master FAIL,发起拉票 |
为减少数据丢失,复制数据更新(offset 更大)的 replica 优先发起选举——它落后主的数据最少。但因为复制是异步的,master 宕机时未同步到 replica 的那部分写会丢失,这是 Redis Cluster 为可用性付出的一致性代价(它选择了 AP 而非 CP)。
工程权衡与边界
- 一致性:异步复制 + 故障转移意味着极端情况会丢数据、可能短暂出现两个 master 写(脑裂窗口)。
cluster-node-timeout调小能更快发现故障,但更易因抖动误判触发不必要的转移;调大则故障恢复慢。这是个需要按网络质量校准的旋钮。 - 多 key 操作受限:跨槽的事务、Lua、MGET 不可用。业务设计要么用 hash tag 把相关 key 聚拢,要么在应用层拆成多次单 key 操作。这往往是从单机迁到集群最痛的改造点。
- 客户端复杂度:需要 smart client 维护槽映射、处理 MOVED/ASK。用不支持集群的老客户端会直接报错或性能极差。
- 最小规模:要保证故障转移能选举成功,至少需要 3 个 master(否则一个挂了剩下凑不齐多数);加上各自的 replica,生产通常 6 节点起步。
常见踩坑
坑一:用了 {} 但把太多 key 塞进同一 tag,造成单槽数据/流量倾斜,该节点成瓶颈,失去了分片的意义。hash tag 要克制使用。
坑二:以为集群自动均衡数据。 新加节点后,槽不会自动迁移过来,需要手动(或借助工具)做 reshard 把槽分配给新节点。光加机器不 reshard,新节点是空的、不分担负载。
坑三:跨槽操作直接报错没预案。 迁移到集群后,原来好好的 MGET/事务突然报 CROSSSLOT 错误。上线前必须梳理所有多 key 操作,提前用 hash tag 或改写。
坑四:cluster-node-timeout 默认值不审视。 网络抖动频繁的环境用太小的值,会反复触发故障转移,集群"自己折腾自己",反而比单点更不稳。
小结
Redis Cluster 用"key → 16384 槽 → 节点"的两级映射实现可平滑扩容的分片,用 hash tag 把数据局部性的控制权交给业务,用 MOVED/ASK 让 smart client 自行路由而免去代理转发;高可用侧用 Gossip 传播状态、用 PFAIL→FAIL 两级判定避免误判、用多数派投票选举新 master 完成故障转移。它的本质取舍是为水平扩展和高可用,牺牲了强一致(异步复制可能丢数据)和多 key 操作的便利。理解这些机制与边界,才能判断你的业务到底该上集群,还是单实例/主从就够了。