Redis突然变慢,排查发现是BigKey惹的祸
线上Redis响应时间从平均1ms飙到了50ms,业务接口全都变慢了。
查了半天,最后发现是一个BigKey导致的。记录一下排查过程。
问题现象
监控数据:
- Redis平均响应时间:1ms → 50ms
- 业务接口P99延迟:50ms → 500ms
- Redis CPU:20% → 80%
- 内存使用:正常
特点:
- 突然变慢,不是逐渐变慢
- 所有命令都变慢,不只是特定命令
- 重启后好一段时间,然后又变慢
排查过程
Step 1:查看慢查询日志
redis-cli
# 查看慢查询日志
SLOWLOG GET 20
输出:
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 OBJECT
redis-cli --scan --pattern '*' | while read key; do
size=$(redis-cli DEBUG OBJECT "$key" 2>/dev/null | grep -oP 'serializedlength:\K\d+')
if [ "$size" -gt 1048576 ]; then # 大于1MB
echo "$key: $size bytes"
fi
done
扫描结果发现了多个BigKey:
user:session:12345: 15728640 bytes (15MB)
cache:product:list: 8388608 bytes (8MB)
temp:import:batch: 5242880 bytes (5MB)
Step 4:分析业务逻辑
查代码发现问题:
// 问题代码:把整个session存成一个大Hash
@Override
public void saveSession(String sessionId, Map<String, Object> data) {
String key = "user:session:" + sessionId;
// 每次访问都往里加数据,从来不清理
redisTemplate.opsForHash().putAll(key, data);
}
// 获取时用HGETALL
public Map<String, Object> getSession(String sessionId) {
String key = "user:session:" + sessionId;
return redisTemplate.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:
// 优化前:一个大Hash
user: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存储序列化后的数据,设置过期时间
public void saveSession(String sessionId, SessionData data) {
String key = "user:session:" + sessionId;
String json = JSON.toJSONString(data);
redisTemplate.opsForValue().set(key, json, 30, TimeUnit.MINUTES);
}
方案三:避免HGETALL
// 优化前:获取整个Hash
Map<String, Object> all = redisTemplate.opsForHash().entries(key);
// 优化后:只获取需要的字段
Object value = 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:12345 0 COUNT 1000
HDEL 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 GET 20
# 扫描BigKey
redis-cli --bigkeys
# 查看Key类型
TYPE <key>
# 查看Hash字段数
HLEN <key>
# 查看List长度
LLEN <key>
# 查看Set成员数
SCARD <key>
# 查看内存占用(需要开启)
MEMORY USAGE <key>
# 查看Key详情
DEBUG OBJECT <key>
# 渐进式扫描
HSCAN <key> 0 COUNT 100
# 异步删除
UNLINK <key>
预防措施
1. 设计阶段
✅ 预估数据量,避免无限增长
✅ 设置合理的过期时间
✅ 考虑数据拆分策略
2. 开发阶段
✅ 避免使用HGETALL、SMEMBERS等全量命令
✅ 大数据量使用SCAN系列命令
✅ 删除大Key使用UNLINK
3. 运维阶段
✅ 定期扫描BigKey
✅ 监控慢查询
✅ 设置maxmemory-policy
经验总结
| 现象 | 可能原因 |
|---|---|
| 所有命令都变慢 | BigKey阻塞 |
| 特定命令变慢 | 该命令操作了BigKey |
| 内存突然增长 | 写入了BigKey |
| 主从同步延迟 | BigKey传输 |
这次的坑:Session数据只写不删,时间一长变成了18万字段的BigKey。
教训:
- Redis的Key一定要设置过期时间
- 避免使用HGETALL等全量命令
- 定期扫描BigKey,加入监控
有问题评论区交流~

浙公网安备 33010602011771号