3. 缓存穿透解决方案-空值缓存:原理、实现与取舍
缓存穿透,说白了就是请求绕过了缓存,直接打到数据库上。你想想看,一个不存在的 key,缓存里没有,数据库里也没有,每次请求都穿透过去,数据库压力能不大吗?
我记得刚接手一个电商项目时,就遇到过这种情况。用户疯狂搜索一个不存在的商品 ID,结果数据库连接池瞬间被打满。当时我就在想,这问题必须得治。
空值缓存,就是最直接的解决方案之一。
3.1 空值缓存的原理
原理其实很简单:当数据库查询结果为空时,我们仍然把这个「空结果」缓存起来。下次同样的请求过来,缓存直接返回空,不再穿透到数据库。
来看个流程图,你就明白了:
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,空值缓存反而会撑爆 Redis 内存。这时候就需要布隆过滤器上场了。
3.4 避坑指南
我曾经在一个社交项目中踩过坑。当时用户量不大,空值缓存跑得好好的。后来做活动,瞬间涌入大量新用户,很多用户 ID 不存在,空值缓存数量暴涨,Redis 内存直接打满。
那次之后,我总结了几个要点:
- 一定要设置内存上限:给 Redis 配置
maxmemory,配合allkeys-lru淘汰策略。 - 空值过期时间要随机化:避免大量空 key 同时过期,导致缓存雪崩。可以在基础 TTL 上加上随机偏移。
- 监控空值缓存数量:如果空值缓存占比超过 30%,就要警惕了。可能是攻击,也可能是业务逻辑有问题。
- 考虑分层防御:空值缓存只是第一道防线。配合参数校验、限流、布隆过滤器,效果更好。
注意: 空值缓存不能解决所有穿透问题。如果你的系统面临高频恶意攻击,建议优先考虑布隆过滤器。空值缓存更适合作为「兜底方案」。
3.5 什么时候该用?什么时候不该用?
我个人的判断标准很简单:
- 该用:业务 key 是有限的、可枚举的。比如商品 ID、用户 ID(注册用户有限)。偶尔有查询不存在的情况。
- 不该用:key 是无限的、可构造的。比如搜索关键词、自定义参数。攻击者可以轻松构造海量不存在的 key。
说白了,空值缓存是个「便宜又好用」的方案,但别指望它能包治百病。了解它的边界,才能用好它。