Redis缓存穿透如何用布隆过滤器精准拦截?

来源:SEO作者:孙悟空头衔:草根站长
导读:本期聚焦于孙悟空创作的《Redis缓存穿透如何用布隆过滤器精准拦截?》,敬请观看详情。缓存穿透是Redis高并发架构中非常典型的风险点:请求查询一个根本不存在的键,缓存中自然没有,流量会直接穿透到数据库,一旦被恶意利用可能造成数据库压力飙升甚至宕机。常规做法诸如缓存空值、互斥锁虽然能缓解症状,却各有短板。缓存空值会占用内存且无法覆盖不断变化的新无效键,互斥锁则引入等待。布隆过滤器提供了一种空间效率极高的概率型数据结构,它通过位数组和多个哈希函数在内存中判断某个键是否可能存在,能从源头拦截绝大多数不存在的数据请求。本文先拆解缓存穿透的产生链路和常见对策,再深入布隆过滤器的位数组、哈希映射、误判率计算与容量规划,最后结合Redisson给出实际落地代码,并讨论删除困难、扩容和误判兜底等工程细节,帮助你构建更稳健的Redis防护层。

缓存穿透是缓存架构中一种典型的异常访问模式。正常查询会先访问Redis,命中则直接返回;未命中再回源数据库,并把结果写入缓存。但如果请求的键在数据库中根本不存在,缓存永远不会产生有效值,每次请求都会穿过Redis直接打到数据库。攻击者可以利用这一特点,构造大量随机不存在的键,绕过缓存层对数据库进行持续冲击,轻则拖慢接口响应,重则导致数据库连接池耗尽、服务不可用。

Redis缓存穿透如何用布隆过滤器精准拦截?

缓存穿透的成因与常见应对方案

缓存穿透的本质是缓存与数据库之间存在空值约定缺失。对于数据库查询返回空的结果,很多业务代码并不会主动写入缓存,因为空值没有业务含义,写了也可能造成大量无意义的缓存条目。于是每一个不存在的键都会完整走一遍Redis查询、数据库查询的链路。当无效请求量大时,数据库就成了整个系统的薄弱点。

缓存空值是最直接的解决办法,也就是在数据库查询不到数据时,仍然向Redis写入一个带短过期时间的空值标记。这样后续相同键的请求至少能命中缓存,不会再打到数据库。但缓存空值的缺点也很明显:如果攻击者不断变化无效键,缓存空值只能覆盖已经请求过的键,新键依然会穿透。同时,大量空值条目会占用Redis内存,尤其是当无效键数量巨大时,会挤占正常业务数据的空间。

另一种常见方案是互斥锁或限流,只在第一个请求到达时回源数据库,其他请求等待结果。这种方式可以限制数据库的并发压力,但实现起来会引入锁等待、超时控制、锁粒度选择等复杂度。在高并发场景下,大量请求阻塞在锁上,响应延迟会明显上升。相比之下,布隆过滤器可以在请求进入缓存查询之前进行快速拦截,以很小的内存代价判断一个键是否可能存在,从源头过滤掉绝大多数无效请求,是处理缓存穿透非常有效的方案。

布隆过滤器的数据结构与误判率

布隆过滤器由一个长度为m的位数组和k个相互独立的哈希函数组成。初始时位数组所有位都是0。向过滤器添加一个元素时,用k个哈希函数分别计算该元素的哈希值,然后对m取模得到k个位置,把这k个位置全部置为1。查询一个元素是否存在时,同样计算这k个位置,如果其中任意一个位置是0,说明该元素一定没有被添加过;如果这k个位置全部是1,则说明该元素可能存在,也可能是因为其他元素的哈希置位导致误判。

这种结构决定了布隆过滤器是一种概率型数据结构:查询结果只会出现一定不存在和可能存在两种情况,不会出现漏判,但会出现误判。假设插入n个元素,位数组长度为m,哈希函数个数为k,某个位在一次哈希后仍然为0的概率是1 - 1/m,经过kn次哈希后仍为0的概率约为e^(-kn/m)。因此误判时k个位全部为1的概率为(1 - e^(-kn/m))^k。通过求导可以得出,当k = (m / n) * ln2时,误判率最低。

工程上通常会先确定预计写入的数据量n和可接受的误判率p,然后反推需要的位数组长度m和哈希函数个数k。计算公式为m = - n * ln p / (ln 2)^2,再代入k = (m / n) * ln2得到最优哈希次数。举个例子,如果预计写入1亿条数据,希望误判率控制在万分之一,那么大约需要19亿位,约占用230MB内存,哈希函数个数约为13个。可以看出,布隆过滤器在允许极小误判的前提下,可以用非常少的空间保护数据库,性能远高于直接查询缓存和数据库。

基于Redisson的布隆过滤器落地实现

在Java技术栈中,Redisson提供了对布隆过滤器的直接支持,底层基于Redis的Bitmap实现,适合分布式环境使用。首先创建Redisson客户端,然后获取RBloomFilter实例。初始化时需要指定期望插入的元素数量和允许的误判率,这两个参数会直接决定Redis中位数组的大小。

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

RBloomFilter<String> bloomFilter = redisson.getBloomFilter("product:bloom");
boolean initialized = bloomFilter.tryInit(100000L, 0.01);
if (initialized) {
    bloomFilter.add("product:1001");
}

boolean exists = bloomFilter.contains("product:1001");
if (!exists) {
    // 直接拒绝请求,不查询数据库
}

这里tryInit方法只能在过滤器首次创建时调用,如果对应的Redis键已经存在,初始化会失败,这可以避免误操作覆盖已有数据。添加元素使用add方法,查询使用contains方法。当contains返回false时,说明该键一定不存在,请求可以在进入Redis前直接返回空结果,避免后续无意义的缓存查询和数据库查询。

实际业务中,布隆过滤器通常放在缓存查询链路的最前面。先判断键是否可能存在,如果不存在则直接返回;如果可能存在,再查询Redis缓存,缓存未命中时回源数据库。由于布隆过滤器存在误判,即使返回可能存在,也仍然需要走完整的缓存与数据库查询流程,不能据此认为数据一定存在。

public Product getProduct(String id) {
    String cacheKey = "product:" + id;

    if (!bloomFilter.contains(id)) {
        return null;
    }

    Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
    if (product != null) {
        return product;
    }

    product = productMapper.selectById(id);
    if (product != null) {
        redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
    } else {
        // 布隆过滤器误判或数据确实不存在时,写入短过期空值兜底
        redisTemplate.opsForValue().set(cacheKey, "", 1, TimeUnit.MINUTES);
    }
    return product;
}

工程实践中的容量规划与避坑要点

布隆过滤器不支持删除操作是一个必须正视的问题。因为位数组中的某一位可能被多个元素共同置为1,直接将该位清零会影响其他元素的判断。如果业务中存在大量删除或过期数据,继续使用传统布隆过滤器会导致误判率逐渐升高。常见的做法是定期重建过滤器,或者引入支持删除的计数布隆过滤器、布谷鸟过滤器。对于大多数商品、用户ID等基础数据来说,数据变更频率较低,定期重建可以满足需求。

容量规划是布隆过滤器落地的关键。初始化时如果对数据量n估计过低,位数组很快会变得密集,误判率会超过预期;如果估计过高,则会造成Redis内存浪费。由于位数组长度在初始化后无法直接调整,需要在设计阶段充分评估业务数据的增长趋势,预留合理冗余。当数据量接近原定容量上限时,应提前创建新的过滤器,通过双写或数据迁移的方式完成扩容,避免在单个过滤器上继续叠加导致性能下降。

布隆过滤器返回可能存在时,并不意味着数据一定存在于数据库。它有天然的误判率,因此业务层仍需保留短过期空值缓存作为兜底。即使某个不存在的键偶尔穿过布隆过滤器,也只会有一次数据库查询,随后空值写入缓存后,后续相同键的请求不会继续穿透。监控方面,应关注过滤器的内存占用、误判情况以及穿透到数据库的无效请求比例,以便及时发现容量不足或参数不合理的问题。

布隆过滤器是解决Redis缓存穿透的一把利器,但也不是银弹。它最适合键空间相对稳定、允许极小误判率的场景。在设计高并发缓存体系时,可以把布隆过滤器、缓存空值、请求限流和数据校验等手段结合起来,形成纵深防御。只有根据业务量认真规划容量和误判率,并在运行过程中持续观察调整,才能让布隆过滤器真正发挥精准拦截的作用。

Redis缓存穿透布隆过滤器误判率修改时间:2026-08-23 18:03:50

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