一、引言
在大数据场景下,很多问题本质上不是“存储完整对象”,而是“快速判断某个对象是否存在”、“快速统计某类状态”、“快速做集合运算”。如果直接用HashSet、数据库索引或完整明细表来解决这些问题,通常会遇到内存占用高、随机访问多、网络与磁盘 IO 压力大等问题。
Bitmap 和 Bloom Filter都是为了用更紧凑的数据结构解决集合相关问题,二者的共同定位是:在大规模数据场景下,用空间换速度,或者用一定精度损失换更低空间成本;但二者解决的问题并不完全一样,Bitmap 更适合表示“一个有限整数集合中的元素是否存在”,布隆过滤器更适合判断“一个任意对象是否可能存在于集合中”。Bitmap 偏向精确集合表示;布隆过滤器偏向近似存在性判断。
二、Bitmap
1.概念原理
Bitmap,也叫位图,本质上是一段连续的二进制位数组,每一位只保存两个状态:0或1。
在集合表示中,通常用第i位表示整数i是否存在,例如集合:
{1, 3, 5}
可以用 Bitmap 表示为:
index: 0 1 2 3 4 5
value: 0 1 0 1 0 1
如果第i位为1,表示元素i存在;如果为0,表示元素i不存在。如果要表示0 ~ 99999999这一亿个整数的存在状态,需要一亿个 bit,约等于100000000 / 8 = 12500000字节,约 11.92 MiB。这就是 Bitmap 在大规模整数集合上非常省空间的原因。
2.常见使用场景
- 用户签到:Redis Bitmap 常用于用户签到场景,可以用某个用户某月的第
n天对应一个 bit,1表示已签到,0表示未签到,这种方式可以非常紧凑地保存每日签到状态。 - 用户标签与人群圈选:广告、推荐、增长系统中经常需要根据标签筛选用户,如果用户 ID 是连续或可映射到紧凑整数空间的,可以用 Bitmap 表示某个标签下的用户集合。
A 用户群:购买过商品 X
B 用户群:最近 7 天活跃
A & B:购买过商品 X 且最近 7 天活跃
A | B:购买过商品 X 或最近 7 天活跃
A ^ B:只满足其中一个条件的用户
##这类运算直接在 bit 层面完成,比逐个用户遍历通常更高效。
- 去重与基数统计的辅助结构:Bitmap 可以用于整数范围内的精确去重,如果数据 ID 范围有限且可控,用 Bitmap 统计去重数量非常合适,对于一亿范围内的整数,Bitmap 只需要约 11.92 MiB 原始 bit 空间,不包括对象、元数据或容器开销。
- 黑名单、白名单:如果黑名单或白名单 ID 是整数型且范围可控,Bitmap 可以用来快速判断某个 ID 是否在名单内,判断不存在和存在都是精确结果。
3.Bitmap 的局限
Bitmap 的最大限制是它依赖索引位置,如果元素不是整数,通常需要先映射成整数,如果整数 ID 非常稀疏,Bitmap 可能浪费空间。例如只有两个 ID:
1
100000000000
如果直接按最大 ID 建 Bitmap,需要至少100000000001个 bit,约 11.64 GiB,但实际只保存两个元素,这种情况下普通 Bitmap 并不划算。
工程上可以通过 Roaring Bitmap 等压缩位图结构改善稀疏场景,Roaring Bitmap 是一种常见的压缩 Bitmap 实现,它适合在稀疏和稠密数据之间做折中,但实现复杂度高于普通 Bitmap。
三、布隆过滤器
1.概念原理
布隆过滤器是一种空间效率很高的概率型数据结构,用于判断一个元素是否属于某个集合。布隆过滤器可能产生假阳性,但不会产生假阴性,前提是没有删除操作且实现正确。
- 假阳性是指:元素实际不存在,但布隆过滤器判断为可能存在。
- 假阴性是指:元素实际存在,但布隆过滤器判断为不存在。
布隆过滤器的回答不是:存在 / 不存在,而是:一定不存在 / 可能存在,这句话是理解布隆过滤器的核心。
布隆过滤器通常由一个 bit 数组和多个哈希函数组成,初始时,bit 数组所有位都是0,插入元素时,用多个哈希函数计算多个位置,并把这些位置设置为1;查询元素时,再用同样的哈希函数计算多个位置;如果这些位置中有任意一个为0,则元素一定不存在;如果全部为1,则元素可能存在。
例如,假设有 3 个哈希函数:
h1(x) = 2
h2(x) = 5
h3(x) = 9
插入x时:
bit[2] = 1
bit[5] = 1
bit[9] = 1
查询x时:
如果 bit[2]、bit[5]、bit[9] 都是 1,则 x 可能存在。
如果其中任何一个是 0,则 x 一定不存在。
2.为什么会误判?
布隆过滤器的误判来自哈希冲突,不同元素可能把同一批 bit 位置设置为1,当某个未插入元素对应的多个位置都已经被其他元素置为1时,布隆过滤器会误判它“可能存在”。
a 设置了 bit[1], bit[3], bit[5]
b 设置了 bit[2], bit[4], bit[6]
c 查询时需要 bit[1], bit[4], bit[6]
如果c实际没有插入,但它对应的位都已经被a和b设置为1,那么布隆过滤器会误判c可能存在。
在标准布隆过滤器模型中,误判率与 bit 数组大小m、插入元素数量n、哈希函数数量k有关,常见近似公式为:
p ≈ (1 - e^(-kn/m))^k
p:误判率
m:bit 数组长度
n:插入元素数量
k:哈希函数数量
#在给定 m 和 n 时,理论上的最优哈希函数数量约为:k ≈ (m / n) * ln 2
bit 数组越大,误判率通常越低;插入元素越多,误判率通常越高。哈希函数数量不是越多越好,过多会导致更多 bit 被置为1,反而可能提高误判率并增加计算成本。
3.使用场景
- 缓存穿透防护:缓存穿透通常指大量请求查询不存在的数据,导致请求绕过缓存并打到数据库或后端存储;布隆过滤器常用于缓存穿透防护,可以把合法数据 ID 预先放入布隆过滤器;请求到来时,先问布隆过滤器,能过滤掉大量确定不存在的请求,误判的请求仍可能进入后端,但不会漏掉真实存在的数据。
请求 ID
↓
布隆过滤器判断
↓
一定不存在 → 直接拒绝或返回空
可能存在 → 查询缓存 / 数据库
- 网页爬虫 URL 去重:爬虫系统需要判断 URL 是否已经抓取过,如果 URL 数量巨大,直接保存完整 URL 集合会占用大量内存;布隆过滤器可以用较小空间判断 URL 是否可能已经访问过,代价是少量未抓取 URL 可能被误判为已抓取,从而被跳过。对搜索引擎级爬虫来说,这种误判是否可接受取决于业务目标、召回要求和后续补偿机制。
- 黑名单过滤:对恶意 IP、垃圾账号、风险设备指纹等集合,可以用布隆过滤器做前置判断。如果布隆过滤器判断一定不存在,就可以快速放行;如果判断可能存在,再查询精确存储。这种二级结构可以降低精确存储系统的查询压力。
- 日志去重与流式处理:在流式计算中,经常需要判断某个事件 ID 是否已经处理过,如果允许极低比例误判,布隆过滤器可以作为轻量级去重结构;但如果业务要求“绝不能漏处理”,普通布隆过滤器不适合单独承担最终去重职责。
四、二者对比与选择
很多人会把 Bitmap 和布隆过滤器都理解成“省内存的集合”,但这个说法太粗糙,更准确的说法是:Bitmap 是精确位索引集合,布隆过滤器是概率型存在性过滤器。
Bitmap 的关键问题是“索引空间是否可控”,布隆过滤器的关键问题是“误判率是否可接受”。如果业务不能接受误判,就不要把普通布隆过滤器当成最终判断依据,如果 ID 极度稀疏,也不要直接用普通 Bitmap 按最大 ID 开数组。
| 维度 | Bitmap | 布隆过滤器 |
|---|---|---|
| 底层结构 | 位数组 | 位数组 |
| 映射方式 | 元素编号直接对应位下标 | 元素通过多个哈希函数映射到多个位 |
| 判断结果 | 精确存在或不存在 | 不存在一定准确,存在可能误判 |
| 是否误判 | 不误判 | 可能误判存在 |
| 是否漏判 | 不漏判 | 不漏判不存在 |
| 空间依赖 | 依赖最大编号范围 | 依赖元素数量和误判率 |
| 删除能力 | 可以直接把对应位清零 | 普通布隆过滤器不支持安全删除 |
| 适合数据 | 整数、编号、可离散映射的数据 | 字符串、URL、用户标识、复杂对象 |
| 典型价值 | 精确标记状态 | 大规模快速过滤 |
优先选择 Bitmap 的情况:
- 元素可以自然表示为整数 ID
- ID 范围有限且不太稀疏
- 需要精确判断存在与否
- 需要删除元素
- 需要做交集、并集、差集等集合运算
优先选择布隆过滤器的情况:
- 元素不是连续整数,例如 URL、邮箱、设备指纹、订单号
- 数据规模很大,完整集合存储成本高
- 可以接受一定比例的假阳性
- 主要问题是“快速排除一定不存在的元素”
- 不依赖精确删除
实际使用中,两者也可以组合使用:先用布隆过滤器做粗筛,再用数据库、缓存或 Bitmap 做精确判断。