在日均百亿次调用的缓存架构中,单实例 Redis 受限于内存容量与单线程事件循环的网卡软中断瓶颈,必须通过分片集群进行水平扩展。本文结合我们在生产环境维护数百节点 Redis Cluster 的经验,探讨其槽位路由机制、扩缩容痛点与大 Key 治理方法。
一、为什么哈希槽数量设计为 16384 而非 65536
Redis Cluster 使用 CRC16(key) & 16383 将所有键映射到 0 ~ 16383 共 16384 个哈希槽(Hash Slot)中。作者 Salvatore Sanfilippo (antirez) 曾明确解释过为什么不直接用完 CRC16 的 65536 个槽位:
- Gossip 心跳包体积开销:每个节点定期发送的
PING/PONG消息头中需要携带一个位图(Bitmap)广播自己负责的槽位配置。在 16384 槽位下,该位图大小为 $16384 / 8 = 2\text{ KB}$;若改为 65536 槽位,每次心跳包光槽位位图就要消耗 $8\text{ KB}$,在节点数较多时会浪费大量内网带宽。 - 集群规模上限:由于 Gossip 协议通信开销随节点数呈平方级增长,官方建议 Redis Cluster 主节点数量不超过 1000 个。对于 1000 个以内的主节点,16384 个槽位已经足够实现均匀分布。
- 压缩效率:节点内存中以位图记录槽位归属,较小的位图在配置纪元(Config Epoch)同步时具备更高的缓存局部性。
二、MOVED 重定向与 ASK 临时重定向的区别
当客户端连接集群任意节点发起请求时,如果目标 Key 所属的哈希槽不在当前节点:
MOVED(槽位已固定属于其他节点):当前节点返回-MOVED 3999 10.0.1.12:6379。智能客户端收到后,不仅会向目标节点重发命令,还会更新本地缓存的Slot -> Node路由映射表。ASK(槽位正在迁移过程中):当槽位3999正处于从源节点 A 向目标节点 B 在线迁移的状态(源节点标记为MIGRATING,目标节点标记为IMPORTING),如果访问的 Key 已经被迁走,源节点 A 返回-ASK 3999 10.0.1.12:6379。客户端必须先向目标节点 B 发送ASKING命令打开一次性访问许可,再发送实际读写命令,且不更新本地永久路由表。
bash
# 使用 Hash Tag 强制将需要执行 Multi-Key 或 Lua 原子操作的键绑定到同一个哈希槽
127.0.0.1:6379> MSET user:{10086}:profile "..." user:{10086}:settings "..."
OK
127.0.0.1:6379> CLUSTER KEYSLOT "user:{10086}:profile"
(integer) 5474
127.0.0.1:6379> CLUSTER KEYSLOT "user:{10086}:settings"
(integer) 5474三、在线迁移与大 Key(BigKey)引发的主线程阻塞
Redis Cluster 在进行槽位迁移时,底层依赖 MIGRATE 命令:源节点通过 DUMP 序列化单个 Key 的全部内容,通过网络发送给目标节点并等待目标节点反序列化恢复完成。
如果某个 Hash 或 ZSet 类型的 Key 包含 50 万个元素(体积达数十 MB),MIGRATE 执行期间源节点和目标节点的事件循环都会被阻塞数百毫秒甚至数秒,严重时会导致集群心跳超时并误触发主从故障转移(Failover)。
生产环境大 Key 排查与治理规范
- 低峰期扫描定位:在从节点(Replica)上执行
redis-cli --bigkeys -i 0.05,或者离线解析 RDB 快照文件(使用rdbtools生成内存报表),绝不在主节点高峰期跑全量扫描。 - 渐进式删除:对大 Key 绝对禁止直接执行
DEL,应使用UNLINK(Redis 4.0+ 异步线程惰性释放内存),或通过HSCAN/SSCAN/ZSCAN每次小批量清理后再删除空壳。 - 分桶拆散结构:对于元素极多的 Hash(如百万级商品库存映射),在业务层对
field再做一次二次哈希取模,拆分为 64 个小 Hash(如product:stock:0~product:stock:63),确保每个子 Key 的元素数量控制在 2000 以内。