文章借鉴于 李琛轩老师的 亿级流量系统架构设计与实战
电子版可关注+私信分享,这里发不出来
一、开篇引言:我们为什么需要布隆过滤器?
在后端开发、大数据处理、爬虫架构中,海量数据的存在性判断是一个极其高频的基础需求。比如:拦截恶意黑名单IP、防止爬虫重复抓取URL、解决Redis缓存穿透、过滤重复注册手机号等。面对动辄千万、上亿级别的数据量,我们传统的存储和查询方案会暴露出明显的性能和内存瓶颈。
先看两类最常用的传统方案弊端:
- 第一,数据库查询,每次存在性判断都走SQL查询,高并发场景下会产生大量DB请求,磁盘IO开销大、查询延迟高,极易拖垮数据库;
- 第二,HashMap、HashSet内存缓存,这类结构查询速度快,但需要完整存储原始数据,1亿条字符串数据会占用数GB内存,空间利用率极低,海量数据场景下完全不适用。
而海量数据判重场景,往往有一个核心共性诉求:接受极小概率误差,但要求极致的内存利用率和查询性能。在这个需求背景下,布隆过滤器(Bloom Filter)应运而生,它是一种经典的概率型空间高效数据结构,用可控的微小误判代价,换取碾压传统结构的内存效率,成为大数据判重场景的核心组件。
不过布隆过滤器并非万能,自身存在诸多原生缺陷,很多场景下必须选择替代方案。本文将从零通俗讲解布隆过滤器核心原理、深度拆解优缺点,全覆盖主流优化方案与替代数据结构,结合实战场景给出精准的技术选型指南,帮大家彻底吃透这类概率型数据结构。
二、布隆过滤器核心原理
布隆过滤器的核心设计极其简洁,核心由二进制位数组(Bit Array)+ 多个独立哈希函数两部分构成,全程不存储任何原始数据,仅通过二进制位的0、1状态记录数据特征,这也是它极致节省内存的根本原因。
#mermaid-svg-4v56nWiW15a8TqMm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4v56nWiW15a8TqMm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4v56nWiW15a8TqMm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4v56nWiW15a8TqMm .error-icon{fill:#552222;}#mermaid-svg-4v56nWiW15a8TqMm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4v56nWiW15a8TqMm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4v56nWiW15a8TqMm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4v56nWiW15a8TqMm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4v56nWiW15a8TqMm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4v56nWiW15a8TqMm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4v56nWiW15a8TqMm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4v56nWiW15a8TqMm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4v56nWiW15a8TqMm .marker.cross{stroke:#333333;}#mermaid-svg-4v56nWiW15a8TqMm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4v56nWiW15a8TqMm p{margin:0;}#mermaid-svg-4v56nWiW15a8TqMm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4v56nWiW15a8TqMm .cluster-label text{fill:#333;}#mermaid-svg-4v56nWiW15a8TqMm .cluster-label span{color:#333;}#mermaid-svg-4v56nWiW15a8TqMm .cluster-label span p{background-color:transparent;}#mermaid-svg-4v56nWiW15a8TqMm .label text,#mermaid-svg-4v56nWiW15a8TqMm span{fill:#333;color:#333;}#mermaid-svg-4v56nWiW15a8TqMm .node rect,#mermaid-svg-4v56nWiW15a8TqMm .node circle,#mermaid-svg-4v56nWiW15a8TqMm .node ellipse,#mermaid-svg-4v56nWiW15a8TqMm .node polygon,#mermaid-svg-4v56nWiW15a8TqMm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4v56nWiW15a8TqMm .rough-node .label text,#mermaid-svg-4v56nWiW15a8TqMm .node .label text,#mermaid-svg-4v56nWiW15a8TqMm .image-shape .label,#mermaid-svg-4v56nWiW15a8TqMm .icon-shape .label{text-anchor:middle;}#mermaid-svg-4v56nWiW15a8TqMm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4v56nWiW15a8TqMm .rough-node .label,#mermaid-svg-4v56nWiW15a8TqMm .node .label,#mermaid-svg-4v56nWiW15a8TqMm .image-shape .label,#mermaid-svg-4v56nWiW15a8TqMm .icon-shape .label{text-align:center;}#mermaid-svg-4v56nWiW15a8TqMm .node.clickable{cursor:pointer;}#mermaid-svg-4v56nWiW15a8TqMm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4v56nWiW15a8TqMm .arrowheadPath{fill:#333333;}#mermaid-svg-4v56nWiW15a8TqMm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4v56nWiW15a8TqMm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4v56nWiW15a8TqMm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4v56nWiW15a8TqMm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4v56nWiW15a8TqMm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4v56nWiW15a8TqMm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4v56nWiW15a8TqMm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4v56nWiW15a8TqMm .cluster text{fill:#333;}#mermaid-svg-4v56nWiW15a8TqMm .cluster span{color:#333;}#mermaid-svg-4v56nWiW15a8TqMm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4v56nWiW15a8TqMm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4v56nWiW15a8TqMm rect.text{fill:none;stroke-width:0;}#mermaid-svg-4v56nWiW15a8TqMm .icon-shape,#mermaid-svg-4v56nWiW15a8TqMm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4v56nWiW15a8TqMm .icon-shape p,#mermaid-svg-4v56nWiW15a8TqMm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4v56nWiW15a8TqMm .icon-shape .label rect,#mermaid-svg-4v56nWiW15a8TqMm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4v56nWiW15a8TqMm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4v56nWiW15a8TqMm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4v56nWiW15a8TqMm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 映射下标1
映射下标5
映射下标13
待存入元素data
哈希函数h₁
哈希函数h₂
哈希函数h₃
Bit位数组
0 1 0 0 0 1 0 ...
2.1 完整工作流程
1. 数据插入流程:当我们需要将一个元素存入布隆过滤器时,会调用预设的多个不同哈希函数,对同一个元素进行多次哈希计算,得到多个不同的位数组下标。随后将位数组中这些下标对应的二进制位,全部从0修改为1,插入操作即完成。整个过程仅需多次哈希计算和位运算,效率极高。
2. 数据查询流程 :判断一个元素是否存在时,依旧通过相同的多个哈希函数计算出对应下标。遍历校验所有下标对应的二进制位,只有所有位全部为1,才判定元素可能存在;只要有一个位为0,可直接判定元素一定不存在。
这里为什么调用多个哈希函数:1. 两个不同元素只要哈希出同一个下标,bit 位就直接重合。误判为两个都存在。2. 空间利用率极差,如果只有一个,必须把数组位拉的超级长。
多哈希的设计思想:必须所有Hash结果全为1,才认为元素存在,但哈希太多,位很快被打满,所以要考虑平衡
3. 原生删除限制原因 :布隆过滤器原生不支持删除元素。核心问题是哈希冲突共享位:多个不同元素的哈希下标可能重叠,某一个二进制位可能被多个元素共同标记为1。如果删除某个元素时,强行将对应位改为0,会导致其他共享该位的元素被误判为不存在,引发数据错乱。
2.2 两大核心概率特性(
无假阴性(100%准确):如果布隆过滤器判定元素不存在,该元素一定不存在。原理很简单:元素从未插入过,对应的哈希下标位必然全部为0,不可能出现全1的情况,不会漏判。
有假阳性(可控误差):如果布隆过滤器判定元素存在,元素不一定真的存在。当多个不同元素的哈希结果刚好覆盖了当前查询元素的所有下标,所有位被意外标记为1,就会出现"误判存在"的情况,这就是假阳性误判,且该误差无法彻底消除,只能通过参数优化降低。
2.3 核心参数与误判率的关系
布隆过滤器的误判率并非固定值,主要由三个核心参数决定:
-
位数组长度(m):数组越长,空闲位越多,哈希冲突概率越低,误判率越小,内存占用越高;
-
哈希函数个数(k):数量适中可降低误判,过多会增加计算开销,过少冲突概率飙升;
-
实际存储元素数量(n):元素越多,位数组被占满,误判率急剧升高。
工程中可通过公式根据预期数据量和可接受误判率,精准计算最优数组长度和哈希函数个数。
三、布隆过滤器剖析
3.1 核心优势
1. 极致的空间利用率:这是布隆过滤器最核心的优势。传统Hash表需要存储完整元素数据、哈希索引、链表指针等冗余信息,而布隆过滤器仅存储二进制位。存储千万级数据时,内存占用仅为Hash表的1/10甚至更低,差距极其明显。
2. 超高读写性能:插入和查询的时间复杂度均为O(k),k为哈希函数个数(通常仅5-10个),可近似看作O(1)。全程仅做哈希计算和位运算,无磁盘IO、无复杂数据迭代,高并发场景性能极其稳定。
3. 适配海量数据场景:不受大数据量冲击,千万、亿级数据下内存增长平缓,不会像Hash表一样出现扩容卡顿、内存溢出等问题,是海量判重的首选基础组件。
4. 结构简单、扩展性强:原理简单、代码实现轻量化,同时适配单机、分布式场景,Redis、Guava、ES等主流框架均内置实现,开箱即用,分布式一致性改造难度低。
3.2 致命短板
1. 存在不可消除的假阳性误判:无法做到100%精准,只能压缩误差。绝对精准的业务场景(如金融交易校验、核心用户信息匹配)无法直接使用。
2. 原生不支持元素删除与过期清理:如前文原理所述,普通布隆过滤器无法删除元素,面对需要动态清理过期黑名单、过期缓存key的场景,原生版本完全不适用,只能通过定时重建整体数据兜底。
3. 无计数能力、无法统计重复次数:仅能判断"是否存在",无法识别元素重复插入的次数,不适合需要统计数据频次、基数的业务场景。
4. 容量过载后误判率爆炸式飙升:预设容量固定,当实际存储数据量远超预期时,位数组大部分位被占满,哈希冲突概率大幅提升,误判率会从万分之一飙升到百分之几十,彻底失效。
四、布隆过滤器主流替代方案
针对布隆过滤器的误判、不可删除、容量固定、无计数四大短板,行业衍生出优化版布隆过滤器,同时诞生了多款针对性替代数据结构。本文将全方位解析各类方案的适用场景。
4.1 优化型布隆过滤器
1. 计数布隆过滤器(Counting Bloom Filter)
优化核心:彻底解决原生布隆过滤器无法删除数据的痛点。
核心原理:将原生的二进制位(0/1)替换为固定位数的计数器(通常4bit),不再是单纯标记状态。插入元素时,对应哈希下标计数器+1;删除元素时,对应下标计数器-1;查询时,只要所有计数器数值大于0,即判定元素存在。
优缺点:支持精准删除元素,兼容原生所有优势;缺点是内存占用提升4倍左右,仍存在假阳性误判,无本质精度提升。
适用场景:需要动态增删数据、对内存敏感度适中、可接受微小误判的场景,如动态黑名单更新、临时流量拦截过滤。
2. 可扩展布隆过滤器(Scalable Bloom Filter)
优化核心:解决原生过滤器容量固定、过载误判飙升的问题。
核心原理:采用动态扩容架构,初始化一个基础布隆过滤器,当数据量达到阈值、误判率接近上限时,自动新建一个更大容量的过滤器,新数据写入新过滤器,查询时遍历所有扩容后的过滤器。
优缺点:无需提前预估精准数据量,支持无限扩容,全程保证低误判率;缺点是多过滤器叠加会轻微增加查询开销,内存碎片化略高。
适用场景:数据量无法预估、业务长期迭代增长、不想频繁手动重建过滤器的场景。
4.2 低误判/零误判替代方案
1. 哈希表 / HashSet
核心特性:零假阳性、零假阴性,100%精准;完整支持增、删、改、查,功能全面,开发适配简单。
- HashSet 就是一个 "精准存、精准查" 的集合,它会真正存储你放进去的每一个元素本身,所以判断 100% 准确。
- 它会:
算元素哈希 → 找到槽位
把你查的元素 和 槽位里存储的真实元素 做 equals 对比
完全一样 → 存在;不一样 → 不存在
核心短板:内存利用率极低,需要存储完整原始数据,海量数据下内存开销巨大;哈希冲突会导致查询性能下降,高并发场景有锁竞争开销。
适用场景:数据量较小(万级、十万级)、业务要求绝对精准无误差、需要频繁增删改查的场景,如后台权限名单、小型白名单校验。
2. 有序数组 + 二分查找
核心特性:零误差、内存占用可控、结构极简,无哈希冲突问题,查询稳定性极强。
核心短板:插入、删除数据需要维护数组有序性,时间复杂度O(n),动态数据场景性能极差,仅适配静态数据。
适用场景:数据基本不更新、静态固定的精准判重场景,如固定字典校验、静态配置白名单。
4.3 高压缩率概率型替代结构
1. 布谷鸟过滤器(Cuckoo Filter)
作为布隆过滤器的强势替代方案,是目前工业界动态海量判重的最优选择之一。
核心优势:支持高效增删改查,不存在传统布隆的删除缺陷;空间利用率比布隆过滤器更高,存储密度提升30%以上;同等内存下误判率更低,性能更稳定。
核心原理:基于哈希桶+元素置换机制,每个元素对应两个哈希桶位置,位置冲突时自动置换已有元素,保证数据高效存储,同时记录元素指纹实现删除能力。
适用场景:需要频繁动态增删、海量数据、低误判、高内存利用率的场景,如实时爬虫URL去重、动态流量风控、海量用户行为去重。
2. 商过滤器(Quotient Filter)
一款轻量化、可迭代、可持久化的概率型数据结构。
核心特性:支持元素删除、排序、遍历迭代,压缩率极高,内存占用优于布隆过滤器;支持磁盘持久化,重启后无需重建数据。
适用场景:需要遍历校验数据、需要落地持久化、冷热数据交替的海量判重场景,多用于大数据离线处理、日志去重分析。
4.4 大数据专属替代方案(场景化专项替代)
1. Roaring Bitmap(压缩位图)
推荐看这个文章
位图结构的极致优化版本,是整数型数据判重、统计的神器。
核心优势:零误判、100%精准,支持高效增删、范围查询、交集/并集计算;自带压缩算法,整数数据存储压缩率极高,千万级整数数据仅需数MB内存,性能远超布隆过滤器。
局限性:仅支持整数类型数据,无法直接存储字符串、URL等复杂类型。
适用场景:用户ID、订单号、设备ID等整数型海量数据的去重、筛选、范围统计,是数仓、大数据分析的核心组件。
2. HyperLogLog
核心定位:不用于精准存在性判断,专门替代布隆过滤器做海量数据基数统计。
核心能力:用极小内存(几KB)统计亿级数据的去重总数,误差可控;完全不存储原始数据,内存利用率达到极致。
核心区别:布隆过滤器是"判断是否存在",HyperLogLog是"统计去重数量",二者场景互补,不可混用。
适用场景:页面UV统计、海量接口访问量去重统计、大数据基数估算场景。
五、核心数据结构横向对比(干货总结)
为方便快速选型,从工程核心维度对所有主流结构进行全方位对比,覆盖所有业务场景诉求:
| 数据结构 | 空间占用 | 查询性能 | 支持删除 | 误差情况 | 落地难度 | 核心适用场景 |
|---|---|---|---|---|---|---|
| 原生布隆过滤器 | 极低 | 极高 | 不支持 | 低假阳性 | 极低 | 静态海量判重、缓存穿透防护、黑名单过滤 |
| 计数布隆过滤器 | 较低 | 高 | 支持 | 低假阳性 | 低 | 动态少量删数、临时风控过滤 |
| 布谷鸟过滤器 | 极低(优于布隆) | 极高 | 支持 | 极低假阳性 | 中 | 海量动态增删判重、爬虫去重、实时风控 |
| Roaring Bitmap | 极低(整数最优) | 极高 | 支持 | 零误差 | 中 | 整数数据去重、范围查询、大数据统计 |
| HashSet | 极高 | 高 | 支持 | 零误差 | 极低 | 小数据量、绝对精准业务场景 |
| HyperLogLog | 极致低 | 极高 | 部分支持 | 基数估算误差 | 低 | 海量数据UV、基数统计 |
六、工程落地场景与选型指南
6.1 布隆过滤器最佳适用场景
-
Redis缓存穿透防护:拦截不存在的key请求,避免大量空请求穿透到数据库,是后端最经典落地场景,可承受亿级空请求拦截。
-
爬虫URL去重:海量URL链接判重,避免重复抓取同一站点资源,可接受微小误判,适配布隆过滤器特性。
-
黑名单过滤:恶意IP、违规账号、垃圾设备的拦截过滤,少量误判可通过二次校验兜底,不影响核心业务。
-
大数据垃圾数据过滤:离线数据清洗中过滤无效、重复数据,提升数据处理效率。
java
@Bean
public RBloomFilter<String> userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) {
RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("userRegisterCachePenetrationBloomFilter");
cachePenetrationBloomFilter.tryInit(100000000L, 0.001);
return cachePenetrationBloomFilter;
}
6.2 必须替换布隆过滤器的场景
-
需要动态删除过期数据(过期黑名单、过期缓存key)→ 优先选择【布谷鸟过滤器】,其次选择计数布隆过滤器。
-
业务绝对不能接受误判(金融、核心用户数据、交易校验)→ 放弃概率结构,使用【HashSet / Roaring Bitmap】。
-
仅需统计去重数量,无需精准判重(页面UV、访问量统计)→ 替换为【HyperLogLog】。
-
整数类型海量数据、需要范围查询(用户ID批量筛选)→ 首选【Roaring Bitmap】,零误差且性能碾压所有概率结构。
6.3 生产落地避坑点
1. 合理配置参数,避免容量过载:上线前必须预估峰值数据量,根据业务可接受误判率(常规业务0.01%)计算数组长度和哈希函数个数,禁止小容量承载大数据量,避免误判率爆炸。
2. 分布式场景一致性问题:单机Guava布隆过滤器无法适配分布式集群,多节点会出现数据不一致,分布式场景优先使用Redis布隆过滤器,保证全局统一。
3. 过期数据无法清理的解决方案:原生布隆无过期机制,生产常用两种方案:① 定时全量重建过滤器(低频次业务);② 分片布隆过滤器,按时间分片存储,过期分片直接删除(高频次业务)。
4. 误判兜底方案:所有使用布隆过滤器的业务,必须预留二次校验逻辑,误判时通过数据库、缓存二次确认,避免影响用户体验。