概述
前些日子在读《Hbase 原理与实践》,书中作者为了给大家介绍Hbase的基本核心原理,设计编写了小(简)型(单)的一个嵌入式的 KV 存储引擎,叫做 MiniBase,代码连接:github.com/openinx/min…。这篇文章是关于 MiniBase 中的布隆过滤器的读后感,代码文件是项目中的 org.apache.minibase.BloomFilter 类。
理论基础
布隆过滤器这个东西大家应该不陌生,算是技术面试出镜频率比较高的一个题目了。布隆过滤器底层存的是一个bit 的数组,假设我们的布隆过滤器长度为 m,那么有如下逻辑
插入元素 X
- 对 X 进行 K 次 Hash
- 每次 Hash 的结果对 m 取模,得到每次 Hash 的 index
- 然后设 布隆过滤器的 相应的 index 为 1
判断X在不在布隆过滤器中
- 对 X 进行 K 次 Hash
- 每次 Hash 的结果对 m 取模,得到每次 Hash 的 index
- 如果每个 index 存的值都为 1 ,则元素可能存在。如果有某一位的值不唯一,则元素肯定不在过滤器里。
逻辑是比较简单的,优缺点也同样很明显。优点是空间复杂度贼低。缺点是有误判的概率。 假设 hash 次数为 K,布隆过滤器长度为 m,期望插入的元素有 n 个,那么根据公式,最优解满足这么一个关系
假设希望的容错率为 p,则
想知道公式是如何推导的可以看这里en.wikipedia.org/wiki/Bloom_…(我反正是看不懂的)嫌公式太复杂不想算的同学也可以直接根据 krisives.github.io/bloom-calcu… 这个地址在线计算。
MiniBase 中的布隆过滤器
MiniBase 中的布隆过滤器只有一个构造方法,一个初始化的 generate 方法,一个判断 key 在不在过滤器里的 contains 方法。整个类的结构大概如下所示
代码也十分的简单,简单到甚至我可以直接把他们全部贴在下面(跟原码略微有些不同,我加了一些注释)
package org.apache.minibase;
public class BloomFilter {
/**
* hash 的次数
*/
private int k;
/**
* key 的每一位的长度
*/
private int bitsPerKey;
/**
* 布隆过滤器的长度
*/
private int bitLen;
/**
* 布隆过滤器的 byte 数组
*/
private byte[] result;
public BloomFilter(int k, int bitsPerKey) {
this.k = k;
this.bitsPerKey = bitsPerKey;
}
/**
* 生成布隆过滤器
*/
public byte[] generate(byte[][] keys) {
assert keys != null;
bitLen = keys.length * bitsPerKey;
bitLen = ((bitLen + 7) / 8) << 3; // align the bitLen.
bitLen = bitLen < 64 ? 64 : bitLen;
result = new byte[bitLen >> 3];
for (int i = 0; i < keys.length; i++) {
assert keys[i] != null;
int h = Bytes.hash(keys[i]);
for (int t = 0; t < k; t++) {
int idx = (h % bitLen + bitLen) % bitLen;
result[idx / 8] |= (1 << (idx % 8));
int delta = (h >> 17) | (h << 15);
h += delta;
}
}
return result;
}
/**
* 判断一个元素是否在布隆过滤器中
*/
public boolean contains(byte[] key) {
assert result != null;
int h = Bytes.hash(key);
for (int t = 0; t < k; t++) {
int idx = (h % bitLen + bitLen) % bitLen;
if ((result[idx / 8] & (1 << (idx % 8))) == 0) {
return false;
}
int delta = (h >> 17) | (h << 15);
h += delta;
}
return true;
}
}
关于这段代码,有几个有意思的点我觉得可以拿出来讲一下:
向上取整
代码中有这么一段
bitLen = ((bitLen + 7) / 8) << 3; // align the bitLen.
这个很有意思,作者给的注释是 align the bitLen,结合上下文,我们可以知道他这个为了对齐 bit数组的长度。但是这个操作怎么就能对齐长度了呢? 其实很简单,首先,我们知道,左移三位是乘以 2^3 ,也就是乘以 8 的意思,同理,这里的 除以 8 也可以用 右移三位来代替(作者这里是直接写的除以8)。然后我们看前面的 bitLen + 7,我们假设
就很好理解了,当 y = 0 的时候 bitLen + 7 = 8x +7, 当 y 取 1~7 的时候
然后再结合上面的 除8乘8,相当于就是一个向上取整的操作了。跟下面的这个代码的效果大概是一样的
public int ceil(int bitLen) {
if (bitLen % 8 == 0) {
return bitLen / 8;
} else {
return (bitLen / 8) + 1;
}
}
又学到了一个装X的小技巧,赞.jpg
ps. 我在 hbase 的源码里(org.apache.hadoop.hbase.util.ClassSize.MemoryLayout#align)也发现了差不多的代码,也是用来向上取整的,代码如下所示~
Hash 算法
MiniBase 里的 Hash 算法比较简单高效,用的自己定义的 Bytes 里 hash 方法,代码如下
public static int hash(byte[] key) {
if (key == null) return 0;
int h = 1;
for (int i = 0; i < key.length; i++) {
h = (h << 5) + h + key[i];
}
return h;
}
这个方法跟 java.util.Array#hashcode 方法长的挺像的,就学习而言的的话,我觉得甚至可以再简单一点,直接用 java.util.Array#hashcode 也问题不大,java.util.Array#hashcode 的代码如下:
package java.util;
public class Arrays {
// 省略其他方法
public static int hashCode(byte a[]) {
if (a == null)
return 0;
int result = 1;
for (byte element : a)
result = 31 * result + element;
return result;
}
}
但是如果我们要线上使用的话,一般是选择一些经过证明的,性能高的,散列性好的 hash 算法,比如 murmurHash 啥的。我翻了翻 Hbase 的源码,如下图所示,他应该是提供了 JenkinsHash,murmurHash,murmurHash3 的实现的。
其他方法
由于只是一个学习的Demo,这里并没有 put 的方法,这块的逻辑跟生成的时候的那个逻辑大概是一样的。
Hbase 中的布隆过滤器
Hbase 中的布隆过滤器的结果大概如下所示
BloomFilterBase 定义了布隆过滤器顶层接口, 然后 BloomFilter 接口的核心是定义了一个 contains 方法,然后 BloomFilterWriter 的核心是定义两个 getWriter 方法和一个 add 方法。
在 StoreFile 进行 append 的时候(org.apache.hadoop.hbase.regionserver.StoreFile.Writer#append),会添加相应的数据到布隆过滤器里,时序图大概如下所示
其中的add方法大概如下所示,可以看到,跟我们的 minibase 其实也是差不多的,也是执行了 n 次的 hash,然后将相应的位数置为1 。只不过他的hash函数是可以配置的,hashCount 也是也是可以配置的罢了。
参考文档
《Hbase 原理与实践》