2. 缓存穿透原理:缓存穿透的成因分析、空值缓存与布隆过滤器原理、穿透对数据库的压力模型

2.1 缓存穿透的成因分析

缓存穿透是指查询一个根本不存在的数据,导致请求绕过缓存层,直接穿透到后端数据库(DB)的现象。由于缓存层对该 key 没有命中(不存在),每次请求都会直接访问数据库,而数据库也查不到该记录,因此不会回写缓存。这种“空查询”在高并发场景下会形成对数据库的直接冲击。

核心成因:缓存层与数据库层均无法命中目标数据,且缓存层未对“不存在”的 key 做特殊处理,导致每次请求都穿透到 DB。

常见触发场景包括:

  • 恶意攻击或爬虫:攻击者构造大量不存在的 key(如随机 ID、负数 ID、超长字符串)发起请求。
  • 业务逻辑漏洞:前端未做参数校验,允许传入非法或无效的查询参数。
  • 缓存与数据库数据不一致:数据已被删除,但缓存未及时失效,导致后续请求查不到缓存后直接查 DB。
  • 缓存预热不完整:新上线功能或数据迁移后,部分合法 key 未被加载到缓存中。
穿透类型 触发原因 典型特征 危害等级
恶意穿透 攻击者构造随机/非法 key QPS 突增,key 无规律 高
误删穿透 数据已删除但缓存未清 少量 key 持续穿透 中
预热穿透 新数据未加载到缓存 上线初期出现 低
参数穿透 前端未校验非法参数 特定参数格式异常 中

2.2 空值缓存原理

空值缓存(Null Cache)是最直接的防御手段。当数据库查询结果为空时,仍然将该 key 写入缓存,但 value 标记为空值占位符(如 null、""、"NULL"),并设置一个较短的过期时间(通常 30~60 秒)。后续相同 key 的请求会直接命中缓存中的空值,从而避免穿透到 DB。

实践要点:空值缓存的 TTL 不宜过长,否则可能导致合法数据写入后,客户端仍读到空值(数据一致性问题)。建议配合缓存更新机制(如主动失效)使用。
// 伪代码示例:空值缓存逻辑
public Object getWithNullCache(String key) {
    // 1. 查缓存
    Object cacheValue = cache.get(key);
    if (cacheValue != null) {
        // 命中缓存(包括空值占位符)
        if (cacheValue instanceof NullPlaceholder) {
            return null; // 返回空,但不再查 DB
        }
        return cacheValue;
    }
    
    // 2. 缓存未命中,查数据库
    Object dbValue = db.query(key);
    if (dbValue == null) {
        // 3. 数据库也不存在,写入空值缓存(TTL=30s)
        cache.set(key, new NullPlaceholder(), 30);
        return null;
    }
    
    // 4. 数据库存在,写入正常缓存(TTL=3600s)
    cache.set(key, dbValue, 3600);
    return dbValue;
}

空值缓存的优缺点对比:

维度 优势 劣势
实现复杂度 极低,仅需修改缓存写入逻辑 需区分空值占位符与正常值
内存占用 仅存储少量空 key 恶意攻击可能产生大量空 key,占用缓存空间
数据一致性 短 TTL 可快速恢复 存在短暂不一致窗口
防御效果 对少量穿透有效 大规模随机 key 攻击时效果有限

2.3 布隆过滤器原理

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它由一个很长的二进制位数组和多个哈希函数组成。其核心特性是:

  • 判断不存在:100% 准确。如果布隆过滤器说某个 key 不存在,那它一定不存在。
  • 判断存在:存在误判率(False Positive)。可能误判某个不存在的 key 为存在。

在缓存穿透治理中,布隆过滤器作为前置过滤器:所有查询请求先经过布隆过滤器,如果判定 key 不存在,则直接拒绝(返回空),不再查询缓存和 DB;如果判定存在,才允许进入缓存层。

注意:布隆过滤器无法删除元素(标准实现)。当数据被删除时,需要重建过滤器或使用计数布隆过滤器(Counting Bloom Filter)变体。
// 伪代码示例:布隆过滤器 + 缓存穿透防御
public class BloomFilterGuard {
    private BloomFilter<String> bloomFilter; // 预加载所有合法 key
    private Cache cache;
    private Database db;
    
    public Object get(String key) {
        // 1. 布隆过滤器前置判断
        if (!bloomFilter.mightContain(key)) {
            // 一定不存在,直接返回
            return null;
        }
        
        // 2. 查缓存
        Object cacheValue = cache.get(key);
        if (cacheValue != null) {
            return cacheValue;
        }
        
        // 3. 查数据库(此时 key 大概率存在)
        Object dbValue = db.query(key);
        if (dbValue != null) {
            cache.set(key, dbValue, 3600);
        }
        return dbValue;
    }
}

布隆过滤器的关键参数:

参数 含义 推荐值
n 预期元素数量 根据业务数据量估算
p 误判率(False Positive Rate) 1% ~ 5%
m 位数组长度(bit) 由 n 和 p 计算得出
k 哈希函数个数 由 n 和 m 计算得出
布隆过滤器 + 空值缓存双重防御流程 客户端请求 布隆过滤器 mightContain(key) ? 不存在 直接返回空 可能存在 查询缓存 命中 返回缓存值 未命中 查询数据库 结果回写 回写缓存(含空值)

2.4 穿透对数据库的压力模型

缓存穿透对数据库的压力可以用一个量化模型来描述。假设系统总 QPS 为 Q,缓存命中率为 H,穿透请求占比为 P(即请求中不存在的 key 比例),则数据库实际承受的查询压力为:

DB_QPS = Q × (1 - H) × P

其中:
- Q:系统总请求量(次/秒)
- H:缓存命中率(0~1)
- P:穿透请求占比(0~1)

当 P 接近 1(恶意攻击场景)且 H 较低时,DB_QPS 会接近甚至等于 Q,即所有请求全部打到数据库。以一个典型电商场景为例:

场景 Q (QPS) H (命中率) P (穿透占比) DB_QPS 数据库状态
正常业务 10,000 0.95 0.01 50 健康
少量穿透 10,000 0.90 0.10 100 轻度压力
恶意攻击 10,000 0.50 0.80 4,000 严重过载
极端攻击 10,000 0.10 0.99 8,910 雪崩风险
关键结论:当穿透占比 P 超过 30% 且缓存命中率 H 低于 80% 时,数据库 QPS 会呈指数级增长。布隆过滤器可以将 P 降低到接近 0(误判率 p 以内),空值缓存则能进一步兜底处理误判导致的少量穿透。

压力模型的非线性特征还体现在:数据库连接池、线程池、磁盘 IO 等资源在达到阈值后会快速恶化。例如,MySQL 在 QPS 超过 5,000 时,查询延迟可能从 5ms 飙升到 500ms,触发连接超时和重试风暴,最终导致缓存雪崩(大量请求同时穿透)。

避坑提示:不要只依赖单一防御手段。空值缓存适合防御少量随机穿透,布隆过滤器适合防御大规模恶意攻击。生产环境建议组合使用:布隆过滤器做第一道防线,空值缓存做第二道兜底,同时配合限流、熔断等弹性防护手段。