MiniBase 读后感之布隆过滤器相关

125 阅读5分钟

概述

前些日子在读《Hbase 原理与实践》,书中作者为了给大家介绍Hbase的基本核心原理,设计编写了小(简)型(单)的一个嵌入式的 KV 存储引擎,叫做 MiniBase,代码连接:github.com/openinx/min…。这篇文章是关于 MiniBase 中的布隆过滤器的读后感,代码文件是项目中的 org.apache.minibase.BloomFilter 类。

理论基础

布隆过滤器这个东西大家应该不陌生,算是技术面试出镜频率比较高的一个题目了。布隆过滤器底层存的是一个bit 的数组,假设我们的布隆过滤器长度为 m,那么有如下逻辑

插入元素 X

  1. 对 X 进行 K 次 Hash
  2. 每次 Hash 的结果对 m 取模,得到每次 Hash 的 index
  3. 然后设 布隆过滤器的 相应的 index 为 1

判断X在不在布隆过滤器中

  1. 对 X 进行 K 次 Hash
  2. 每次 Hash 的结果对 m 取模,得到每次 Hash  的 index
  3. 如果每个 index 存的值都为 1 ,则元素可能存在。如果有某一位的值不唯一,则元素肯定不在过滤器里。

逻辑是比较简单的,优缺点也同样很明显。优点是空间复杂度贼低。缺点是有误判的概率。 假设 hash 次数为 K,布隆过滤器长度为 m,期望插入的元素有 n 个,那么根据公式,最优解满足这么一个关系

K=mnln2K=\frac{m}{n}ln2

假设希望的容错率为 p,则

m=nlnp(ln2)2m=-\frac{n*lnp}{(ln2)^2}

想知道公式是如何推导的可以看这里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,我们假设

bitLen=8x+y;y[0,7]bitLen = 8x+y; \quad y\in [0,7] \\

就很好理解了,当 y = 0 的时候 bitLen + 7 = 8x +7, 当 y 取 1~7 的时候

bitLen=8(x+1)+z;z[0,6]bitLen = 8(x+1) + z; \quad z\in [0,6] \\

然后再结合上面的 除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)也发现了差不多的代码,也是用来向上取整的,代码如下所示~ image.png

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 的实现的。 image.png

image.png

其他方法

由于只是一个学习的Demo,这里并没有 put 的方法,这块的逻辑跟生成的时候的那个逻辑大概是一样的。

Hbase 中的布隆过滤器

Hbase 中的布隆过滤器的结果大概如下所示 image.png BloomFilterBase 定义了布隆过滤器顶层接口, 然后 BloomFilter 接口的核心是定义了一个 contains 方法,然后 BloomFilterWriter 的核心是定义两个 getWriter 方法和一个 add 方法。 在 StoreFile 进行 append 的时候(org.apache.hadoop.hbase.regionserver.StoreFile.Writer#append),会添加相应的数据到布隆过滤器里,时序图大概如下所示

image.png

其中的add方法大概如下所示,可以看到,跟我们的 minibase 其实也是差不多的,也是执行了 n 次的 hash,然后将相应的位数置为1 。只不过他的hash函数是可以配置的,hashCount 也是也是可以配置的罢了。

image.png

参考文档

《Hbase 原理与实践》