← 返回题目列表

什么是缓存击穿?如何解决?

高频 中等 第 8 / 25 题 更新于 2026/07/28
缓存缓存击穿互斥锁逻辑过期

简化版

缓存击穿指某个热点 key(大量请求访问的存在的数据)在缓存中突然过期失效的瞬间,大量并发请求同时未命中缓存、一起压向数据库,可能把 DB 打垮。它和穿透的区别是:数据是存在的,只是 key 刚好过期了。两个主流解法:① 互斥锁——只让一个线程去查 DB 重建缓存,其他线程等待或重试;② 逻辑过期——缓存永不物理过期,用字段标记逻辑过期时间,过期后异步重建、期间返回旧值。

详细版

问题本质:一个被高并发访问的热点 key 过期的那一瞬间,成百上千请求同时穿过缓存打到 DB。

解法一:互斥锁(保证只有一个请求重建缓存)

Object getData(String key) {
    Object value = redis.get(key);
    if (value != null) return value;            // 命中
    // 未命中,尝试获取锁(只有一个线程能拿到)
    if (redis.setnx(lockKey, 1, 10)) {          // 分布式锁
        try {
            value = db.query(key);              // 只有拿到锁的线程查 DB
            redis.set(key, value, 3600);        // 重建缓存
        } finally {
            redis.del(lockKey);                 // 释放锁
        }
    } else {
        Thread.sleep(50);                       // 没拿到锁,短暂等待后重试
        return getData(key);
    }
    return value;
}

解法二:逻辑过期(缓存不设 TTL,靠字段判断)

// 缓存的 value 里带一个 expireTime 字段,key 本身永不过期
Object getData(String key) {
    CacheData data = redis.get(key);
    if (data == null) return null;              // 理论上热点 key 一直在
    if (data.expireTime > now()) return data.value;   // 逻辑未过期,直接返回
    // 逻辑已过期:拿到锁的线程开异步任务重建,当前直接返回旧值
    if (redis.setnx(lockKey, 1, 10)) {
        asyncRebuild(key);                      // 异步重建
    }
    return data.value;                          // 无论是否重建,都先返回旧值
}
  • 互斥锁:保证只有一个请求打 DB,其他等待。强一致(返回的是最新值),但等待期间有阻塞。
  • 逻辑过期:永不阻塞、可用性高,但过期后一段时间返回的是旧数据(最终一致)。
  • 另可给热点 key 设永不过期(后台定时刷新)。

完整版教学

一、击穿 vs 穿透 vs 雪崩:先分清

  • 击穿(本题)单个热点 key 存在,但刚好过期失效的瞬间,海量请求压向 DB。特点是「一个热点 key、失效瞬间」。
  • 穿透:查的数据根本不存在,缓存永远挡不住。
  • 雪崩大量 key 同时失效或缓存整体宕机,DB 被压垮。特点是「大批 key、同时失效」。

击穿的关键词是**「热点 + 失效瞬间」**——数据本身是存在且被高频访问的,问题出在它过期的一刹那,缓存出现了短暂空窗,所有请求一拥而上。比如一个爆款商品详情,QPS 上万,缓存过期的那一秒,上万请求全部落到 DB。

二、解法一:互斥锁——只放一个进去查 DB

核心思路:既然问题是「过期瞬间大量请求同时查 DB」,那就限制只让一个请求去查 DB 重建缓存,其余请求等它建好。

实现:请求未命中缓存后,先去抢一把互斥锁(单机用 synchronized/Lock,分布式用 Redis 的 SETNX 分布式锁):

  • 抢到锁的线程:去查 DB、重建缓存、然后释放锁。
  • 没抢到锁的线程:说明已有线程在重建,短暂等待(sleep 几十毫秒)后重试读缓存——此时缓存大概率已被重建好,直接命中。

这样无论多少并发,同一时刻只有一个请求真正打到 DB,其他都被挡在锁外。

优点:返回的是最新数据,强一致。 缺点:没抢到锁的线程要等待/重试,有一定阻塞和延迟;锁本身要处理好超时释放(防止持锁线程宕机导致死锁)、误删(用唯一 value 标识 + Lua 删除)等问题。

三、解法二:逻辑过期——永不真过期,异步重建

另一种思路彻底避免「缓存空窗」:让热点 key 在 Redis 里永不设置物理 TTL(永不真正过期消失),而是在 value 内部存一个 expireTime 逻辑过期时间字段。

读的时候:

  • 缓存里一直有数据(不会 miss);
  • 检查 value 里的 expireTime没到逻辑过期时间 → 直接返回已过逻辑过期时间 → 说明该刷新了,此时抢锁,抢到的线程开一个异步任务去重建缓存,而当前请求(以及重建期间的其他请求)直接返回旧的 value

这样任何请求都不会阻塞、不会打到 DB(重建是异步的),可用性极高。代价是逻辑过期后、异步重建完成前,返回的是旧数据(牺牲了一致性,只保证最终一致)。

四、两种方案的本质权衡:一致性 vs 可用性

互斥锁逻辑过期
一致性强(返回最新值)弱(过期后返回旧值)
可用性有等待/阻塞无阻塞,始终有值返回
复杂度中(锁的管理)高(额外字段 + 异步重建)
DB 压力低(只一个请求)极低(异步、非阻塞)
适用要求数据准确要求高可用、能容忍旧值

这正是 CAP 权衡在缓存层的体现:互斥锁偏一致性(CP 味),逻辑过期偏可用性(AP 味)。选哪个看业务——金额、库存这类要准的用互斥锁;商品详情、排行榜这类能容忍短暂旧值的用逻辑过期。

五、更简单的兜底:热点 key 永不过期 + 定时刷新

对于已知的、固定的热点数据(如首页、热门榜单),可以干脆不设过期时间,由一个后台定时任务主动刷新缓存。这样根本不存在「过期瞬间」,也就没有击穿。适合热点可预测的场景;对热点不可预测(临时爆红)的场景,还得靠互斥锁/逻辑过期动态应对。

六、常见误区与追问

考点正确口径
缓存击穿单个热点 key 过期,瞬时并发打到数据库
典型场景爆款商品、明星直播间、热点配置
核心手段互斥锁、逻辑过期、热点永不过期、异步刷新
hot key expires
10000 requests miss cache
only 1 request rebuilds cache with mutex
others wait, retry, or return stale value

击穿的关键词是“热点 key + 过期瞬间 + 高并发回源”。

  • 误区:缓存击穿和缓存穿透一样。 击穿访问的是存在的热点数据,只是缓存失效;穿透访问的是不存在或非法数据。
  • 误区:给热点 key 加互斥锁就没有代价。 锁会增加等待和超时风险,要设置过期、降级和失败兜底。
  • 误区:热点 key 永不过期就完美。 永不过期要配合异步刷新和版本控制,否则可能长期返回旧数据。
  • 追问:逻辑过期怎么工作? 缓存值里存过期时间,过期后先返回旧值,同时后台异步重建。
  • 追问:互斥锁应该锁什么粒度? 通常按 key 加锁,避免不同 key 相互阻塞。
  • 追问:热点 key 如何发现? 可通过 Redis hotkeys、访问日志、业务埋点、代理层统计和突增流量监控发现。

七、加强记忆

缓存击穿 = 单个热点 key(数据存在)在缓存过期的瞬间,海量并发一起打到 DB。区别于穿透(数据不存在)、雪崩(大量 key 同时失效)。两大解法:① 互斥锁——只让抢到锁的一个线程查 DB 重建,其余等待重试,强一致但有阻塞② 逻辑过期——key 永不物理过期、value 里存逻辑过期时间,过期后异步重建、期间返回旧值,高可用但返回旧数据(最终一致)。本质是一致性 vs 可用性的权衡。对可预测的热点还可永不过期 + 定时刷新。记住击穿关键词:热点 key、失效瞬间、只放一个进去