一、什么是缓存穿透?
缓存穿透 指客户端请求的数据在 缓存(如 Redis 中不存在) ,同时在 数据库中也不存在 。
由于缓存不命中,每次请求都会直接穿透到数据库。当大量这样的请求(例如恶意攻击或随机非法 key)并发过来时,数据库会瞬间承受巨大压力,甚至崩溃,导致整个服务不可用。
典型场景
- 攻击者伪造大量不存在的用户 ID(如
-1,9999999)查询用户信息。 - 业务代码未做空值校验,直接查询数据库。
危害
- 数据库连接被占满,正常请求无法处理。
- 缓存失去"挡箭牌"作用,系统整体可用性急剧下降。
二、常规应对方案的局限
1. 缓存空对象
将查询不到的数据也缓存一个空值(例如 null),并设置较短过期时间。
❌ 缺点:
- 非法 key 可以是无限多个(随机生成),缓存会被大量无意义空值占满,浪费内存。
- 攻击者不断变换 key,依然可以绕过。
2. 请求参数校验(如 ID 格式、范围)
❌ 缺点 :无法防御随机生成但格式合法的 key,例如 1000000 到 9999999 之间大量不存在的 ID。
结论 :需要一种能高效判断 "某个 key 一定不存在" 的数据结构 ------ 布隆过滤器 应运而生。
三、布隆过滤器(Bloom Filter)------ 缓存穿透的第一道防线
3.1 什么是布隆过滤器?
布隆过滤器是一个 非常节省空间的概率性数据结构,它可以告诉您:
- 一定不存在(绝对可靠)
- 可能存在(有一定误判率)
3.2 原理图解
- 初始化一个 位数组(bit array),所有位为 0。
- 插入元素时:
使用 k 个不同的哈希函数 对元素计算 k 个哈希值,分别对数组长度取模,将对应位置设为 1。 - 查询元素时:
同样计算 k 个哈希位置,只要有一个位置为 0 ,则该元素 一定不存在 ;
如果所有位置都为 1,则 可能存在(因为可能与其他元素发生哈希冲突)。
3.3 布隆过滤器能解决缓存穿透吗?
能 。
在查询缓存之前,先用布隆过滤器判断 key 是否存在:
- 若布隆过滤器说"不存在",直接返回(不查缓存,更不查 DB)。
- 若说"可能存在",再走正常的缓存 → DB 流程。
由于大多数非法 key 都是不存在的,布隆过滤器会直接拦截掉绝大部分请求,保护数据库。
3.4 布隆过滤器的缺点
- ❌ 不支持删除元素
因为多个元素的哈希可能共享同一个位,如果把某个位置 0,会误伤其他元素。 - ❌ 存在误判率 (假阳性)
随着元素增多,位数组越来越满,误判率会上升。 - ❌ 参数(位数组长度、哈希次数)需要根据预估数据量和可容忍误判率通过公式计算,新手容易设错。
3.5 Google Guava 轻松使用布隆过滤器
java
// 预估要插入 100 万个元素,期望误判率为 1%
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1_000_000,
0.01
);
// 插入存在的 key
bloomFilter.put("user:1001");
// 判断是否存在
if (bloomFilter.mightContain("user:9999")) {
// 可能存在,去查缓存/DB
} else {
// 一定不存在,直接返回
}
Guava 会根据元素个数和误判率自动计算出最优的位数组长度和哈希次数,无需手动公式。
3.6 数据量增长导致误判率升高怎么办?
- 重建过滤器 :
创建一个新的、更大的布隆过滤器,将数据库中所有存在的 key 重新插入一次,然后原子切换使用新过滤器。 - 注意:重建期间需要双写或暂停写入,以避免数据不一致。
✨ 3.7 为什么不支持删除会成为问题?------ 真实业务场景举例
很多业务场景中,数据会从数据库中物理删除,而布隆过滤器由于无法删除元素,会一直认为这个 key 仍然"可能存在"。
场景一:用户注销/账号删除
- 用户注册时,系统将
user:{uid}插入布隆过滤器。 - 某天用户注销账号,从 DB 中删除了该用户记录。
- 布隆过滤器中该用户的指纹位依然为 1。
- 攻击者或巧合请求该已注销的 uid → 布隆过滤器返回"可能存在",于是请求放行到缓存(无数据)→ 查询 DB(无数据)→ 返回空。
结果:每次请求依然会打到数据库,布隆过滤器形同虚设,防护效果丢失。
场景二:商品下架/库存清理
- 电商平台商品 ID 范围相对固定,活动商品下架后从 DB 删除。
- 布隆过滤器无法同步删除 → 大量对已下架商品的请求仍然穿透到 DB。
- 大促期间,这些无效请求可能挤占数据库资源,影响正常商品查询。
场景三:黑名单动态管理
- 系统维护一个恶意 IP 黑名单,采用布隆过滤器加速判断。
- 某个 IP 被解封(从黑名单中删除),但布隆过滤器仍认为它在黑名单中 → 导致误拦截正常用户。
- 要解决只能重建整个黑名单过滤器,代价高昂。
场景四:缓存穿透防护中的"已删除数据"
原始缓存穿透场景中,我们使用布隆过滤器存储"所有存在于 DB 中的 key"。
当 DB 中某个 key 被删除后,过滤器本应不再认为该 key 存在 ,从而直接拦截对该 key 的请求(因为确实不存在)。
但因为布隆过滤器无法删除,它仍然放行该请求前往 DB,导致 本可以拦截的请求变成了多余的 DB 查询,甚至可能被恶意利用来试探哪些 ID 已被删除。
🎯 结论 :在数据有增、删、改的业务中,布隆过滤器会逐渐积累"已删除但未清除"的标记,导致防护效果衰减,甚至产生误判。这就迫切需要一种支持删除的过滤器。
四、从布隆过滤器的痛点到布谷鸟过滤器
布隆过滤器最大的两个痛点:
- 不支持删除(上述业务场景充分说明了其缺陷)
- 随着容量增长,误判率不易动态调整(需重建)
于是,一个更现代、支持删除的过滤器 ------ 布谷鸟过滤器(Cuckoo Filter) 被提出。
五、布谷鸟过滤器(Cuckoo Filter)详解
布谷鸟过滤器的核心思想是 使用桶(bucket)+ 指纹(fingerprint) ,并通过 布谷鸟哈希 实现踢出与重定位。
5.1 核心数据结构
- 一个 桶数组 ,每个桶可以存储多个 指纹(通常 4~8 个)。
- 指纹 = 元素的哈希值截取低若干位(如 8 位),用于存储和比对。
- 每个元素通过哈希计算出 两个候选桶位置
h1和h2,并且这两个位置可以相互推导(通过异或运算)。
5.2 关键公式(标准布谷鸟过滤器)
设:
fingerprint = fingerprint(x)(取哈希的低f位)i1 = hash(x) % bucket_numi2 = i1 ⊕ hash(fingerprint)
重要特性 :
i2 和 i1 可以通过指纹和异或运算互相计算,这在踢出元素时非常有用。
5.3 插入流程(最精彩的部分)
- 计算
fingerprint,i1,i2。 - 尝试将
fingerprint放入i1桶的空位,如果满则尝试i2桶的空位。 - 如果两个桶都满了,则 随机选择一个桶 ,并随机踢出该桶内的一个旧指纹
old_finger。 - 将新指纹放入被踢出的位置。
- 对被踢出的
old_finger,根据它当前所在的桶位置 (假设是i_current)和old_finger计算出它的另一个候选桶:
i_other = i_current ⊕ hash(old_finger) - 将
old_finger插入到i_other桶中(如果该桶也满,则继续踢出,形成递归)。 - 如果踢出次数超过阈值(如 500 次),表示过滤器太满,需要扩容或重建。
🧠 这个"鸠占鹊巢、反复挪窝"的过程,正是"布谷鸟"名字的由来。
5.4 查询流程
- 计算
fingerprint,i1,i2。 - 检查
i1桶和i2桶中是否存在相同的fingerprint。
若存在,返回"可能存在";否则返回"一定不存在"。
5.5 删除流程(布隆过滤器做不到的事)
- 计算
fingerprint,i1,i2。 - 在
i1桶和i2桶中查找与fingerprint完全匹配的指纹。 - 如果找到,删除其中一份副本(注意:同一个桶内可能有重复指纹,只删一个)。
✅ 删除不会影响其他元素,因为每个桶存储的是独立指纹,不与其他元素共享位。
5.6 布谷鸟过滤器的优点
| 特性 | 布隆过滤器 | 布谷鸟过滤器 |
|---|---|---|
| 空间效率 | 高 | 更高(同等误判率下) |
| 支持删除 | ❌ | ✅ |
| 查询性能 | O(k) | O(桶大小) 通常很小 |
| 动态扩容 | 需重建 | 可设计渐进式扩容(复杂) |
| 实现复杂度 | 简单 | 中等 |
5.7 注意事项
- 布谷鸟过滤器仍有 极低概率的误判(指纹冲突)。
- 删除时可能误删:如果两个不同元素碰巧生成相同的指纹且位于同一对桶中,删除一个会导致另一个也被认为不存在(但概率极低)。
- 填充率超过 ~95% 时,插入可能失败(需扩容)。
六、如何选择?
- 不需要删除 + 实现简单 → 布隆过滤器(适合数据只增不减的场景,如用户注册 ID 永不删除)。
- 需要删除 + 空间敏感 → 布谷鸟过滤器(适合黑名单、动态商品库、用户数据可能注销等场景)。
- 极低误判率要求(像数据库索引) → 仍用传统数据库或精确集合(如 Redis Set)。
在实际缓存穿透防御中,如果业务数据很少删除(如用户 ID 一旦生成就不会删除),布隆过滤器完全够用。
如果存在频繁的 key 删除(例如用户注销、商品下架、黑名单动态更新),布谷鸟过滤器更合适。
七、总结
| 阶段 | 解决方案 | 核心特点 |
|---|---|---|
| 缓存穿透问题 | 布隆过滤器 | 高效拦截不存在的 key,但不支持删除 |
| 布隆过滤器局限(需要删除) | 布谷鸟过滤器 | 支持删除,空间效率更高,但实现稍复杂 |
从 Redis 缓存穿透,到布隆过滤器,再到布谷鸟过滤器,这是一条从 简单高效 到 更灵活强大 的技术演进之路。
理解它们的原理和适用场景,能帮助我们在高并发系统中更好地保护后端数据库。
📌 最后提醒:任何过滤器都只能拦截"一定不存在"的请求,对于真实存在的 key,仍需要缓存 + 数据库的正常读写流程。