内存条加到头了,然后呢
哨兵上线后,缓存安稳了半年。直到某天容量报表递到面前:Redis 实例 60G,机器内存 64G——内存条加到头了。这时候才明白第 12 篇结尾那句话的分量:哨兵救的是「挂」,救不了「装不下」。主从模式里每个节点都存着全量数据,加再多从库也只是副本翻倍,容量纹丝不动。想让容量横向扩,只有一条路:把数据切开,分片存储——这就是 Cluster 集群。
分片的核心问题只有一个:一条数据来了,该放进哪台机器?接下来就沿着这个问题的三个候选答案,看 Redis 作者 antirez 为什么最终选了「哈希槽」。
三个候选答案
| 方案 | 扩缩容时的影响 | 倾斜风险 |
|---|---|---|
| hash(key) % N | N 变了,几乎全部 key 要搬家 | 无 |
| 一致性哈希 | 只有相邻区间的 key 迁移 | 节点在环上分布不均时倾斜 |
| 哈希槽 | 只搬被移走的槽,粒度可控 | 槽位规划明确,风险可控 |
取模方案的死穴在扩容:3 台变 4 台,取模结果几乎全部变化,等于数据大迁徙。一致性哈希用「环」缓解了这个问题,但环上节点位置随机,容易倾斜,还得引入虚拟节点来匀。antirez 的答案是多加一层间接:固定 16384 个哈希槽,key 先映射到槽,槽再分配给节点。槽是死的、可枚举的,扩缩容变成「搬几个槽」的精细操作,数据分布从此有了账本。
槽怎么算:CRC16 与 hash tag
每个 key 落在哪个槽,由 CRC16(key) mod 16384 决定,简单到没有悬念。但这里藏着 Cluster 模式最实用的技巧——hash tag:key 中花括号里的内容才参与哈希计算。
# 普通键:user:1001 和 order:1001 大概率不在同一槽
# hash tag:花括号里的内容参与计算,强制落到同一槽
mset {order:1001}:detail xx {order:1001}:items yy为什么需要它?Cluster 的多 key 命令、事务、Lua 脚本都要求所有 key 落在同一个槽。一个订单的详情、明细、状态如果能用同一个 tag 圈起来,业务代码几乎不用改造。顺带回答那个经典问题——为什么是 16384 而不是 65536:节点之间要用心跳交换「我负责哪些槽」的位图,16384 个槽的位图是 2KB,65536 就是 8KB;而官方建议的集群规模(1000 节点以内)用不到 65536 个槽,作者干脆取了小号,省带宽。
去中心化:MOVED 与 smart client
Cluster 没有中心节点,每个节点都知道全集群的槽位分布,节点之间用 gossip 协议互相同步状态。你直接给任意节点发命令,试试会发生什么:
# 请求发到了不负责这个槽的节点
get user:1001
# (error) MOVED 9189 10.0.0.2:6379MOVED 重定向的意思是:这个槽归 10.0.0.2 管,你去问它。客户端要是每次都吃一次重定向,性能就没法看了——所以成熟的客户端都是 smart client:启动时拉一份「槽位→节点」映射表缓存在本地,请求直接打对节点,只有拓扑变化时才重新拉取。Java 里用 Lettuce 只需给种子节点:
// Cluster 模式:给种子节点,客户端自动发现全部拓扑
RedisClusterClient client = RedisClusterClient.create(
RedisURI.create("redis://10.0.0.1:6379"),
RedisURI.create("redis://10.0.0.2:6379"),
RedisURI.create("redis://10.0.0.3:6379"));扩缩容:搬槽进行时
加节点的流程由 redis-cli --cluster reshard 一键完成:从现有节点匀一批槽给新节点。真正有趣的是迁移过程中的语义——被迁移的槽里的 key 正在半路上,此时访问:源节点返回 ASK 重定向(这次去问目标节点,但映射表别改),目标节点确认后就绪后,后续访问才变成正式的 MOVED。ASK 和 MOVED 的区别,就是「临时指路」和「正式改地址」的区别。缩容反过来:先把槽搬空,再下线节点。
最后是一份「上了 Cluster 要改的习惯」清单:
- 多 key 操作先想槽:mget、事务、Lua 都要求同槽,跨槽就把批量拆成单发,或用 hash tag 规划 key。
- 只有 db0:Cluster 模式不支持多数据库,依赖 select 1 的老代码要改。
- 高可用是自带的:每个主节点配一个从节点,主挂了从自动顶上,不需要也不应该再叠哨兵——Cluster 自己会选主,别把它和哨兵混着用。
- 规模别超配:官方建议 1000 节点以内,gossip 同步和槽位管理的成本会随节点数上涨,多数业务几十个节点绰绰有余。
总结:哈希槽的本质是在 key 和节点之间垫了一层可枚举的中间层,扩缩容从「数据大迁徙」变成「搬槽」;hash tag 解决多 key 同槽,smart client 消化重定向,高可用自带免哨兵。到这里,Redis 的架构三部曲(主从、哨兵、集群)就讲完了。下一篇回归日常编码——性能优化:为什么 keys 命令是生产毒药?Pipeline 怎么把一万次网络往返压成一次?慢查询日志又该怎么读?下篇见。
咖啡凉了,记得趁热喝。
评论 (0)