主题
第十三章:大 Key 问题与治理
Q40:什么是大 Key、热 Key?它们会带来什么问题?
答:
「大 Key」和「热 Key」是两个不同的概念,但都可能导致 Redis 出现性能抖动,需要单独识别和治理。
一、什么是大 Key
大 Key 通常指单个 key 的 value 体积过大。
Redis 内部无论是 String 还是集合,本质都是「一个 key 对应一块数据」,这块数据越大,操作它时的成本就越高。
常见的大 Key 形式:
| 类型 | 大 Key 表现 | 典型来源 |
|---|---|---|
| String | 单个 value 几十 KB 甚至几 MB | 整段 JSON、序列化的长文本、大对象快照 |
| List / Set / ZSet | 单个 key 里塞了上万甚至几十万个元素 | 消息堆积、加购物车未清理、粉丝列表 |
| Hash | 单个 key 里成千上万个 field | 用户画像无限膨胀、把整个表塞进一个 Hash |
| Stream / Bitmap 等 | 消息 ID 堆积、offset 稀疏导致底层字符串超大 | 日志流堆积、不合理的 offset 设计 |
大 Key 本身不一定有「对错」,但会带来几个实实在在的问题:
阻塞主线程:Redis 是单线程执行命令,
DEL一个超大的 key、HGETALL一个拥有百万 field 的 Hash、KEYS/SMEMBERS遍历超大集合,都会长时间占用主线程,导致其他客户端命令排队等待。一旦单条命令耗时达到秒级,整个 Redis 就会出现「卡顿」。网络抖动:读写大 Key 时,单次传输的数据量大,容易撑满网卡带宽,挤占其他命令的传输,造成整体延迟上升。
内存浪费与碎片:大 String 反复整体覆盖更新,容易产生内存碎片。
过期删除卡顿:一个超大 key 过期时,主线程要一次性回收它,同样可能卡一下。
迁移 / 持久化困难:在 Cluster 中,大 Key 会导致单节点负载不均;在 RDB/AOF 持久化、主从同步时也会拖慢整体节奏。
二、什么是热 Key
热 Key 指被极其频繁访问的 key,例如秒杀商品、明星微博、排行榜头部数据。访问量集中到极少数 key,会让它们所在的分片承受不成比例的负载,成为集群的「热点」。
大 Key 和热 Key 可能同时出现(一个超大 List 又被海量并发读写),这种情况最危险。
三、问题的直观表现
text
大 Key 热 Key
value = 100 万行的哈希表 每秒 10 万次 GET hot:goods:1001
│ │
├─► DEL 耗时 3 秒,主线程被卡住 ├─► 单节点 QPS 被打满
├─► HGETALL 返回 20MB,网卡被挤爆 ├─► 集群负载严重倾斜
└─► 过期回收卡顿 └─► 大量请求集中到同一分片Q41:如何发现和定位大 Key、热 Key?
答:
治理的前提是「先看见」。发现大 Key、热 Key 的手段,从轻量到深入依次是:
一、用 Redis 自带命令做初步扫描
SCAN + 类型特异性命令:不要用 KEYS(会阻塞主线程),用 SCAN 游标遍历,配合类型的长度/大小命令逐个判断。
bash
# 用 SCAN 遍历 key
redis-cli --scan --pattern '*'
# 针对不同类型查大小
redis-cli STRLEN user:1001:profile # String 的字节长度
redis-cli LLEN task:queue # List 元素个数
redis-cli HLEN user:1001:tags # Hash 的 field 个数
redis-cli SCARD video:6:liked_users # Set 元素个数
redis-cli ZCARD rank:video:hot # ZSet 元素个数MEMORY USAGE:直接查某个 key 实际占用的内存字节数,比元素个数更准确。
bash
redis-cli MEMORY USAGE user:1001:profile
redis-cli MEMORY USAGE rank:video:hot二、用 redis-cli 提供的分析工具
Redis 自带两个常被忽略的分析工具:
bash
# 扫描并找出大 key
redis-cli --bigkeys
# 配合选项,指定扫描采样数量
redis-cli --bigkeys -i 0.1--bigkeys 会按类型统计每种结构里元素最多、字节最大的 key,并给出汇总。优势是开箱即用;局限是它主要按「元素个数」判断,对单个大 String 的字节数判断有限,且默认会遍历全部 key,量大时本身也有开销。
新版 Redis 也提供了 MEMORY DOCTOR 做内存诊断,以及 redis-cli --memkeys 辅助分析。
三、热 Key 的发现
热 Key 是「访问频率」问题,用静态扫描发现不了,需要动态观测:
| 手段 | 说明 |
|---|---|
redis-cli --hotkeys | 基于内存采样估算热门 key(需要配置 maxmemory-policy 为 LFU) |
MONITOR | 实时打印所有命令,可以观察高频 key,但会显著拖慢性能,生产慎用 |
慢日志 SLOWLOG | 发现耗时异常的命令,间接定位大 Key/热 Key |
| 客户端埋点 / 指标系统 | 在应用侧统计 key 的访问次数,最精确但需要额外开发 |
| 代理层统计(如 Codis 等) | 中间件层统计访问热点 |
实操建议:先跑 --bigkeys 快速摸清「大」的问题;热 Key 优先依靠应用侧埋点或 LFU 采样,避免在生产环境跑 MONITOR。
四、结合项目场景的排查清单
| 可疑现象 | 优先排查 |
|---|---|
| 某段时间整机延迟飙升、其他命令排队 | 是否在 DEL / 遍历大 Key |
| 网卡流量异常、传输缓慢 | 是否在读写大 String / 大集合 |
| 集群单节点负载远高于其他节点 | 是否有热 Key 集中在某个分片 |
| 内存缓慢上涨不回落 | 是否有集合 key 只增不删、无限膨胀 |
| 主从同步、RDB 备份变慢 | 是否存在超大 key 导致整体拖慢 |
Q42:如何治理和优化大 Key、热 Key?
答:
治理大 Key、热 Key 的核心思想只有四句话:能拆就拆,能删就删,删要渐进,热要分散。下面分别展开。
一、大 Key 的治理
1. 拆分(能拆就拆)
把一个超大结构拆成多个小结构,是治本手段。
- 大 String:不要把一个对象的完整 JSON 或长文本整体存进一个 key。拆成多个字段,用 Hash 或 RedisJSON 存,只按需取更新的部分。
- 大 Hash:按维度分片,例如
user:1001:tags:0、user:1001:tags:1,或按业务字段拆成多个独立 Hash。 - 大 List:按时间或按业务分队列,例如
task:queue:2026-08-27,而不是所有任务堆进一个task:queue。 - 大 ZSet:按时段、按业务分排名,或只保留 top N(配合
ZREMRANGEBYRANK定期裁剪)。
text
拆之前 拆之后
user:1001:profile (String, 2MB) user:1001:basic (Hash, 几 KB)
user:1001:tags (Hash, 几 KB)
user:1001:history (Hash, 几 KB)
task:queue (List, 100 万条) task:queue:00 (List, 少量)
task:queue:01 (List, 少量)
task:queue:02 (List, 少量)2. 删除要「渐进」(删要渐进,删除要用 UNLINK)
直接 DEL 一个大 key 会阻塞主线程。正确的做法有两个:
用 UNLINK 替代 DEL:UNLINK 是异步回收,命令立即返回,真正的内存释放在后台线程完成,不阻塞主线程。
bash
redis-cli UNLINK user:1001:history分批删除集合:对于超大 Hash/Set/ZSet/List,用 HSCAN/SSCAN/ZSCAN + 分批 HDEL/SREM/ZREM,或 LPOP 循环,每次只删一小批,中间 sleep 让出主线程。
python
# 分批删除大 Hash 的示例思路
cursor = 0
while True:
cursor, fields = redis_client.hscan("user:1001:history", cursor, count=100)
if fields:
redis_client.hdel("user:1001:history", *fields.keys())
if cursor == 0:
break对 List,可以直接用 LTRIM 只保留最新的 N 条来「裁剪」:
bash
redis-cli LTRIM task:queue 0 9999 # 只保留最新 1 万条3. 从源头避免(能拆就拆的延伸)
- 给集合设置上限或定期裁剪(如
LTRIM、ZREMRANGEBYRANK); - 给缓存设置合理的 TTL,避免「只写不删」无限膨胀;
- 设计 key 时就评估单个 key 的规模上限。
二、热 Key 的治理
热 Key 的本质是「访问量过于集中」,所以手段都是「分散」和「减压」。
1. 本地缓存(多级缓存)
在应用进程内加本地缓存,把热点数据挡在 Redis 之前,减少对 Redis 的访问压力:
text
请求 → 本地缓存 → Redis → 数据库
命中即返回 未命中再查热点数据(首页、热搜、秒杀商品)非常适合放入本地缓存,能显著降低 Redis 单 key 的压力,牺牲的是一致性(本地缓存刷新有延迟)。
2. 读写分离 / 副本扩容
对读多写少的热 Key,通过加只读副本(replica)分担读压力,读请求打到副本而不是主节点。
3. 二级 key / 打散(分散热点)
把「一个热 key」拆成「多个冷 key」,例如 hot:goods:1001:0、hot:goods:1001:1,读的时候随机或 hash 到其中一个,把单一 key 的压力分摊到多个 key,进一步配合 Cluster 把流量分散到多个分片。
4. 限流与降级
在应用侧对热点接口做限流、缓存预热、兜底降级,避免瞬时流量把热点 key 打穿。
三、治理手段速查表
| 对象 | 治理手段 | 命令 / 示例 | 要点 |
|---|---|---|---|
| 大 String | 拆分字段 | 改为 Hash / RedisJSON | 按需取、按需更新,避免整体读写 |
| 大 Hash/Set/ZSet | 分片 | user:1001:tags:0 / rank:video:hot:0 | 分片 key 控制规模 |
| 大 List | 分队列 + 裁剪 | LTRIM task:queue 0 9999 | 控制长度,避免无限堆积 |
| 大 key 删除 | 异步删除 | UNLINK key | 不阻塞主线程 |
| 大集合删除 | 分批删除 | HSCAN / SSCAN + 分批 HDEL / SREM | 每次少量 + sleep 让出主线程 |
| 大 ZSet | 保留 top N | ZREMRANGEBYRANK rank:video:hot 0 -1001 | 只留榜单头部 |
| 热 Key | 本地缓存 | 应用内本地缓存热点数据 | 减少 Redis 访问 |
| 热 Key | 读写分离 / 副本 | 读请求走 replica | 副本分担读压力 |
| 热 Key | 打散为二级 key | hot:goods:1001:0 ~ hot:goods:1001:N | 注意力/压力分散到多 key |
| 热 Key | 限流 / 降级 / 预热 | 应用层限流兜底 | 保护 Redis 和数据库 |
小结
- 大 Key 是「单个 key 太大」,主要危害是阻塞主线程、拖慢网络、造成内存碎片和迁移困难;治理靠拆分、异步删除、分批删除、源头设限。
- 热 Key 是「单个 key 访问太频繁」,主要危害是负载倾斜、单分片被打满;治理靠本地缓存、副本、打散、限流降级。
- 两者都要先发现、再治理:大 Key 用
--bigkeys、MEMORY USAGE、STRLEN/HLEN/LLEN等扫描;热 Key 用--hotkeys、LFU 采样、应用侧埋点等手段观测。 - 核心口诀:能拆就拆,能删就删,删要渐进(UNLINK / 分批),热要分散(本地缓存 / 副本 / 打散)。