Redis 常见问题
Redis 用作缓存后,系统速度会提升,但也引入了新的问题:缓存穿透、缓存击穿、缓存雪崩、数据一致性、热 Key、大 Key。学习这些问题的重点不是背名词,而是理解“缓存层失效后流量会打到哪里”。
缓存问题总览
mermaid
flowchart TD
A["用户请求"] --> B["Redis 缓存"]
B --> C{"缓存是否可用或命中"}
C -- "命中" --> D["快速返回"]
C -- "未命中或失效" --> E["请求数据库"]
E --> F{"数据库能否承受"}
F -- "能" --> G["回源并重建缓存"]
F -- "不能" --> H["接口变慢或数据库被打垮"]缓存问题的本质:
| 问题 | 本质 |
|---|---|
| 缓存穿透 | 查询根本不存在的数据,缓存挡不住 |
| 缓存击穿 | 单个热点 Key 过期,瞬间大量请求回源 |
| 缓存雪崩 | 大量 Key 同时失效或 Redis 故障 |
| 热 Key | 单个 Key 请求太集中 |
| 大 Key | 单个 Key 太大,操作和传输成本高 |
| 一致性 | Redis 和数据库可能短时间不一致 |
缓存穿透
缓存穿透指查询一个数据库里也不存在的数据。因为 Redis 没有,数据库也没有,如果不做处理,每次请求都会打到数据库。
mermaid
flowchart TD
A["请求不存在的 id"] --> B["Redis 未命中"]
B --> C["MySQL 查询也为空"]
C --> D["不写缓存"]
D --> E["下次请求继续打 MySQL"]方案一:缓存空值
数据库查不到时,也把空结果写入 Redis,但 TTL 要短。
java
if (article == null) {
redisTemplate.opsForValue().set("article:" + id, "NULL", Duration.ofMinutes(5));
return null;
}优点:简单直接。
风险:无效 Key 很多时会占用 Redis 空间,所以空值 TTL 要短,还要配合参数校验。
方案二:布隆过滤器
布隆过滤器可以快速判断某个 ID 是否“可能存在”。
mermaid
flowchart TD
A["请求 id"] --> B["布隆过滤器判断"]
B --> C{"可能存在吗"}
C -- "否" --> D["直接返回空"]
C -- "是" --> E["继续查 Redis / MySQL"]布隆过滤器可能误判“存在”,但不会误判“绝对不存在”。适合 ID 空间大、恶意请求多的场景。底层原理、误判率、初始化和 RedisBloom 示例看:布隆过滤器原理。
缓存击穿
缓存击穿指一个热点 Key 在过期瞬间,大量请求同时回源数据库。
mermaid
flowchart TD
A["热点 Key 过期"] --> B["大量请求同时未命中"]
B --> C["同时查数据库"]
C --> D["数据库压力瞬间升高"]方案一:互斥锁
只有一个线程负责查库并重建缓存,其他线程等待、重试或返回旧值。
mermaid
flowchart TD
A["缓存未命中"] --> B["尝试获取重建锁"]
B --> C{"是否拿到锁"}
C -- "是" --> D["查数据库并写缓存"]
C -- "否" --> E["短暂等待后重试缓存"]方案二:逻辑过期
缓存物理上不过期,value 中保存逻辑过期时间。逻辑过期后,先返回旧数据,再异步重建缓存。
json
{
"data": {
"id": 1,
"title": "Redis"
},
"expireAt": "2026-06-28 10:00:00"
}适合读多写少、允许短时间旧数据的场景。不适合强一致要求很高的数据。
缓存雪崩
缓存雪崩指大量 Key 同时过期,或 Redis 整体不可用,导致大量请求同时打到数据库。
mermaid
flowchart TD
A["大量 Key 设置相同 TTL"] --> B["同一时间过期"]
B --> C["请求集中回源 MySQL"]
C --> D["数据库压力暴涨"]解决方式:
- TTL 加随机值,避免同一时间过期。
- 热点数据提前预热。
- Redis 做高可用。
- 接口限流和降级。
- 多级缓存保护核心热点。
示例:
java
long ttl = 3600 + ThreadLocalRandom.current().nextLong(0, 300);
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttl));数据一致性问题
缓存和数据库之间很难做到绝对强一致。常见策略是:先更新数据库,再删除缓存。
mermaid
flowchart TD
A["更新业务数据"] --> B["更新 MySQL"]
B --> C["删除 Redis 缓存"]
C --> D["下一次查询缓存未命中"]
D --> E["查 MySQL 重建缓存"]为什么不推荐先删缓存再更新数据库:
- 删除缓存后,另一个请求可能立刻查询数据库旧值。
- 它会把旧值重新写入缓存。
- 随后数据库更新完成,但缓存里仍是旧值。
生产中常见补偿:
- 删除缓存失败后记录日志或消息。
- 异步重试删除。
- 给缓存设置合理 TTL。
- 重要业务使用 binlog 订阅做缓存修正。
Java Demo:缓存空对象和随机 TTL
java
public Article getArticle(Long id) {
String key = "article:" + id;
String cache = redisTemplate.opsForValue().get(key);
if (cache != null) {
if ("NULL".equals(cache)) {
return null;
}
return JSON.parseObject(cache, Article.class);
}
Article article = articleMapper.selectById(id);
if (article == null) {
redisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));
return null;
}
long randomSeconds = ThreadLocalRandom.current().nextLong(0, 300);
redisTemplate.opsForValue().set(
key,
JSON.toJSONString(article),
Duration.ofMinutes(30).plusSeconds(randomSeconds)
);
return article;
}这个 Demo 同时处理:
- 缓存穿透:不存在的数据缓存空值。
- 缓存雪崩:正常数据 TTL 增加随机值。
如果是热点 Key,还要继续加互斥锁或逻辑过期。
线上排查清单
| 现象 | 排查方向 |
|---|---|
| 数据库突然被打满 | 是否缓存雪崩或击穿 |
| 某个接口 Redis QPS 极高 | 是否热 Key |
| Redis 响应变慢 | 是否大 Key、慢命令、网络问题 |
| 数据偶尔旧 | 是否缓存删除失败或 TTL 太长 |
| 不存在 ID 请求很多 | 是否缓存穿透或恶意请求 |
命令:
bash
redis-cli slowlog get 20
redis-cli --bigkeys
redis-cli info memory
redis-cli info stats小结
Redis 缓存问题本质是流量保护问题:
- 穿透:挡住不存在的数据。
- 击穿:挡住热点 Key 过期瞬间的并发回源。
- 雪崩:避免大面积缓存同时失效。
- 一致性:接受短时间不一致,并设计补偿。
- 热 Key 和大 Key:保护 Redis 自身稳定。
缓存能提升性能,但它不是免费的。每加一层缓存,都要同时设计失效、回源、降级和一致性策略。
