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
}

问题

  1. Session数据一直往Hash里加,不删除
  2. 时间一长,Hash就变成了BigKey
  3. 每次获取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

最终解决

  1. 临时处理:用UNLINK异步删除那几个BigKey
  2. 代码修复:Session改用String存储,设置30分钟过期
  3. 添加监控:定期扫描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。

教训

  1. Redis的Key一定要设置过期时间
  2. 避免使用HGETALL等全量命令
  3. 定期扫描BigKey,加入监控

有问题评论区交流~


posted @ 2025-12-12 13:46  花宝宝  阅读(0)  评论(0)    收藏  举报