集群再大,key 只住一个分片
Redis Cluster 把 key 按哈希槽摊到各个分片上,横向扩容应对的是「总量大」。但热 Key 是另一种病:单个 key 的 QPS 高到打满它所在的那个分片。明星官宣、爆款秒杀、突发热搜,一条 key 每秒被敲几十万次——这个分片的 CPU 和网卡先饱和,落在它身上的其他 key 全部跟着遭殃。更尴尬的是,加机器解决不了:key 哈希到固定的槽、住在固定的分片,扩容不会把它挪薄。
击穿是热 Key 的「过期瞬间」切片,热 Key 是它的「全程放大版」:不只是重建那一下拥挤,是每一秒都在拥挤。
先发现:热 Key 藏在哪
热 Key 的麻烦从发现就开始——它平时不存在,爆发在毫无准备的时刻。发现手段按层级排:客户端统计最直接——在访问封装层按 key 计数、按秒窗口聚合,超过阈值就上报,代价是每客户端一份统计;proxy 层聚合最全面——有代理架构的,代理天然看到全部流量,统计一次全局共享;服务端观测兜底——redis-cli --hotkeys 借助 LFU 计数找热点,监控面板盯单分片的 QPS 与 CPU 突刺。事后的兜底永远慢半拍,客户端埋点加阈值告警是主流选择:热度超阈值的 key 实时推给治理层。
治理一:本地缓存顶在最前
热 Key 的读流量里,绝大多数是重复读同一个不变的数据——这种流量根本不必每次都出进程。本地缓存(Caffeine 挂在应用内存里)挡在 Redis 前面,极热 key 在本地命中,Redis 只承接本地过期后的回源:
// 进程内一级缓存:扛住绝大多数热读
LoadingCache<String, String> hotCache = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterWrite(3, SECONDS) // 秒级 TTL,容忍短暂旧值
.build(k -> redis.get(k)); // 未命中回源 Redis秒级 TTL 换来的旧值窗口对大多数业务无感,Redis 的压力直接降一个数量级。代价是每个应用实例一份副本、失效通知要额外通道——这本就是多级缓存的雏形,完整展开留给第 16 篇。
治理二:多副本打散
本地缓存扛不住的读,或者本地缓存没法用的场景(实例少、数据大),用空间换分散:同一个数据存 N 份副本,key 带上随机后缀摊到不同槽位上,读的时候随机挑一份:
// 写:一份热数据存 10 个副本
for (int i = 0; i < 10; i++) {
redis.setex("item:detail:" + id + ":" + i, 1800, toJson(d));
}
// 读:随机挑一个副本,流量摊到 10 个分片
int slot = ThreadLocalRandom.current().nextInt(10);
String v = redis.get("item:detail:" + id + ":" + slot);分片压力从 1 摊到 10,但写路径要维护 N 份副本的一致性——更新时全量刷新或轮换版本号。两个治理手段不互斥:本地缓存挡第一波,多副本兜第二波,读写分离的只读副本再接一层,热 Key 就从「一个分片扛全站」变回「大家分着扛」。
热 Key 是流量往少数 key 上堆,它的孪生兄弟是数据往少数 key 上堆——一个 key 里塞了十万条成员、一个 string 里塞了几 MB 内容,问题从「读得多」变成「本身太重」。大 Key 这颗定时炸弹,下一篇拆。
评论 (0)