Skip to content

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["数据库压力暴涨"]

解决方式:

  1. TTL 加随机值,避免同一时间过期。
  2. 热点数据提前预热。
  3. Redis 做高可用。
  4. 接口限流和降级。
  5. 多级缓存保护核心热点。

示例:

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 重建缓存"]

为什么不推荐先删缓存再更新数据库:

  1. 删除缓存后,另一个请求可能立刻查询数据库旧值。
  2. 它会把旧值重新写入缓存。
  3. 随后数据库更新完成,但缓存里仍是旧值。

生产中常见补偿:

  1. 删除缓存失败后记录日志或消息。
  2. 异步重试删除。
  3. 给缓存设置合理 TTL。
  4. 重要业务使用 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 同时处理:

  1. 缓存穿透:不存在的数据缓存空值。
  2. 缓存雪崩:正常数据 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 缓存问题本质是流量保护问题:

  1. 穿透:挡住不存在的数据。
  2. 击穿:挡住热点 Key 过期瞬间的并发回源。
  3. 雪崩:避免大面积缓存同时失效。
  4. 一致性:接受短时间不一致,并设计补偿。
  5. 热 Key 和大 Key:保护 Redis 自身稳定。

缓存能提升性能,但它不是免费的。每加一层缓存,都要同时设计失效、回源、降级和一致性策略。