大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
凌晨三点,收到告警:Redis延迟飙升至500ms,大量请求超时。紧急排查后发现,罪魁祸首是一个200MB的大Key。你战战兢兢地想删掉它,结果DEL命令又把Redis卡了3秒。
不敢读、不敢写、不敢删------这就是Redis大Key的"死亡循环"。
上周讲了"Redis大key怎么排查",今天讲"排查出来之后怎么治"。大Key的本质危害不是"数据量大",而是Redis是单线程的,处理大Key就像让一个人一次性扛200斤重物,不仅慢,还会堵住后面所有人的路。
大Key的优化本质 = 把单次大块内存操作拆成多次小块操作。总内存不变,但风险下降上百倍。
今天从三种大Key类型出发,拆解拆分策略和数据结构选型,一次性把大Key根治这件事讲清楚。
一、先搞清楚:大Key有三种类型
类型一:String大Value
单个String类型的Key存储的Value过大,比如存了一整个序列化对象、一个大JSON、一张图片的Base64。生产环境建议单个String不超过10KB。
类型二:集合类大Key
Hash、Set、ZSet、List中存储了过多的元素(以万为单位)。一个Hash有100万字段,一个Set有500万成员,一个List有几百万条消息。
类型三:Key数量爆炸
一个集群存储了上亿个Key,Key本身过多也带来了更多的空间占用。
核心认知:大Key真正可怕的不是占内存,而是单次操作太大。Redis是单线程模型,同一时间只能干一件事,大Key会让主线程长时间独占,其他命令全部排队等待。
二、拆分策略:不同类型怎么拆?
1. String类型
场景A:该对象需要整存整取
将一个String拆成多个Key-Value,使用MGET批量获取。分拆的意义在于将单次操作的压力分摊到多个Redis实例中,降低对单个Redis的I/O影响。
场景B:该对象只需要存取部分数据
可以像场景A一样分拆成几个Key-Value,也可以存在一个Hash中,每个Field代表一个具体属性。使用HGET、HMGET获取部分Value,使用HSET、HMSET更新部分属性。
参考阈值:String超过10MB必须拆分,建议512KB内切片。
2. Hash类型
核心思路:固定桶数量,本地计算路由
固定一个桶的数量(比如10000),每次存取时先在本地计算Field的Hash值,模除桶数量,确定该Field落在哪个Key上。
bash
# 伪代码示例
BUCKET_COUNT = 10000
def get_hash_key(base_key, field):
bucket = hash(field) % BUCKET_COUNT
return f"{base_key}:{bucket}"
原先的hget(hashKey, field)变成hget(newHashKey, field)。
参考阈值 :Hash字段数超过5000或总大小超过1MB时建议拆分。单次HGETALL操作耗时从几十ms降至亚ms级。
3. Set / ZSet / List类型
同理,将一个大集合按规则拆成多个小集合。
Set拆分:按业务维度做Sharding,将一个大Set按逻辑切分成多个小Set。
ZSet拆分:如果是为了排序场景,可以考虑改用ZSET替代。按时间或分数范围分桶。
List拆分 :需要保证LPOP的数据确实是最早Push进去的,需要在Key的拼接上做一些工作,比如按时间分拆。
三、渐进式拆分:如何在不影响业务的情况下完成拆分?
直接拆大Key风险极高------可能造成数据不一致、业务报错、甚至Redis阻塞。正确的做法是渐进式拆分。
第一步:双写阶段
新的写入同时写到旧Key和新Key。应用层代码改造,写入时同时更新新旧两套结构。
第二步:双读阶段
读取时先读新Key,如果不存在再读旧Key。确保数据"读得到"。
第三步:数据迁移阶段
后台任务逐步将旧Key中的数据迁移到新Key。使用HSCAN、SSCAN、ZSCAN分批扫描,每批处理少量数据。不要一次全量迁移。
第四步:切读阶段
确认新Key数据完整后,将读取逻辑完全切换到新Key。
第五步:下线阶段
确认新Key稳定运行一段时间后,逐步下线旧Key。使用UNLINK命令异步删除,不要用DEL。
关键原则:全程利用Redis的非阻塞命令和异步特性,最大程度降低对现有业务的影响。
四、删除大Key的正确方式
禁止使用DEL直接删除大Key ------Redis是单线程的,DEL会一次性释放大块内存,主线程被卡住,可能造成Redis阻塞甚至主备倒换。
Redis 4.0及以上版本使用UNLINK命令,异步删除,不阻塞主线程。
bash
# 正确删除方式
redis-cli UNLINK big_key_name
# 错误删除方式(会阻塞)
redis-cli DEL big_key_name
删除大Key的正确姿势:
-
先用
RENAME key tmp:key将Key重命名(让应用无法再访问到它) -
再用
UNLINK异步删除 -
或使用
EXPIRE key <seconds>设置过期时间,让Redis自动清理
五、数据结构选型:从源头杜绝大Key
预防大Key比治理大Key更重要。从数据结构选型阶段就开始防患于未然。
| 数据结构 | 适用场景 | 大Key风险 | 预防建议 |
|---|---|---|---|
| String | 简单键值、缓存对象 | 序列化后体积大 | 压缩后存储,单个<10KB |
| Hash | 对象属性存储 | 字段过多 | 按业务维度拆分,每Hash<5000字段 |
| Set | 去重集合 | 成员过多 | 按业务或时间分桶,每Set<1万成员 |
| ZSet | 排行榜、延迟队列 | 成员过多 | 按时间或分数范围分桶 |
| List | 消息队列、历史记录 | 元素过多 | 按时间分片,定期清理过期数据 |
选型原则:
-
不要把所有数据都塞进一个Key里
-
集合类数据要控制大小,必要时按用户、时间、业务类型拆分
-
读取时不要一次取完,能分页就分页,能分批就分批
-
对已经存在的大Key,要先摸清楚来源,再安排低峰期处理,避免直接在线上粗暴删除
六、总结
大Key治理不是一次性的任务,而是需要纳入日常运维体系的工作。
根治大Key的四步法:
| 步骤 | 手段 | 目标 |
|---|---|---|
| 发现 | redis-cli --bigkeys、MEMORY USAGE、慢查询、RDB分析 |
能看见 |
| 拆分 | 按业务维度、哈希取模、时间范围拆分 | 把大拆小 |
| 迁移 | 渐进式拆分、双写双读、分批迁移 | 平滑过渡 |
| 删除 | 使用UNLINK异步删除 |
安全下线 |
大Key优化的核心:把单次大块内存操作拆成多次小块操作。总内存不变,但风险下降上百倍。拆分后单次阻塞从几十毫秒降至亚毫秒级,内存操作更平滑,网络流量更平稳。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~