热点缓存如何拆分成多份来分散压力?
简化版
热点拆分是把一个逻辑 Key 映射为多个物理 Key,让请求按随机、哈希或业务维度分散到不同分片。只读数据可多副本随机读,可合并计数可分桶写后聚合;强一致状态不能直接复制多写。拆分会引入写扩散、版本切换和读聚合,必须先明确一致性语义。
详细版
先判断瓶颈来自读取 QPS、响应带宽还是写竞争。读热点可建立 key:v:0..N-1 多份副本,读端稳定随机选择;更新使用新版本批量构建,全部完成后原子切换 manifest,避免半新半旧。大对象热点更适合按字段/分页拆分,避免每次传输完整 value。
计数等满足可结合语义的数据可按请求或用户哈希写入 N 个桶,读取时求和;库存、余额等强不变量只能在预分配额度、中心仲裁或条件写下拆分,否则会超卖/重复扣款。桶数不是越多越好:它增加内存、写失效、读扩散和运维成本。
迁移需兼容新旧 Key:先让写路径生成新结构,回填与校验后灰度切读,最后停止旧写并延迟删除。监控每桶 QPS/内存的 max/avg、版本完成率、聚合误差和旧结构流量,防止热点只是从一个 Key 转成一个热点桶。
完整版教学
一、拆分前先判断热点属于哪一种
一个 Key 过热可能是每秒请求太多、单次响应太大,或大量写必须串行。复制小对象能解决读 QPS,却不能解决 20MB value 的带宽;分桶计数能解决可合并写,却不能直接用于余额。
| 热点类型 | 合理拆分 | 不宜做法 |
|---|---|---|
| 只读小对象 | 多物理副本随机读 | 所有读仍固定到副本 0 |
| 只读大对象 | 按字段/分页/块拆 | 完整复制后仍返回全量 |
| 可合并计数 | 多桶写、读取聚合 | 要求每次读取绝对实时却无聚合协议 |
| 有限库存 | 额度预分桶、售罄收口 | 每桶都保存总库存 |
| 余额/锁 | 保持单权威或共识 | 多份独立可写副本 |
记忆钩子:能否拆写取决于业务状态能不能安全合并;不能合并的数据只能优化入口,不能复制真相。
二、读副本如何把 QPS 分散
假设逻辑 Key product:42 承受 12 万 QPS,单分片安全容量 3 万 QPS。建立 8 个物理副本 product:42:v7:0..7,客户端按请求哈希选择,理想平均约 1.5 万 QPS/副本,留有偏差余量。
选择函数需分布均匀且在重试时可换副本。若按 userId 哈希,可获得会话稳定性;纯随机更均匀但难复现。虚拟副本必须通过不同 hash tag/slot 落到不同节点,Redis Cluster 中若所有 Key 使用同一 {product:42} hash tag,它们仍会在同一个 slot,拆分就失效。
replica = hash(requestId or userId) % 8
physicalKey = "product:42:v7:" + replica
注意:不要用相同 hash tag 强制它们同槽
三、版本化发布避免半新半旧
直接依次更新 8 份副本时,读请求会在更新窗口看到不同版本。可先离线写完整的新版本 v8:0..7,校验内容摘要与副本数量,然后原子更新一个小的 manifest 指向 v8。读端先读 manifest,再选 v8 副本。
manifest 自身可能成为热点,但 value 很小,可用本地短 TTL 缓存和版本通知。切换后保留 v7 一段时间用于回滚,确认无旧客户端后再删除。若 v8 构建只完成 7/8,禁止切换。
build v8 replicas -> verify 8/8 + checksum
-> SET product:42:active_version v8
-> observe -> retire v7
四、可合并写如何分桶
浏览量、点赞近似计数可写入 counter:42:0..15,桶由用户/请求哈希选择,读取时 MGET 后求和。16 个桶把单 Key 写压力理想降到 1/16,但一次精确读取会产生 16 倍读扩散。
可通过后台周期聚合到快照 Key,在线读“上次快照 + 增量桶”,在延迟与精度间取舍。清桶时要防聚合与新写竞争,可按 epoch 使用新一代桶,聚合旧 epoch 后再回收。
若要求每次返回绝对线性一致总数,分桶聚合仍需协调,性能收益会减弱。必须把允许误差和刷新周期写进业务契约。
五、库存预分桶为何更难
1000 件库存可以预分到 10 个桶各 100,入口把用户路由到某桶并原子扣减。某桶卖完时其他桶可能仍有库存,需要换桶或调拨;所有桶接近售罄时要进入中心收口,防止调拨与扣减竞态。
绝不能让每个桶都以为自己拥有 1000 件,那会产生最高 10000 件承诺。额度分配总和必须不超过权威库存,调拨使用版本与幂等流水。退款返还也要决定回原桶还是中心池。
global inventory 1000
allocate: bucket0..9 each 100, sum = 1000
sell: conditional decrement within bucket
rebalance: idempotent transfer ledger
tail: centralize remaining quota near sold-out
六、桶数和倾斜怎样选择
桶数至少要让单桶峰值低于单节点安全容量,并考虑哈希方差、节点故障和未来增长。桶太少仍热点,太多会增加 key 数、内存元数据、读扩散、发布和清理成本。
可从 ceil(hot_qps / safe_qps_per_shard) 得到理论下限,再乘 1.5~2 的余量并通过压测校准。12 万 QPS、每分片安全 3 万,理论 4 桶,实际可先选 8 而非直接 1000。
监控每桶 QPS 的最大/平均比。若某业务维度天然倾斜,简单哈希可能不够,需要对超级租户单独拆分或使用随机盐。
七、迁移、回滚与观测
迁移先上线能读旧/新结构的客户端,再双写或从权威源构建新桶;比较内容、总数和版本;按租户灰度切读,最后停止旧写。不要从可能已经陈旧的旧缓存直接当权威回填新结构。
指标包含逻辑 Key 总 QPS、各桶 max/avg、构建完成率、版本混读、聚合误差、旧 Key 访问和下游回源。回滚只需把 manifest 指回旧版本,前提是旧结构在观察期内仍被正确维护。
bucket_count: 8
max_to_avg_qps: 1.22
active_version: v8
replicas_ready: 8
old_version_traffic: 0.04%
aggregate_error: 0.1%
八、常见误区与追问
- 误区:复制多个 Key 后 Redis 会自动均匀分布。 Key 设计、hash tag 和哈希结果决定槽位,仍需验证实际分片。
- 误区:读热点拆分也能解决强一致写热点。 读副本可以复制,写入真相仍需原子约束或安全可合并语义。
- 误区:桶越多越能抗流量。 过多桶会放大内存、读取、发布与运维成本。
- 误区:逐个覆盖副本不会产生一致性问题。 更新窗口会混读新旧版本,应版本化构建后原子切换入口。
- 追问:为什么相同 Redis hash tag 会让拆分失效? 带相同
{...}的 Key 被强制映射到同一 slot,仍落在同一主节点。 - 追问:分桶计数如何清零? 使用 epoch/版本切换到新桶,再聚合和回收旧桶,避免与并发写竞态。
- 追问:库存何时需要收口? 接近售罄、桶间余量碎片明显时集中剩余额度,降低跨桶调拨与超卖风险。
九、加强记忆
热点拆分按“先辨读写、物理分槽、版本切换、控制桶数”设计。只读数据复制多份并随机读,大对象按访问维度拆;可合并写才做分桶聚合,库存需额度守恒,余额等强不变量不能多主复制。最终用每桶 max/avg 和版本完整性证明压力真的分散且数据没有混乱。