导读:本期聚焦于小伙伴创作的《怎么利用 Bloom Filter 布隆过滤器在数据入库前过滤无效的缓存穿透请求》,敬请观看详情。缓存穿透让大量不存在的数据请求直击数据库,极易引发雪崩。布隆过滤器以位数组和多个哈希函数构建概率型结构,能在写入前快速判断元素是否绝对不在集合中。把待查主键先过一遍过滤器,未命中直接拒绝,可拦掉绝大多数非法查询。相比空值缓存,它省内存且零误存脏数据,但存在极小误判率。实践中用Redis的BitMap或本地Guava实现,预热全量合法键,配合互斥锁兜底,可稳定保护存储层。

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

怎么利用 Bloom Filter 布隆过滤器在数据入库前过滤无效的缓存穿透请求

布隆过滤器的基本原理

布隆过滤器由一个长度为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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。