← 返回题目列表

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

高频 中等 第 6 / 25 题 更新于 2026/07/28
缓存缓存穿透布隆过滤器空值缓存

简化版

缓存穿透指查询一个数据库里根本不存在的数据:缓存没有(因为没这数据)、数据库也没有,于是每次请求都穿过缓存直接打到数据库。恶意攻击者用大量不存在的 id 疯狂请求,能把数据库压垮。两个主流解法:① 缓存空值——查不到也把「空结果」缓存起来(设较短过期),下次直接命中返回空;② 布隆过滤器——把所有存在的 key 预先放进布隆过滤器,请求先过滤,判定「一定不存在」的直接拦掉,不查库。

详细版

问题本质:请求的数据「压根不存在」,缓存永远不会命中(没东西可缓存),每次都落到数据库。

解法一:缓存空值(null)

Object getData(String key) {
    Object value = redis.get(key);
    if (value != null) {
        return value == EMPTY ? null : value;   // 命中(含空值标记)
    }
    value = db.query(key);
    if (value == null) {
        redis.set(key, EMPTY, 60);      // 不存在也缓存,过期时间短(如 60s)
        return null;
    }
    redis.set(key, value, 3600);
    return value;
}

解法二:布隆过滤器(Bloom Filter)

// 初始化:把所有存在的 key 加入布隆过滤器
bloomFilter.add(allExistingKeys);

Object getData(String key) {
    if (!bloomFilter.mightContain(key)) {
        return null;                    // 布隆判定"一定不存在",直接拦截
    }
    // 可能存在,再走正常缓存 + DB 流程
    ...
}
  • 缓存空值:实现简单,但会占用缓存空间(大量不同的不存在 key);过期时间要短。
  • 布隆过滤器:空间效率极高,能拦截海量不存在请求;缺点是有小概率误判(判存在实际不存在)、且删除元素麻烦。
  • 两者可结合:布隆挡大部分 + 空值缓存兜底。

完整版教学

一、什么是穿透:数据「不存在」是关键

先分清三个易混概念——穿透、击穿、雪崩

  • 穿透:查的数据根本不存在,缓存和 DB 都没有,缓存形同虚设。
  • 击穿:查的数据存在,但缓存里的这个热点 key 刚好过期失效,大量请求瞬间压向 DB。
  • 雪崩大量 key 同时失效或缓存整个宕机,DB 被海量请求压垮。

穿透的独特点在于「数据不存在」——正常缓存逻辑是「查不到就回源 DB,DB 有就填回缓存」,但不存在的数据,DB 也返回空,没东西可填缓存,于是缓存永远挡不住这类请求。攻击者正是利用这点:构造大量随机的、不存在的 id(如 id=-1id=999999999)疯狂请求,全部直穿到 DB,把数据库打挂。

二、解法一:缓存空值

最简单的思路:既然 DB 返回「不存在」,那就把这个「不存在」也缓存起来。 下次同样的 key 再来,缓存直接命中一个「空值标记」,返回空,不再查 DB。

关键细节:

  • 要用特殊标记区分「空值」和「未缓存」:不能只存 null(无法区分「缓存了空」还是「没缓存」),要存一个约定的占位符(如空字符串、特殊对象 EMPTY)。
  • 过期时间要短(如 30~60 秒):因为这数据以后可能被真正创建出来,短过期能让缓存尽快感知到「它现在存在了」;也避免大量不存在 key 长期占内存。

缺点:如果攻击者用海量不同的不存在 key,会往缓存里塞进大量空值,占用内存。所以空值缓存适合「不存在 key 相对有限」的场景,海量随机 key 攻击要靠布隆过滤器。

三、解法二:布隆过滤器

布隆过滤器(Bloom Filter) 是一个空间效率极高的概率型数据结构,能判断「一个元素是否可能在集合中」。它的特性是:

  • 判定「不存在」→ 一定不存在(不会漏)。
  • 判定「存在」→ 可能存在(有小概率误判),也可能真存在

用法:系统启动时把所有真实存在的 key 都加入布隆过滤器。请求进来先问布隆——如果布隆说「不存在」,那这 key 绝对不在库里,直接返回空、拦截掉,根本不查 DB;如果布隆说「可能存在」,再走正常的缓存 + DB 流程。

因为攻击的都是不存在的 key,布隆能拦掉绝大部分,DB 压力骤降。

四、布隆过滤器的原理(为什么省空间、为什么会误判)

布隆过滤器本质是一个很长的二进制位数组 + 多个哈希函数

  • 添加元素:用 k 个哈希函数算出 k 个位置,把这些位都置 1。
  • 查询元素:同样算 k 个位置,如果有任意一位是 0→元素一定不存在(因为存在的话这些位早被置 1 了);如果k 位全是 1→元素可能存在(但也可能是别的元素把这些位碰巧都置了 1,这就是误判/假阳性)。

所以:

  • 不会假阴性(说不存在就真不存在)——这保证了拦截的正确性,不会误拦真实数据。
  • 有假阳性(说存在可能不存在)——被误判为「可能存在」的少量请求会漏过去查 DB,但比例可控(可通过增大位数组、调哈希函数个数把误判率压到很低)。
  • 删除困难:直接把位清 0 会影响其他元素(多个元素可能共用位),所以标准布隆不支持删除,要用**计数布隆过滤器(Counting Bloom Filter)**变体。

五、两种方案怎么选、能否结合

缓存空值布隆过滤器
实现难度简单较复杂(需维护过滤器)
内存占用不存在 key 多时占用大极省空间
精确性精确有小概率误判
适用不存在 key 有限海量随机 key 攻击

实践中常两者结合:布隆过滤器挡在最前面拦掉绝大多数海量攻击,漏过去的少量再用空值缓存兜底。此外还要配合接口层参数校验(如 id 必须为正数、格式合法)和限流/黑名单,多层防护。

六、常见误区与追问

考点正确口径
缓存穿透查询不存在数据,缓存和数据库都没有命中
风险恶意或异常请求绕过缓存持续打库
治理空值缓存、布隆过滤器、参数校验、限流
request id = -1 or random UUID
cache miss
database miss
repeat 100000 times -> database pressure

bloom filter says "definitely not exists" -> reject before DB

穿透要先问“这个 key 对应的数据是否本来就不存在”,不要和热点过期混淆。

  • 误区:缓存穿透就是缓存过期。 穿透通常是查不存在的数据,缓存和数据库都没有;过期热点是击穿。
  • 误区:空值缓存没有副作用。 空值也会占空间,还要设置较短 TTL,避免新数据创建后仍被空值挡住。
  • 误区:布隆过滤器能告诉你一定存在。 布隆过滤器只能判断一定不存在或可能存在,存在误判率。
  • 追问:布隆过滤器为什么省数据库? 它在访问缓存/数据库前过滤明显不存在的 key,把无效请求拦住。
  • 追问:非法参数校验有什么价值? 很多穿透请求来自负数 ID、超长字符串、格式错误 UUID,入口校验能直接拦截。
  • 追问:布隆过滤器误判怎么办? 误判只会让少量不存在请求继续查库,可结合空值缓存和限流兜底。

七、加强记忆

缓存穿透 = 查根本不存在的数据,缓存和 DB 都没有,请求全部直穿到 DB(常被攻击者用海量不存在 id 打库)。两大解法:① 缓存空值——DB 返回空也缓存一个空标记(短过期、用特殊占位符区分),简单但海量随机 key 会占内存;② 布隆过滤器——预存所有真实 key,请求先过滤,「判定不存在→一定不存在」直接拦截(原理是位数组 + 多哈希,无假阴性、有小概率假阳性、删除难)。实践常布隆挡前 + 空值兜底 + 参数校验/限流。区分记忆:穿透查的是「不存在」,击穿是「热点 key 刚失效」,雪崩是「大量 key 同时失效」