[特殊字符] 从 Redis 缓存穿透到布隆过滤器,再到布谷鸟过滤器:一次穿透防护的进化之旅

一、什么是缓存穿透?

缓存穿透 指客户端请求的数据在 缓存(如 Redis 中不存在) ,同时在 数据库中也不存在

由于缓存不命中,每次请求都会直接穿透到数据库。当大量这样的请求(例如恶意攻击或随机非法 key)并发过来时,数据库会瞬间承受巨大压力,甚至崩溃,导致整个服务不可用。

典型场景

  • 攻击者伪造大量不存在的用户 ID(如 -1, 9999999)查询用户信息。
  • 业务代码未做空值校验,直接查询数据库。

危害

  • 数据库连接被占满,正常请求无法处理。
  • 缓存失去"挡箭牌"作用,系统整体可用性急剧下降。

二、常规应对方案的局限

1. 缓存空对象

将查询不到的数据也缓存一个空值(例如 null),并设置较短过期时间。

缺点

  • 非法 key 可以是无限多个(随机生成),缓存会被大量无意义空值占满,浪费内存。
  • 攻击者不断变换 key,依然可以绕过。

2. 请求参数校验(如 ID 格式、范围)

缺点 :无法防御随机生成但格式合法的 key,例如 10000009999999 之间大量不存在的 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 已被删除。

🎯 结论 :在数据有增、删、改的业务中,布隆过滤器会逐渐积累"已删除但未清除"的标记,导致防护效果衰减,甚至产生误判。这就迫切需要一种支持删除的过滤器


四、从布隆过滤器的痛点到布谷鸟过滤器

布隆过滤器最大的两个痛点:

  1. 不支持删除(上述业务场景充分说明了其缺陷)
  2. 随着容量增长,误判率不易动态调整(需重建)

于是,一个更现代、支持删除的过滤器 ------ 布谷鸟过滤器(Cuckoo Filter) 被提出。


五、布谷鸟过滤器(Cuckoo Filter)详解

布谷鸟过滤器的核心思想是 使用桶(bucket)+ 指纹(fingerprint) ,并通过 布谷鸟哈希 实现踢出与重定位。

5.1 核心数据结构

  • 一个 桶数组 ,每个桶可以存储多个 指纹(通常 4~8 个)。
  • 指纹 = 元素的哈希值截取低若干位(如 8 位),用于存储和比对。
  • 每个元素通过哈希计算出 两个候选桶位置 h1h2,并且这两个位置可以相互推导(通过异或运算)。

5.2 关键公式(标准布谷鸟过滤器)

设:

  • fingerprint = fingerprint(x) (取哈希的低 f 位)
  • i1 = hash(x) % bucket_num
  • i2 = i1 ⊕ hash(fingerprint)

重要特性
i2i1 可以通过指纹和异或运算互相计算,这在踢出元素时非常有用。

5.3 插入流程(最精彩的部分)

  1. 计算 fingerprint, i1, i2
  2. 尝试将 fingerprint 放入 i1 桶的空位,如果满则尝试 i2 桶的空位。
  3. 如果两个桶都满了,则 随机选择一个桶 ,并随机踢出该桶内的一个旧指纹 old_finger
  4. 将新指纹放入被踢出的位置。
  5. 对被踢出的 old_finger根据它当前所在的桶位置 (假设是 i_current)和 old_finger 计算出它的另一个候选桶:
    i_other = i_current ⊕ hash(old_finger)
  6. old_finger 插入到 i_other 桶中(如果该桶也满,则继续踢出,形成递归)。
  7. 如果踢出次数超过阈值(如 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,仍需要缓存 + 数据库的正常读写流程。

相关推荐
@小匠1 小时前
Redis 7 持久化机制
数据库·redis·缓存
Geoffwo1 小时前
Oracle MySQL8.0升级8.4,无感升级数据库
数据库·oracle
u0110225121 小时前
如何自定义查询历史记录面板的展示风格_时间轴样式设计
jvm·数据库·python
2301_769340671 小时前
HTML怎么实现快捷跳转顶部_HTML固定悬浮锚点按钮【介绍】
jvm·数据库·python
m0_609160491 小时前
MySQL如何限制触发器递归调用的深度_防止触发器死循环方法
jvm·数据库·python
呼Lu噜1 小时前
基于C#的ASP.NET Core中分析async、await的使用场景
数据库·c#·asp.net
李白的天不白1 小时前
大规模请求数据并发问题
java·前端·数据库
zjy277772 小时前
Golang bcrypt如何加密密码_Golang密码加密教程【收藏】
jvm·数据库·python
万邦科技Lafite2 小时前
API接口一键获取商品评论,根据商品评论分析客户画像
linux·服务器·数据库·windows·microsoft·电商开放平台