3. 缓存穿透解决方案-空值缓存:原理、实现与取舍

缓存穿透,说白了就是请求绕过了缓存,直接打到数据库上。你想想看,一个不存在的 key,缓存里没有,数据库里也没有,每次请求都穿透过去,数据库压力能不大吗?

我记得刚接手一个电商项目时,就遇到过这种情况。用户疯狂搜索一个不存在的商品 ID,结果数据库连接池瞬间被打满。当时我就在想,这问题必须得治。

空值缓存,就是最直接的解决方案之一。

3.1 空值缓存的原理

原理其实很简单:当数据库查询结果为空时,我们仍然把这个「空结果」缓存起来。下次同样的请求过来,缓存直接返回空,不再穿透到数据库。

来看个流程图,你就明白了:

空值缓存流程图 客户端请求 缓存层 数据库 空值缓存 1. 查询 2. 未命中 3. 结果为空 4. 直接返回空 关键点: • 空值缓存不是不缓存,而是把「空」也当作一个有效值来缓存 • 需要设置较短的过期时间,避免长期占用内存 • 适用于 key 固定、但对应数据不存在的场景

3.2 实现方式

实现起来并不复杂。我习惯用 Redis 来做缓存,直接上代码:

// 伪代码:空值缓存实现
public Object getData(String key) {
    // 1. 先从缓存查
    Object cacheValue = redis.get(key);
    if (cacheValue != null) {
        // 命中缓存,直接返回
        return cacheValue;
    }

    // 2. 缓存未命中,查数据库
    Object dbValue = database.query(key);
    
    if (dbValue == null) {
        // 3. 数据库也没有,缓存空值
        // 设置较短的过期时间,比如 30 秒
        redis.set(key, "NULL_VALUE", 30);
        return null;
    } else {
        // 4. 数据库有数据,正常缓存
        redis.set(key, dbValue, 3600);
        return dbValue;
    }
}

这里有几个细节要注意:

  • 空值标识:不要直接存 null,Redis 对 null 的处理可能有问题。我习惯用一个特殊字符串,比如 "NULL_VALUE" 或 "__EMPTY__"。
  • 过期时间:空值缓存的 TTL 要短,一般 30-60 秒。太长会浪费内存,太短又起不到保护作用。
  • 内存控制:如果空 key 太多,可以考虑用 LRU 淘汰策略,或者限制空值缓存的总数量。
小技巧: 我曾在项目中用 setex 命令一次性设置值和过期时间,避免两步操作带来的并发问题。Redis 的 SET key value EX seconds NX 也是个好选择。

3.3 优缺点分析

任何方案都有两面性。空值缓存也不例外。

维度 说明
优点
  • 实现简单,几乎零成本接入
  • 能有效拦截大部分穿透请求
  • 对现有代码侵入性小
  • 适合 key 空间有限的场景
缺点
  • 占用额外内存,空 key 多时压力大
  • 数据恢复后,空值缓存可能导致短暂不一致
  • 无法防御恶意攻击(攻击者可以构造无限新 key)
  • 需要额外的空值标识处理逻辑
核心取舍: 空值缓存适合「key 有限、偶尔穿透」的场景。如果攻击者每秒构造一万个不同的不存在的 key,空值缓存反而会撑爆 Redis 内存。这时候就需要布隆过滤器上场了。

3.4 避坑指南

我曾经在一个社交项目中踩过坑。当时用户量不大,空值缓存跑得好好的。后来做活动,瞬间涌入大量新用户,很多用户 ID 不存在,空值缓存数量暴涨,Redis 内存直接打满。

那次之后,我总结了几个要点:

  1. 一定要设置内存上限:给 Redis 配置 maxmemory,配合 allkeys-lru 淘汰策略。
  2. 空值过期时间要随机化:避免大量空 key 同时过期,导致缓存雪崩。可以在基础 TTL 上加上随机偏移。
  3. 监控空值缓存数量:如果空值缓存占比超过 30%,就要警惕了。可能是攻击,也可能是业务逻辑有问题。
  4. 考虑分层防御:空值缓存只是第一道防线。配合参数校验、限流、布隆过滤器,效果更好。
注意: 空值缓存不能解决所有穿透问题。如果你的系统面临高频恶意攻击,建议优先考虑布隆过滤器。空值缓存更适合作为「兜底方案」。

3.5 什么时候该用?什么时候不该用?

我个人的判断标准很简单:

  • 该用:业务 key 是有限的、可枚举的。比如商品 ID、用户 ID(注册用户有限)。偶尔有查询不存在的情况。
  • 不该用:key 是无限的、可构造的。比如搜索关键词、自定义参数。攻击者可以轻松构造海量不存在的 key。

说白了,空值缓存是个「便宜又好用」的方案,但别指望它能包治百病。了解它的边界,才能用好它。