线上Redis响应时间从平均1ms飙到了50ms,业务接口全都变慢了。
查了半天,最后发现是一个BigKey导致的。记录一下排查过程。
问题现象
监控数据:
- Redis平均响应时间:1ms → 50ms
- 业务接口P99延迟:50ms → 500ms
- Redis CPU:20% → 80%
- 内存使用:正常
特点:
- 突然变慢,不是逐渐变慢
- 所有命令都变慢,不只是特定命令
- 重启后好一段时间,然后又变慢
排查过程
Step 1:查看慢查询日志
redis-cli# 查看慢查询日志SLOWLOG GET20输出:
1) 1) (integer) 1001 2) (integer) 1702345678 3) (integer) 45123 # 微秒,约45ms 4) 1) "HGETALL" 2) "user:session:12345"发现大量HGETALL命令耗时几十毫秒,正常应该是亚毫秒级。
Step 2:查看这个Key的信息
# 查看Key类型TYPE user:session:12345# hash# 查看Hash的字段数量HLEN user:session:12345# 182356# 查看Key占用内存DEBUG OBJECT user:session:12345# serializedlength:15728640 约15MB问题找到了!这个Hash有18万个字段,占用15MB内存。
这就是BigKey,对它执行HGETALL要把18万个字段全部遍历,当然慢。
Step 3:查找其他BigKey
# Redis 4.0+ 可以用 --bigkeys 扫描redis-cli --bigkeys# 或者用 SCAN 配合 DEBUG OBJECTredis-cli --scan --pattern'*'|whilereadkey;dosize=$(redis-cli DEBUG OBJECT"$key"2>/dev/null|grep-oP'serializedlength:\K\d+')if["$size"-gt1048576];then# 大于1MBecho"$key:$sizebytes"fidone扫描结果发现了多个BigKey:
user:session:12345: 15728640 bytes (15MB) cache:product:list: 8388608 bytes (8MB) temp:import:batch: 5242880 bytes (5MB)Step 4:分析业务逻辑
查代码发现问题:
// 问题代码:把整个session存成一个大Hash@OverridepublicvoidsaveSession(StringsessionId,Map<String,Object>data){Stringkey="user:session:"+sessionId;// 每次访问都往里加数据,从来不清理redisTemplate.opsForHash().putAll(key,data);}// 获取时用HGETALLpublicMap<String,Object>getSession(StringsessionId){Stringkey="user:session:"+sessionId;returnredisTemplate.opsForHash().entries(key);// HGETALL}问题:
- Session数据一直往Hash里加,不删除
- 时间一长,Hash就变成了BigKey
- 每次获取Session都用
HGETALL,遍历整个Hash
BigKey的危害
1. 阻塞单线程
Redis是单线程的,操作BigKey时会阻塞其他命令:
正常Key(1KB): 1ms完成 BigKey(10MB): 50ms完成 这50ms内其他所有命令都在排队等待2. 网络带宽压力
每次HGETALL返回15MB数据 1秒请求10次 = 150MB/s 网络可能成为瓶颈3. 内存不均衡
如果是Redis集群,BigKey会导致某个节点内存远大于其他节点。
4. 删除时阻塞
DEL user:session:12345# 删除15MB的Key,可能阻塞好几秒解决方案
方案一:拆分BigKey
把大Hash拆成多个小Hash:
// 优化前:一个大Hashuser:session:12345→{field1:v1,field2:v2,...field180000:v180000}// 优化后:按照某种规则拆分user:session:12345:0→{field1:v1,...field1000:v1000}user:session:12345:1→{field1001:v1001,...field2000:v2000}...方案二:改用合适的数据结构
Session数据不需要存18万个字段,只需要保留最近访问的数据:
// 使用String存储序列化后的数据,设置过期时间publicvoidsaveSession(StringsessionId,SessionDatadata){Stringkey="user:session:"+sessionId;Stringjson=JSON.toJSONString(data);redisTemplate.opsForValue().set(key,json,30,TimeUnit.MINUTES);}方案三:避免HGETALL
// 优化前:获取整个HashMap<String,Object>all=redisTemplate.opsForHash().entries(key);// 优化后:只获取需要的字段Objectvalue=redisTemplate.opsForHash().get(key,"targetField");// 或者批量获取部分字段List<Object>values=redisTemplate.opsForHash().multiGet(key,Arrays.asList("f1","f2"));方案四:异步删除BigKey
# Redis 4.0+ 支持异步删除UNLINK user:session:12345# 异步删除,不阻塞# 或者渐进式删除Hash# 每次删1000个字段HSCAN user:session:123450COUNT1000HDEL user:session:12345 field1 field2... field1000最终解决
- 临时处理:用
UNLINK异步删除那几个BigKey - 代码修复:Session改用String存储,设置30分钟过期
- 添加监控:定期扫描BigKey,超过1MB告警
BigKey标准
| 数据类型 | BigKey阈值 | 说明 |
|---|---|---|
| String | > 10KB | 单个值太大 |
| Hash | > 5000字段 或 > 10MB | 字段太多或总大小太大 |
| List | > 5000元素 | 元素太多 |
| Set | > 5000成员 | 成员太多 |
| ZSet | > 5000成员 | 成员太多 |
排查命令汇总
# 查看慢查询SLOWLOG GET20# 扫描BigKeyredis-cli --bigkeys# 查看Key类型TYPE<key># 查看Hash字段数HLEN<key># 查看List长度LLEN<key># 查看Set成员数SCARD<key># 查看内存占用(需要开启)MEMORY USAGE<key># 查看Key详情DEBUG OBJECT<key># 渐进式扫描HSCAN<key>0COUNT100# 异步删除UNLINK<key>预防措施
1. 设计阶段
✅ 预估数据量,避免无限增长 ✅ 设置合理的过期时间 ✅ 考虑数据拆分策略2. 开发阶段
✅ 避免使用HGETALL、SMEMBERS等全量命令 ✅ 大数据量使用SCAN系列命令 ✅ 删除大Key使用UNLINK3. 运维阶段
✅ 定期扫描BigKey ✅ 监控慢查询 ✅ 设置maxmemory-policy经验总结
| 现象 | 可能原因 |
|---|---|
| 所有命令都变慢 | BigKey阻塞 |
| 特定命令变慢 | 该命令操作了BigKey |
| 内存突然增长 | 写入了BigKey |
| 主从同步延迟 | BigKey传输 |
这次的坑:Session数据只写不删,时间一长变成了18万字段的BigKey。
教训:
- Redis的Key一定要设置过期时间
- 避免使用HGETALL等全量命令
- 定期扫描BigKey,加入监控
有问题评论区交流~