
Redisson Bloom Filter并发读写下误判率为什么不再等于设计值【免费下载链接】redissonRedisson: Valkey Redis Java Client and Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..项目地址: https://gitcode.com/GitHub_Trending/re/redisson上周我们注册去重链路在大促期间开始漏放重复请求。过滤器是初始化时按 1% 误判率FPPFalse Positive Probability即一个从未插入的元素被过滤器判定为可能存在的概率建立的 Redisson Bloom Filter运行一个月后抽样实测误判率已经到设计值的四倍左右。这篇复盘记录从过滤器说谎的现象出发把写路径、生命周期和监控三个环节逐一拆开看。现象流量上来后老用户开始被当成新来的故障表象很简单去重接口本应拦截已注册用户但大促期间一批明明注册过的用户被判定为新用户重复走完了整个注册流程。第一反应是高并发写把过滤器写坏了——这个猜测在当时很自然因为直觉上多个节点同时add位运算会互相踩踏。但这个方向后来被推翻了。先看 Redisson 把过滤器拆成了什么。先排查写路径add 比想象中更原子在 Redis 里一个 Redisson Bloom Filter 实际占两个 key见源码 redisson/src/main/java/org/redisson/RedissonBloomFilter.java位数组本体一个普通的字符串 key元素指纹通过SETBIT/GETBIT读写配置一个 Hash key形如{name}:config存size位数组长度、hashIterations每个元素要置位的哈希函数个数、expectedInsertions、falseProbability四个参数。tryInit(expectedInsertions, falseProbability)做的事就是用这两个参数按经典公式反推size位数组要开多长和hashIterations每个元素置几个位。位数组开多长、每个元素置几个位直接决定误判率水平这两个参数是过滤器全部行为的锚点。客户端拿到元素后先在本地算出要置的位下标然后整段 add 逻辑封装进一个 Lua 脚本发给 Redis 执行。脚本的核心节选自addAsync内嵌脚本长这样用来验证写入是否真的原子、配置漂移是否会被服务端抓住local size redis.call(hget, KEYS[1], size) local hashIterations redis.call(hget, KEYS[1], hashIterations) assert(size ARGV[1] and hashIterations ARGV[2], Bloom filter config has been changed) -- 随后对客户端算好的每个下标执行 SETBIT并统计新置位的元素数量两个关键结论位级并发写不是问题。Redisson 客户端对 Redis 的写操作是单线程执行的一个add里的所有SETBIT都在同一个 Lua 脚本内完成不存在两个节点同时置位互相踩踏的空间。给add外面再包一层分布式锁只是白白增加延迟。服务端会校验客户端的配置视图。客户端实例在 JVM 内缓存了一份size/hashIterations首次调用时懒加载如果这份缓存和服务端不一致add 的脚本直接断言报错contains 的脚本则返回 0——正确性由服务端兜底代价是下文要讨论的两类故障表现。所以排查方向要从元素级并发转到过滤器级生命周期。真正的风险点在生命周期初始化之前读写key 刚建好的那个窗口如果配置 Hash 还不存在客户端的懒加载会直接抛出IllegalStateException(Bloom filter is not initialized!)。线上踩到的场景是新扩容的节点启动时先于运维脚本执行tryInit就收到了请求日志里一片初始化异常。更隐蔽的变体是有人手动DEL掉了{name}:config这一个 key——位数组还在但过滤器对所有客户端来说等于未初始化读路径全部报错。两个 key 必须成对出现、成对删除任何绕过 Redisson API 的裸操作都是隐患。一次没人注意的重新初始化contains 静默返回 0这是本次故障的真正根因。tryInit被一个定时任务重复执行过一次幂等保护没做好配置 Hash 被重写size变了。旧 JVM 里还活着、缓存着旧配置的客户端实例此后每次 contains 的脚本都命中配置不一致分支静默返回 0——所有老用户被判为不存在去重等于失效。注意这里没有任何报错日志干净得反常。排查时正是靠add 有断言报错、contains 却静默失败这个不对称才把范围锁到配置漂移上。如果业务里 contains 的失败率突然异常第一个该做的动作是比对客户端内存中的size和服务端HGET {name}:config size是否一致。超出设计容量误判率公式不会说谎第三个风险点是容量。误判率不是常数设计值只在插入量 ≤ expectedInsertions时成立。插入量超过预期后位数组填得越满FPP 单调上升过滤器只是变得更不准不会报错。本次故障里 4 倍的实测误判率相当一部分就来自注册量冲到设计容量 1.6 倍而无人察觉。另外单个过滤器有硬上限tryInit计算出的size超过Integer.MAX_VALUE * 2L约 42.9 亿 bit合 512MB 位数组会直接抛异常。容量规划要留出余量真正要突破这个量级时正确做法是按业务键分片成多个过滤器而不是拉大单个size。初始化把只做一次交给服务端tryInit的实现值得单独看一眼因为它回答了并发初始化会不会写坏的问题。下面是部署侧要用的最小初始化代码tryInit返回false表示已经有别的调用方初始化过了RBloomFilterLong filter redisson.getBloomFilter(reg_user); // 预期 1000 万条误判率 1%size 与 hashIterations 由服务端按公式推导 filter.tryInit(10_000_000, 0.01);它把检查是否已初始化 写入配置合进同一个 Lua 脚本EXISTS为真则返回 0所以任意多个节点同时启动、同时调tryInit也只有一个成功不需要应用层加锁。真正需要防的不是并发初始化而是初始化后的配置变更——size和hashIterations一旦定下就不该再变任何想改参数的冲动都应该翻译成换一个名字重建。监控水位怎么知道过滤器快满了布隆过滤器本身不提供精确计数但count()给出了无偏估计它统计位数组已置位的数量c代入-m/k * ln(1 - c/m)反推插入量。配合设计容量就能做水位告警这段监控逻辑要验证的就是插入量是否在逼近设计容量long estimated filter.count(); double load estimated / (double) filter.getExpectedInsertions(); if (load 0.8) { alert(bloom filter load {}接近设计容量误判率将上升, load); }另一条线是对 contains 结果做抽样核对定期取一批已知存在的元素批量contains命中率显著低于 100% 就是静默返回 0类配置漂移的信号——这类故障不报错只有主动抽样才能发现。再补一条内存观测sizeInMemory同时覆盖位数组和配置两个 key水位、命中率、内存三个指标同时看基本能覆盖过滤器会出的主要问题。把恢复变成标准动作确认要重建时不要在线改任何参数走旁路重建 改名切换新建reg_user_v2从权威数据源DB回填并抽样验证然后改名顶替旧名字。切换用rename完成它对位数组和配置两个 key 一起原子改名同名旧 key 被直接替换RBloomFilterLong newFilter redisson.getBloomFilter(reg_user_v2); // 回填与抽样验证通过后原子顶替旧名旧数据即被覆盖 newFilter.rename(reg_user);切换瞬间读到旧配置的客户端下一次调用会被服务端断言拒绝或静默判空但不会读错重启或重建实例后即恢复一致。若只是短期过载而非数据过期也可以先用expire给过滤器设 TTL 兜底到期后按上述流程重建。复盘的结论可以收成三句话Redisson 的写路径本身是原子的元素级并发不需要额外加锁要给并发加防护的是初始化、改名、删除这些生命周期操作size与hashIterations一经确定就不可变任何参数调整都应换成换名重建容量水位、contains 抽样命中率、内存占用三个监控指标里后两个对静默故障尤其关键。过滤器会说谎但它的谎话是有模式的把生命周期操作收敛到 API 内、把监控前置这类异常基本都能提前看到。相关参考官方文档docs/data-and-services/collections.md源码实现redisson/src/main/java/org/redisson/RedissonBloomFilter.java【免费下载链接】redissonRedisson: Valkey Redis Java Client and Real-Time Data Platform. Sync/Async/RxJava/Reactive API. Over 50 Valkey and Redis based Java objects and services: Set, Multimap, SortedSet, Map, List, Queue, Deque, Semaphore, Lock, AtomicLong, Map Reduce, Bloom filter, Spring, Tomcat, Scheduler, JCache API, Hibernate, RPC, local cache..项目地址: https://gitcode.com/GitHub_Trending/re/redisson创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考