Redis 常见线上问题
问题
生产环境 Redis 突然变慢、大 Key 阻塞、热 Key 打爆单节点、缓存击穿,怎么处理?
答案
问题速查
| 问题 | 表现 | 紧急程度 |
|---|---|---|
| 大 Key | 操作/删除时阻塞 | 高 |
| 热 Key | 单节点 CPU 打满 | 高 |
| 缓存击穿 | DB 瞬间流量飙升 | 高 |
| 内存暴涨 | OOM / 淘汰频繁 | 中 |
| 集群倾斜 | 某节点内存/QPS 过高 | 中 |
大 Key 问题
大 Key 定义:String > 10KB,Hash/List/Set > 5000 个元素。
发现大 Key
# 离线扫描(推荐)
redis-cli --bigkeys
# 指定阈值扫描
redis-cli --bigkeys --i 0.1 # 每 100ms 扫一个 key
# RDB 分析(不影响线上)
rdb --command memory dump.rdb --bytes 10240 --largest 100
大 Key 危害:
- 操作延迟高(
HGETALL一个百万字段的 Hash 可能耗时秒级) - 删除时阻塞(
DEL一个大 Key 时 Redis 单线程卡住) - 集群迁移困难
大 Key 处理方案
// ❌ 直接删除大 Key(会阻塞)
redisTemplate.delete("big:key");
// ✅ 异步删除(Redis 4.0+,后台线程处理)
redisTemplate.unlink("big:key");
// ✅ 分批删除 Hash
String cursor = "0";
do {
ScanResult<Map.Entry<String, String>> result =
jedis.hscan("big:hash", cursor, new ScanParams().count(100));
for (Map.Entry<String, String> entry : result.getResult()) {
jedis.hdel("big:hash", entry.getKey());
}
cursor = result.getCursor();
} while (!"0".equals(cursor));
热 Key 问题
热 Key:某个 key 的 QPS 特别高,集中在一个 Redis 节点上。
热 Key 解决方案
// 方案 1:本地缓存(Caffeine)+ Redis 双层缓存
Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofSeconds(5)) // 短过期
.build();
public Object get(String key) {
// 先查本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) return value;
// 本地没有再查 Redis
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
}
return value;
}
// 方案 2:key 打散到多个节点
// 给 key 加后缀,分散到不同 slot
public Object getWithShard(String key) {
int shard = ThreadLocalRandom.current().nextInt(10);
String shardKey = key + ":" + shard;
Object value = redisTemplate.opsForValue().get(shardKey);
if (value == null) {
value = loadFromDB(key);
// 写入所有分片
for (int i = 0; i < 10; i++) {
redisTemplate.opsForValue().set(key + ":" + i, value, 10, TimeUnit.MINUTES);
}
}
return value;
}
缓存击穿实战
缓存击穿:热 Key 过期瞬间,大量请求穿透到 DB。
分布式锁 + 缓存重建
public Object getWithLock(String key) {
Object value = redisTemplate.opsForValue().get(key);
if (value != null) return value;
// 缓存未命中,加分布式锁防止穿透
String lockKey = "lock:" + key;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (locked) {
try {
// 双重检查
value = redisTemplate.opsForValue().get(key);
if (value != null) return value;
// 查 DB 并重建缓存
value = loadFromDB(key);
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 没拿到锁,等待后重试
Thread.sleep(50);
return getWithLock(key);
}
return value;
}
集群数据倾斜
查看集群各节点内存
redis-cli --cluster info <host>:<port>
# 查看 slot 分布
redis-cli cluster slots
| 原因 | 解决 |
|---|---|
| 大 Key 集中在某 slot | 拆分大 Key |
| Hash Tag 不当 | 避免 {user} 让大量 key 落在同一 slot |
| 热 Key | 本地缓存 + 分片 |
常见面试问题
Q1: Redis 突然变慢,排查思路?
答案:
slowlog get 10:查慢命令(KEYS、HGETALL 大 Key)info memory:内存是否打满,是否在频繁淘汰info clients:连接数是否过多info stats:看keyspace_misses缓存命中率- 检查是否有大 Key 在做 DEL、过期
- 检查是否开启了 RDB/AOF 重写在 fork 子进程
详见 Redis 性能优化。
Q2: 缓存穿透/击穿/雪崩的区别?
答案:
| 缓存穿透 | 缓存击穿 | 缓存雪崩 | |
|---|---|---|---|
| 定义 | 查不存在的数据 | 热 Key 过期 | 大量 Key 同时过期 |
| 原因 | 异常请求 | 过期策略 | 过期时间一致 |
| 方案 | 布隆过滤器 / 空值缓存 | 互斥锁 / 永不过期 | 过期时间加随机值 |
详见 Redis 缓存问题。
Q3: 大 Key 怎么预防?
答案:
- 设计阶段:控制集合类型元素数量,String 控制大小
- 监控阶段:定时扫描大 Key + 告警
- 使用阶段:避免
HGETALL/SMEMBERS,用HSCAN/SSCAN分批获取