REDIS · BIG KEY · HOT KEY · --BIGKEYS · --HOTKEYS · UNLINK · LOCAL CACHE
一个 key 太胖,或者一个 key 太红,都能拖垮一个节点
Redis 把压力分摊开靠的是“key 很多、每个都不大、访问也分散”。大 key 打破了“每个都不大”,热 key 打破了“访问分散”。两种都会让某一个节点、某一条命令、某一段网络单独被压垮,而加机器往往没用。
大 keybig key
一个 key 里塞了几十万个元素,或者一个值就有几 MB。读它、删它、迁移它,都要一次干完,主线程和网络被它独占。
热 keyhot key
一个 key 每秒被读几万次。它只能在一个槽、一个节点上,那个节点 CPU 和网卡被打满,别的节点却很闲。
big key值太大或元素太多,一次操作就要很久。
问题在“大小”hot key同一时间所有人都来读它,分不出去。
问题在“集中”--bigkeys · --hotkeys先找到是哪个 key,再对症下药。
尽量在从节点或离线做拆分 · 复制 · 本地缓存大的切小,热的多放几份、挡在外面。
都有代价大 key:多大算大,大了有什么坏处
没有统一标准,常见的经验线是:String 超过 10 KB;Hash、List、Set、ZSet 的元素超过 5000 个,或者总大小超过 10 MB。真正要看的是:操作它一次要多久、传它一次要多少流量。
大 key 计算器:一个 Hash 有多危险
粗略估算:读每个元素约 1 微秒,释放每个元素约 0.5 微秒,每个元素额外开销约 40 字节;实际和机器、编码有关,只看数量级。
大 key 怎么治
核心就一句:别让一次操作碰到太多数据。能拆就拆,拆不了就少读、分批读、后台删。
user:follows:1001一个 Hash,100 万个字段读写某个字段时,先算 hash(field) % 100 找到它在哪一份,只碰那一份。集群下这 100 份还会落到不同节点,内存也均匀了。
static final int SHARDS = 100;
String shardKey(long userId, String field) {
int i = Math.floorMod(field.hashCode(), SHARDS);
return "user:follows:" + userId + ":" + i;
}
redis.opsForHash().put(shardKey(uid, f), f, value); // 写只碰一份
redis.opsForHash().get(shardKey(uid, f), f); // 读也只碰一份
少读:用 HMGET 取需要的字段,别 HGETALL;LRANGE 只取一页。
分批读:HSCAN、SSCAN、ZSCAN 每次取几百个。
后台删:用 UNLINK,或打开 lazyfree-lazy-user-del;Redis 4.0 之前只能用 HSCAN + HDEL 一批批删。
压缩大 String:几 MB 的 JSON 用 gzip 或 snappy 压一下,或者干脆放到对象存储,Redis 里只存地址。别用 JDK 序列化,它会让值变大很多(Redis 第 04 篇)。
热 key:为什么加机器没用
秒杀商品、热搜第一、明星官宣、首页配置,这些 key 一个就能占掉一个节点大半的流量。集群按 key 分片,同一个 key 永远在同一个槽、同一个节点(Redis 第 03 篇)。加再多节点,分走的也只是别的 key。
热 key 实验台:集群总共每秒 15 万次请求
示意模型:普通 key 均匀分布;热 key 本来全在节点 B。本地缓存命中 90%;复制 3 份并保证落在 3 个不同节点;从节点分担热 key 一半的读;限流把打到 Redis 的热 key 请求控制在每秒 2 万。
热 key 怎么治
思路是让热 key 的请求“到不了”或者“分得开”。最有效的通常是在应用里加一层本地缓存,因为热 key 的特点就是一份数据被反复读。
Cache<String, String> local = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(2)) // 最多 2 秒的旧数据
.build();
String get(String key) {
// 同一个 key 并发时只有一个线程去 Redis(MyBatis 第 04 篇讲过)
return local.get(key, k -> redis.opsForValue().get(k));
}
static final int COPIES = 3;
void setHot(String key, String v) {
for (int i = 0; i < COPIES; i++)
redis.opsForValue().set(key + "#" + i, v, Duration.ofMinutes(10));
}
String getHot(String key) {
int i = ThreadLocalRandom.current().nextInt(COPIES);
return redis.opsForValue().get(key + "#" + i);
}
// 不能用 {key}#i 这种 hash tag,否则几份会落在同一个槽
| 方案 | 怎么做 | 效果 | 代价 |
|---|---|---|---|
| 本地缓存(多级缓存) | 每台应用用 Caffeine 缓存热 key 几秒 | 最有效,热 key 请求大部分不出应用 | 几秒内数据不一致;要知道哪些是热 key |
| Redis 客户端缓存 | Redis 6 的 CLIENT TRACKING:客户端缓存读过的 key,值变了服务端主动通知失效 | 一致性比固定过期更好 | 要 RESP3;客户端要支持(Lettuce 6 有);被追踪的 key 多了服务端也要记 |
| 复制多份打散 | key#0~key#N 分到不同节点,读随机挑 | 压力分到 N 个节点 | 写要写 N 份,可能短暂不一致;多占内存 |
| 从节点分担读 | Lettuce 配 ReadFrom.REPLICA_PREFERRED | 读能力翻倍左右 | 从节点有复制延迟;对所有 key 生效 |
| 限流降级 | Sentinel 的热点参数限流,超过阈值走兜底 | 节点一定不被打垮 | 一部分用户拿到降级结果 |
| 提前预热、不过期 | 活动前把已知热点写进去,不设过期,定时刷新 | 避免热 key 过期那一刻击穿到数据库 | 要提前知道谁会热(Redis 第 01 篇) |
| 加节点 | 扩容集群 | 对热 key 几乎没用 | 花钱,只分走了别的 key |
怎么找到它们
找大 key 和热 key 本身也可能伤到线上实例。能在从节点上做就别在主节点上做,能离线分析就别在线扫。
| 工具 | 找什么 | 原理 | 注意 |
|---|---|---|---|
redis-cli --bigkeys | 大 key | 用 SCAN 扫一遍,每种类型报告元素最多(String 报字节最多)的那个 | 只报每种类型的第一名;加 -i 0.1 每批之间歇一下 |
redis-cli --memkeys | 大 key | SCAN + 对每个 key 调 MEMORY USAGE | 按真实内存排,比 --bigkeys 准,也更慢 |
MEMORY USAGE key | 单个 key 多大 | 估算这个 key 占的字节数 | 集合类默认抽样 5 个元素估算 |
| 离线分析 RDB | 大 key | 拿一份 RDB 文件,用 redis-rdb-tools 等工具统计 | 对线上零影响,适合定期巡检 |
redis-cli --hotkeys | 热 key | SCAN + OBJECT FREQ 看访问频率 | 要求淘汰策略是 LFU 类,否则报错 |
MONITOR | 热 key | 实时打印所有命令,统计次数 | 吞吐可能直接掉一半,只能短时间用 |
| 客户端或代理统计 | 热 key | 在客户端埋点,按 key 计数,汇总后发现热点并推送给各实例 | 最实时,能和本地缓存联动;要自己开发或用现成框架 |
Redis 版本,以及 JDK 8 和 JDK 17
治理手段越来越多,但大 key 和热 key 的本质没变。Java 这边主要是客户端能力和本地缓存库的版本。
Redis 4.0 ~ 5 · Boot 2
JDK 8+- 4.0 起有 UNLINK、lazyfree、LFU 淘汰策略和
OBJECT FREQ,--hotkeys 才能用 - 4.0 起有
MEMORY USAGE - 本地缓存用 Caffeine 2.x;Boot 2.4 起默认的 Lettuce 已经是 6.x
Redis 6 ~ 7 · Boot 3
JDK 17+- 6.0 起支持 RESP3 和客户端缓存(
CLIENT TRACKING),Lettuce 6 提供ClientSideCaching - 6.0 起有
lazyfree-lazy-user-del,普通 DEL 也能后台释放 - 本地缓存用 Caffeine 3.x;虚拟线程下更要防止大量请求同时去 Redis 读同一个热 key,用
cache.get(key, loader)合并
常踩的坑
很多大 key 和热 key 不是设计出来的,是慢慢长出来、突然火起来的。
业务刚上线时只有几百个元素,几年后有个大 V 有几百万粉丝。设计时就按用户和业务 ID 拆分,并对元素个数做监控告警。
用了 DEL。改用 UNLINK;老版本用 HSCAN / SSCAN 一批批删。
槽里有个大 key,MIGRATE 超时。先把大 key 拆掉或删掉,再迁移。
数据倾斜:要么有大 key,要么用 hash tag 把太多 key 塞进了同一个槽。用 --bigkeys 和 CLUSTER COUNTKEYSINSLOT 查。
热 key。加本地缓存、复制多份打散;临时先对这个 key 限流。
热 key 和缓存击穿常常一起出现。已知的热点提前预热、逻辑过期,重建时加互斥锁。
写了 N 份但不是同时生效。对一致性要求高的数据别用这招,或者改完价格后等所有副本写完再生效。
没设 maximumSize,所有读过的 key 都缓存了。本地缓存只放热 key,并限制大小和过期时间。
MONITOR 要把每条命令都发给你,吞吐可能掉一半。用 --hotkeys(需 LFU)、客户端统计,或者短时间在从节点上看。
它每种类型只报最大的那个。要完整清单,用 --memkeys 或离线分析 RDB。