2. 缓存穿透原理:缓存穿透的成因分析、空值缓存与布隆过滤器原理、穿透对数据库的压力模型
2.1 缓存穿透的成因分析
缓存穿透是指查询一个根本不存在的数据,导致请求绕过缓存层,直接穿透到后端数据库(DB)的现象。由于缓存层对该 key 没有命中(不存在),每次请求都会直接访问数据库,而数据库也查不到该记录,因此不会回写缓存。这种“空查询”在高并发场景下会形成对数据库的直接冲击。
常见触发场景包括:
- 恶意攻击或爬虫:攻击者构造大量不存在的 key(如随机 ID、负数 ID、超长字符串)发起请求。
- 业务逻辑漏洞:前端未做参数校验,允许传入非法或无效的查询参数。
- 缓存与数据库数据不一致:数据已被删除,但缓存未及时失效,导致后续请求查不到缓存后直接查 DB。
- 缓存预热不完整:新上线功能或数据迁移后,部分合法 key 未被加载到缓存中。
| 穿透类型 | 触发原因 | 典型特征 | 危害等级 |
|---|---|---|---|
| 恶意穿透 | 攻击者构造随机/非法 key | QPS 突增,key 无规律 | 高 |
| 误删穿透 | 数据已删除但缓存未清 | 少量 key 持续穿透 | 中 |
| 预热穿透 | 新数据未加载到缓存 | 上线初期出现 | 低 |
| 参数穿透 | 前端未校验非法参数 | 特定参数格式异常 | 中 |
2.2 空值缓存原理
空值缓存(Null Cache)是最直接的防御手段。当数据库查询结果为空时,仍然将该 key 写入缓存,但 value 标记为空值占位符(如 null、""、"NULL"),并设置一个较短的过期时间(通常 30~60 秒)。后续相同 key 的请求会直接命中缓存中的空值,从而避免穿透到 DB。
// 伪代码示例:空值缓存逻辑
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;如果判定存在,才允许进入缓存层。
// 伪代码示例:布隆过滤器 + 缓存穿透防御
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 计算得出 |
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 | 雪崩风险 |
压力模型的非线性特征还体现在:数据库连接池、线程池、磁盘 IO 等资源在达到阈值后会快速恶化。例如,MySQL 在 QPS 超过 5,000 时,查询延迟可能从 5ms 飙升到 500ms,触发连接超时和重试风暴,最终导致缓存雪崩(大量请求同时穿透)。