3. 缓存穿透治理方案:布隆过滤器原理与实现、空值缓存策略、参数校验与限流
缓存穿透是指查询一个根本不存在的数据,缓存层和数据库层都无法命中,导致每次请求都直接穿透到数据库。在高并发场景下,这会使数据库承受巨大压力,甚至引发雪崩。本章将系统讲解三种核心治理手段:布隆过滤器、空值缓存策略以及参数校验与限流。
核心原则:缓存穿透治理的目标是在请求到达数据库之前,尽可能早地拦截无效查询。布隆过滤器用于“预判不存在”,空值缓存用于“标记不存在”,参数校验与限流用于“拒绝非法与超量请求”。
3.1 布隆过滤器原理与实现
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它允许一定的误判率(假阳性),但绝对不会漏判(假阴性率为0)。
3.1.1 工作原理
- 初始化:创建一个长度为 m 的位数组(bit array),所有位初始化为 0。
- 添加元素:使用 k 个独立的哈希函数,将元素映射到位数组的 k 个位置,并将这些位置的值设为 1。
- 查询元素:同样使用 k 个哈希函数计算位置,如果所有位置的值都是 1,则元素可能存在;如果任何一个位置是 0,则元素一定不存在。
关键参数:
- m:位数组长度(bit 数)
- n:预期插入元素数量
- k:哈希函数个数
- p:期望误判率
最优哈希函数个数:k = (m/n) * ln(2)
3.1.2 在缓存穿透治理中的应用
在缓存层之前部署布隆过滤器,将所有可能存在的 key 预先加载到位数组中。当请求到达时:
- 布隆过滤器判断不存在:直接返回空响应,不查询缓存和数据库。
- 布隆过滤器判断可能存在:放行请求,继续查询缓存或数据库。
注意事项:
- 布隆过滤器无法删除元素(标准实现)。如需删除,可使用 Counting Bloom Filter。
- 数据更新时,需要同步更新布隆过滤器(重建或使用增量更新机制)。
- 误判率 p 建议设置在 1% ~ 5% 之间,以平衡空间和准确性。
3.1.3 代码实现示例(Guava)
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;
public class BloomFilterDemo {
// 预期插入数量
private static final int EXPECTED_INSERTIONS = 1000000;
// 期望误判率
private static final double FPP = 0.01;
private static final BloomFilter<String> bloomFilter =
BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),
EXPECTED_INSERTIONS, FPP);
public static void init(List<String> allPossibleKeys) {
for (String key : allPossibleKeys) {
bloomFilter.put(key);
}
}
public static boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
}
3.2 空值缓存策略
当查询数据库发现数据不存在时,将空值(或特殊标记)也写入缓存,并设置一个较短的过期时间。这样,后续相同的请求在缓存中就能直接命中空值,避免再次穿透到数据库。
3.2.1 实现步骤
- 查询缓存:如果缓存命中,直接返回结果(包括空值标记)。
- 查询数据库:如果缓存未命中,查询数据库。
- 数据存在:将数据写入缓存,设置正常过期时间。
- 数据不存在:将空值(如
null或"EMPTY")写入缓存,设置较短过期时间(如 30 ~ 60 秒)。
3.2.2 参数对比
| 策略 | 缓存内容 | 过期时间 | 适用场景 |
|---|---|---|---|
| 正常缓存 | 真实数据 | 较长(如 1 小时) | 数据存在且稳定 |
| 空值缓存 | 空值标记 | 较短(如 30 秒) | 数据不存在,但可能被频繁查询 |
最佳实践:空值缓存应与布隆过滤器配合使用。布隆过滤器拦截大部分不存在 key,空值缓存作为兜底,处理布隆过滤器误判或数据删除后重新查询的情况。
3.3 参数校验与限流
参数校验和限流是缓存穿透治理的第一道防线,在请求进入系统的最前端进行拦截。
3.3.1 参数校验
对请求参数进行严格的合法性校验,拒绝明显无效的 key。例如:
- 格式校验:key 必须符合 UUID 格式、数字范围、字符串长度等。
- 业务规则校验:用户 ID 必须大于 0、订单号必须存在等。
- 黑名单校验:拦截已知的恶意 key 或攻击模式。
// 参数校验示例
public Response getProduct(String productId) {
// 1. 基础校验
if (productId == null || productId.isEmpty()) {
return Response.error("productId 不能为空");
}
// 2. 格式校验
if (!productId.matches("^[A-Z]{2}\\d{6}$")) {
return Response.error("productId 格式不正确");
}
// 3. 业务校验
if (isBlacklisted(productId)) {
return Response.error("非法请求");
}
// ... 后续查询逻辑
}
3.3.2 限流
对同一 key 或同一 IP 的请求进行限流,防止恶意攻击或异常流量导致缓存穿透。常用限流算法:
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 令牌桶 | 以固定速率生成令牌,请求需获取令牌才能执行 | 允许突发流量,平滑限流 | 实现相对复杂 |
| 漏桶 | 请求以固定速率流出,超出桶容量则丢弃 | 流量整形效果好 | 无法应对突发流量 |
| 滑动窗口 | 将时间划分为小窗口,统计每个窗口内的请求数 | 实现简单,精度可控 | 窗口边界可能出现流量尖峰 |
限流粒度建议:
- 全局限流:限制整个系统的 QPS,防止数据库被打满。
- Key 级别限流:对同一个 key 的访问频率进行限制,防止热点 key 穿透。
- 用户/IP 级别限流:限制单个用户或 IP 的访问频率,防止恶意攻击。
3.4 三种策略对比与选型建议
| 策略 | 拦截能力 | 空间成本 | 时间成本 | 误判风险 | 推荐场景 |
|---|---|---|---|---|---|
| 参数校验与限流 | 低(仅拦截明显非法请求) | 低 | 低 | 无 | 所有系统必备 |
| 布隆过滤器 | 高(拦截大部分不存在 key) | 中(位数组) | 低(O(k) 哈希计算) | 有(假阳性) | key 空间大、数据相对静态 |
| 空值缓存 | 中(拦截已查询过的不存在 key) | 低(少量空值缓存) | 中(需查询数据库一次) | 无 | key 空间小、数据动态变化 |
推荐组合:参数校验与限流(必选)+ 布隆过滤器(核心)+ 空值缓存(兜底)。三层防线层层递进,能够有效抵御 99% 以上的缓存穿透攻击。