Bitmap 与布隆过滤器:大数据场景下的原理与取舍

0 阅读9分钟

一、引言

在大数据场景下,很多问题本质上不是“存储完整对象”,而是“快速判断某个对象是否存在”、“快速统计某类状态”、“快速做集合运算”。如果直接用HashSet、数据库索引或完整明细表来解决这些问题,通常会遇到内存占用高、随机访问多、网络与磁盘 IO 压力大等问题。

Bitmap 和 Bloom Filter都是为了用更紧凑的数据结构解决集合相关问题,二者的共同定位是:在大规模数据场景下,用空间换速度,或者用一定精度损失换更低空间成本;但二者解决的问题并不完全一样,Bitmap 更适合表示“一个有限整数集合中的元素是否存在”,布隆过滤器更适合判断“一个任意对象是否可能存在于集合中”。Bitmap 偏向精确集合表示;布隆过滤器偏向近似存在性判断。

二、Bitmap

1.概念原理

Bitmap,也叫位图,本质上是一段连续的二进制位数组,每一位只保存两个状态:01

在集合表示中,通常用第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实际没有插入,但它对应的位都已经被ab设置为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 做精确判断。