缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都没有命中,导致请求每次都落到数据库上。当恶意刷量或随机ID遍历发生时,这种无效请求会瞬间打满数据库连接,造成正常业务不可用。布隆过滤器是一种空间效率极高的概率型数据结构,它能在数据写入系统前就判断某个key是否绝对不可能存在,从而把无效请求挡在入库逻辑之外。

布隆过滤器的基本原理
布隆过滤器由一个长度为m的位数组(bit array)和k个相互独立的哈希函数组成。初始化时所有位都是0。当要添加一个元素时,用这k个哈希函数对元素计算,得到k个数组下标,把对应位设为1。查询元素时同样计算k个下标,只要其中有一位为0,就可以确定该元素一定不在集合中;如果全部为1,则元素可能在集合中,但也可能是其他元素写入导致的误判。
这种特性正好适合缓存穿透防护:我们把所有合法存在的数据主键提前加入过滤器。业务查缓存未命中后,先问过滤器“这个ID在吗”。过滤器说不在,那就直接返回空,绝不再查数据库,也就谈不上数据入库或回写缓存。只有过滤器说可能在,才放行到数据库校验。由于误判只会让极少真实不存在的key漏过去,整体拦截率非常高。
误判率与参数选择
误判率p、元素数量n和位数组大小m的关系近似为 m = -n*ln(p) / (ln2)^2,哈希函数个数 k = m/n * ln2。假设系统有1000万合法用户ID,允许万分之一误判,那么m大约需要1.7亿位,即约20MB内存,对现代服务来说完全可以接受。参数设计时要按峰值数据量预留空间,避免后期元素过多导致误判飙升。
需要注意的是,标准布隆过滤器不支持删除元素。如果业务有软删除,可以改用计数布隆过滤器或者用定时重建的方式保持数据新鲜。在入库前过滤场景里,通常合法集合变化不频繁,每天凌晨重建一次过滤器是比较稳妥的做法。
基于Redis BitMap的实现示例
利用Redis的SETBIT和GETBIT命令,可以轻松搭建分布式布隆过滤器。下面以Java配合Redis模板为例,展示初始化和判断逻辑。为简化,这里用三个哈希函数做演示,生产环境建议使用更严谨的算法如Murmur3组合。
import redis.clients.jedis.Jedis;
import java.nio.charset.StandardCharsets;
import java.util.zip.CRC32;
public class SimpleBloomFilter {
private Jedis jedis;
private String key = "user_id_bloom";
private int bitSize = 1 << 24; // 约1600万位
public SimpleBloomFilter(Jedis jedis) {
this.jedis = jedis;
}
// 添加元素
public void put(long id) {
for (int i = 0; i < 3; i++) {
int index = hash(id, i) % bitSize;
jedis.setbit(key, index, true);
}
}
// 判断是否存在
public boolean mightContain(long id) {
for (int i = 0; i < 3; i++) {
int index = hash(id, i) % bitSize;
if (!jedis.getbit(key, index)) {
return false; // 绝对不在
}
}
return true; // 可能在
}
private int hash(long id, int seed) {
CRC32 crc = new CRC32();
crc.update((id + "-" + seed).getBytes(StandardCharsets.UTF_8));
return (int) crc.getValue();
}
}
上述代码在应用启动时从数据库全量读取合法主键并调用put方法预热。在查询接口中,先调用mightContain,返回false就直接抛异常或返回空列表,不执行后续数据库操作。这样无效缓存穿透请求在到达存储层之前就被布隆过滤器消化掉了。
如果部署多个服务节点,共用同一个Redis实例即可保证过滤器视图一致。为了避免大key集中写爆,可以用分段key,比如按id区间拆成多个bitmap,减轻单实例压力。同时给key设置不过期,由后台任务负责重建。
与空值缓存方案的对比
另一种常见防穿透做法是:数据库查不到时,在缓存写一条短期过期的空值。它的优点是实现简单,缺点是如果攻击方用大量不同随机key,缓存会被无效空值占满,而且短过期内这些key再次穿透仍要查库。布隆过滤器则从源头判断,不合法key根本不进缓存也不进库。
| 方案 | 内存占用 | 误判情况 | 防御随机穿透 |
|---|---|---|---|
| 空值缓存 | 随攻击key增长 | 无 | 弱,依赖过期 |
| 布隆过滤器 | 固定且小 | 有极低误判 | 强,提前拦截 |
从表中可以看出,当面对有意的穿透攻击时,布隆过滤器在内存可控性和拦截能力上明显占优。当然它不能替代缓存空值的所有场景,比如合法但暂时无数据的查询,仍可用短空值做二层保护。
落地时的注意事项
第一,过滤器必须覆盖所有入库合法键。如果新注册用户没及时加入,会被误判为不存在,导致写入失败。因此写库成功的同时要异步补写过滤器,或依赖定时全量重建保证最终一致。
第二,误判率虽小但存在,下游数据库仍需有基本限流和熔断。布隆过滤器是概率拦截不是绝对屏障,把它当作第一道防线,配合连接池限制和降级策略,才能构建完整防护体系。
第三,选择本地还是分布式实现要看业务。单机Guava BloomFilter延迟最低,但多节点数据同步麻烦;Redis实现集中统一,但多一次网络往返。对延迟极敏感且节点少的系统可用本地,对一致性要求高用Redis更合适。
Bloom_Filter缓存穿透数据入库修改时间:2026-08-06 13:39:35