阅读指南
这篇文章从一个经典的"40 亿整数判存在"问题出发,带你理解工程系统(以 HBase 为例)的设计思路:当数据规模大到无法全部放入内存时,系统如何通过巧妙的数据结构和读取路径设计,高效地回答"某个数据在不在集合中"这类问题。
建议你这样阅读:
-
• 第一遍:先抓住主要思路:问题目标 → 约束条件 → 解决方案的选择 -
• 第二遍:再关注技术细节:Bitmap 的使用限制、Bloom Filter 的误判原理、HBase 读取路径中过滤和索引的分工
学习目标
读完这篇文章,希望你能够:
-
• 用"Membership Test"这个概念来统一理解各种看似不同的问题 -
• 解释为什么 Bitmap 在某些情况下是"完美解决方案",但在 HBase 场景中却不可行 -
• 理解 Bloom Filter 在 HBase 中的作用:主要是减少不必要的磁盘读写操作,而不是直接给出最终答案
需要提前了解的知识
(如果不太熟悉也没关系,可以边看边学习):
-
• 哈希函数的基本特点(分布均匀、结果确定) -
• 基本的外部存储访问成本:磁盘或 SSD 的随机读取比内存访问慢很多
一、问题的起点:一个看似“算法题”的问题
给定 40 亿个不重复、未排序的整数,在内存限制为 1GB 的条件下,如何快速判断某个整数是否在这 40 亿个数中?
这是学习数据结构和算法时很可能会遇到的一个问题。虽然看起来像一道算法题,但实际上包含了多个系统层面的限制条件:
-
• 数据规模巨大(40 亿) -
• 数据无序 -
• 查询可能是高频的 -
• 内存受限(1GB)
在继续之前,我们先暂停一下,思考一个更基础的问题:
这个问题的核心目标到底是什么?
不是排序,不是遍历,而是——
判断某个元素是否属于一个巨大集合。
这类问题在计算机科学中有一个统一的名字:
大规模集合成员查询问题(Membership Test Problem)
为了让后面的讨论更“工程化”,我们把问题写成一个更明确的接口(只描述你要做什么,而不提前绑定实现):
# 说明:Membership Test 的目标接口
build(S): 给定集合 S,构建可查询的表示
contains(x): 查询 x 是否属于 S
后续所有方案,都是在不同约束下实现同一个接口,并在“内存、构建成本、查询吞吐、准确性”之间做取舍。
二、为什么“直觉解法”都行不通?
在分析问题时,先看看“直觉上可能想到但实际行不通的解法”是很有帮助的。
1. 排序 + 二分查找?
-
• 排序需要将 40 亿整数加载到内存或进行外排序 -
• 查询虽然是 O(log N),但整体成本极高 -
• 不满足“快速判断”的工程预期
❌ 不合适
2. HashSet / HashMap?
-
• 40 亿整数(4,000,000,000 个) -
• 每个整数在 Java 中: -
• int 值:4 字节 -
• 对象头:约 12-16 字节 -
• 指针开销:8 字节(64 位系统) -
• 考虑装载因子(0.75):实际需要约 53 亿个槽位
👉 总内存消耗:40 亿 × (4 + 16 + 8) ≈ 40 亿 × 28 字节 ≈ 11.2GB
(远超 1GB 限制,即使考虑优化也至少需要 4-5GB)
❌ 不可行
3. 顺序扫描?
-
• 时间复杂度 O(N) -
• 对高频查询完全不可接受
❌ 不现实
4. 一个重要的转折点
到这里,你可能会感到有些困惑。此时非常关键的一步是重新审视问题目标:
我们真的需要“存下这 40 亿个整数的值”吗?
我们真正关心的只有一件事:
某个整数在 / 不在
这句话背后隐含了一个工程上的“降维”思想:如果你的查询只需要 1 bit 的答案(在/不在),那么存储结构最好也尽可能接近 1 bit 的信息量,而不是把整数以对象、指针等形式“膨胀存储”。
三、Bitmap:在“理想约束”下的完美答案
1. Bitmap 的基本思想
如果整数的取值范围是有限的(例如 32 位无符号整数),可以使用一个位数组:
-
• 下标:整数值 -
• 值:1 表示存在,0 表示不存在
每个整数只占 1 bit。
2. 内存是否可行?
-
• 32-bit 整数最大值域:2³² = 4,294,967,296 个可能值 -
• 所需位数:2³² bits = 4,294,967,296 bits -
• 折算为内存:
[
2^{32} \text{ bits} ÷ 8 = 2^{29} \text{ bytes} = 536,870,912 \text{ bytes} = 512 \text{ MiB}
]
👉 完全在 1GB(1,024 MiB)内存限制内
内存计算:
-
• bit ÷ 8 = byte = 512 MiB -
• (注:MiB = byte,便于与 计算对齐)
3. Bitmap 的性能特征
| 维度 | Bitmap |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
可以说,在当前约束条件下,Bitmap 几乎是一个完美解。
为了更具象,下面给出一份“位图存取”的伪代码(把整数映射到 bit 位):
# 说明:示例展示 Bitmap 的核心访问方式(按位设置/测试)
# 假设 key 落在 [0, U) 且 U 可控(例如 U = 2**32)
# 注意:这里使用无符号整数,实际工程中需要考虑负数处理
def set_bit(bitmap: bytearray, key: int) -> None:
byte_index = key >> 3 # key // 8(等价于整除8)
bit_offset = key & 7 # key % 8(取模8,得到0-7的偏移)
bitmap[byte_index] |= 1 << bit_offset # 设置对应位为1
def test_bit(bitmap: bytearray, key: int) -> bool:
byte_index = key >> 3
bit_offset = key & 7
return (bitmap[byte_index] >> bit_offset) & 1 == 1 # 测试对应位是否为1
# 位操作解释:
# >> 3:右移3位相当于除以8,得到字节索引
# & 7:与7进行位与操作相当于对8取模,得到位偏移(0-7)
# 1 << bit_offset:将1左移对应位数,创建位掩码
四、关键反思:Bitmap 成立依赖哪些前提?
这是本文第一个关键知识点。
Bitmap 并不是“普适解法”,它隐含了多个非常强的前提条件:
-
1. Key 是整数 -
2. 值域是有限且可控的 -
3. Key 分布相对稠密 -
4. 可以为整个值域分配连续内存
此时我们需要思考这么一个问题:
如果这些前提中有任何一个被打破,会发生什么?
理解"前提被打破"的后果,有助于你将这个思路应用到其他问题:
-
• 如果值域不可控:需要为“可能从未出现”的值也分配 bit 位,空间迅速失控。 -
• 如果极度稀疏:集合大小 明明很小,但值域 很大,Bitmap 的空间消耗由 决定而不是由 决定,性价比很差。 -
• 如果 Key 不是整数:需要先把 Key 映射到整数域,映射必须可逆(才能做到 0 误判),这通常会回到“存储原始值”的问题。
五、问题换一层皮:HBase 面临的“同一个问题”
现在,我们把问题稍作变化。
1. 问题的新表述
在 HBase 中,系统每天都要面对类似的问题:
给定海量 RowKey,如何快速判断某个 RowKey 是否存在?
我们做一个一一对应:
| 原问题 | HBase 场景 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
从抽象层面看,这仍然是一个 Membership Test 问题。
为了让映射更清晰,可以把 HBase 的一次读取(Get)拆成两个子问题:
-
• Membership Test:某个 RowKey 是否可能出现在某个数据文件(HFile)里? -
• 精确定位:如果“可能存在”,如何在外存结构中快速定位到目标 Key 对应的 Cell?
HBase 的工程实现恰好把这两件事拆开:先用 Bloom Filter 过滤不必要的文件访问,再用 HFile 的索引结构完成精确查找。(来源:Apache HBase Reference Guide;《HBase: The Definitive Guide》)
六、为什么 Bitmap 在 HBase 中不可行?
这是第二个关键知识点。
| Bitmap 的前提 | HBase 的现实 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
结论非常明确:
不是 Bitmap 不够优秀,而是 HBase 的约束条件彻底否定了 Bitmap。
把这句话落到更具体的工程细节上:
-
• RowKey 是 byte[](变长),值域理论上近乎无穷,Bitmap 无法“枚举所有可能”。 -
• 即使把 byte[]通过哈希映射到固定整数域,也会引入哈希冲突;除非额外存储原 Key 来消解冲突,否则无法做到 0 误判,这又把空间压力拉回来了。 -
• HBase 数据分片在多个 Region、多个 Store、多个 HFile 中;Bitmap 更像“对一个固定全集做全量映射”,与这种多文件、多层结构的组织方式不匹配。
七、工程系统的选择:Bloom Filter + 外存精查
1. 工程系统的现实取舍
在 HBase 这样的系统中,目标并不是:
“一次判断就给出 100% 正确答案”
而是:
尽可能少地触发昂贵的磁盘 IO
这对应一个典型的系统事实:一次“无谓的外存查找”(读了一堆文件后发现不存在)可能比一次简单的内存计算(哈希 + bit 测试)贵几个数量级。Bloom Filter 的价值就在于把“高代价操作”变成“少发生的操作”。
2. Bloom Filter 的角色
Bloom Filter 的作用可以用一句话概括:
判断“是否有必要继续查”
-
• 如果 Bloom Filter 判断“不存在”
→ 一定不存在,直接跳过该 HFile -
• 如果判断“可能存在”
→ 再去磁盘做精确查找
这里有一个必须明确的语义:
-
• Bloom Filter 只有“误判存在”(False Positive),不会“误判不存在”(False Negative)。 -
• 因此它适合作为“过滤器”,不适合作为最终答案来源。(来源:Bloom, 1970)
Bloom Filter 的误判率与参数之间的关系常用近似公式表示( 为 bit 数, 为插入元素数, 为哈希函数个数):
-
• 误判率: -
• 最优 (使误判率最小):
(来源:Bloom, 1970;Mitzenmacher & Upfal, Probability and Computing)
3. HBase 的实际读路径(简化)
# 说明:读路径示意,展示“先过滤、再精查”的分工
Get(rowKey)
↓
MemStore(内存)
↓
BlockCache(内存)
↓
Bloom Filter(是否可能在该 HFile)
↓
HFile 精确查找(磁盘)
这里形成了一个非常经典的工程组合:
Bloom Filter(内存,快速过滤) + 外存精确结构(HFile)
补充两点,避免“过度简化”带来的误解:
-
• HBase 的 Bloom Filter 通常是“每个 HFile(StoreFile)一个”,用于判断“这个文件里是否可能有该 Key”,从而减少对不相关文件的访问。(来源:Apache HBase Reference Guide) -
• “HFile 精确查找”内部还包含索引与块读取:它不是线性扫描,而是借助多级索引定位到目标数据块,再读取并在块内二分/顺序验证。(来源:Apache HBase Reference Guide;《HBase: The Definitive Guide》)
八、Bitmap vs HBase:放在同一个抽象层下对比
现在,我们可以把两个方案放回到统一的 Membership Test 框架中:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这说明了一个非常重要的结论:
算法不是被问题本身选择的,而是被约束条件选择的。
对我们来说,真正应掌握的是“选择依据”:
-
• 当值域 可控且稠密时,Bitmap 的空间与 线性相关,但常数极小,查询极快。 -
• 当值域不可控且稀疏时,应让空间主要随集合大小 增长(例如 Bloom Filter、哈希索引、LSM 的索引层)。 -
• 当系统的主要瓶颈来自 IO 时,优先用内存结构去减少 IO 次数,往往比把单次查找做得“更精确”更重要。
九、统一抽象:Membership Test 的三类解法
从这个角度看,Membership Test 的常见解法可以抽象为三类:
-
1. Bitmap -
• 值域可控、Key 稠密 -
• 追求极致性能和确定性 -
2. Bloom Filter + 精确结构 -
• Key 不可控、规模巨大 -
• 工程系统的主流选择 -
3. 索引结构(B+Tree / LSM) -
• 需要绝对精确、可扩展 -
• 接受更高的访问成本
要建立"何时用哪类"的直觉,可以用一条简单但实用的判断线索:
-
• 你能否为“所有可能的 Key”分配一个稳定的编号且编号空间不大? -
• 能:优先考虑 Bitmap 或其压缩变体 -
• 不能:在“概率过滤 + 精确结构”与“纯精确索引”之间权衡
在 HBase 这类系统中,常见组合是:LSM(MemStore + 多个 HFile)作为精确结构,Bloom Filter 作为概率过滤层,用于降低读放大。
十、深入思考
这里有一些值得深入思考的问题,帮助你检验学习成果:
-
1. 如果 HBase 的 RowKey 是 32-bit 整数,Bitmap 是否可行? -
2. Bloom Filter 的误判率应该如何配置? -
3. 为什么 HBase 选择“每个 HFile 一个 Bloom Filter”? -
4. 在什么场景下,Bloom Filter 的收益会非常低?
尝试用本文的逻辑去分析这些问题:明确约束、列出指标、权衡取舍。不必追求唯一的标准答案,关键在于推导过程是否合理。
结语
从“40 亿个整数”到“HBase 的 RowKey”,问题的表象发生了巨大变化,但问题的本质从未改变:
如何在巨大集合中,低成本地回答“它在不在?”
理解这一点,比记住任何一个具体实现都更重要。
参考资料
-
• Burton H. Bloom. “Space/Time Trade-offs in Hash Coding with Allowable Errors.” Communications of the ACM, 1970. -
• Michael Mitzenmacher, Eli Upfal. Probability and Computing: Randomization and Probabilistic Techniques in Algorithms and Data Analysis. -
• Apache HBase Reference Guide(官方文档):https://hbase.apache.org/book.html -
• Lars George. HBase: The Definitive Guide(关于 HFile、读路径与 Bloom Filter 的工程描述)

