Leveldb源碼解析之Bloom Filter

Bloom Filter,即布隆過濾器,是一種空間效率很高的隨機數據結構。算法

原理:開闢m個bit位數組的空間,並所有置零,使用k個哈希函數將元素映射到數組中,相應位置1.以下圖,元素K經過哈希函數h1,h2,h3在數組上置1。數組

LevelDB中加入bloom filter的支持。目前針對一次查詢,LevelDB可能須要在每一個level上進行一次磁盤隨機訪問。經過使用bloom filter能夠大大減小所須要的磁盤I/O操做。好比,假設調用者正在查找一個值爲"Foo"的key,LevelDB會從每一個level下選擇相應的SSTable文件(那些range包含了該key的文件),以後會在這些SSTable文件上進行隨機讀。若是每一個SSTable都有一個對應的bloom filter,那麼查找時就能夠很容易地經過檢查bloom filter跳過那些不包含該key的SSTable文件。數據結構

在leveldb的實現中,Name()返回"leveldb.BuiltinBloomFilter",所以metaindex block 中的key就是"filter.leveldb.BuiltinBloomFilter"。Leveldb使用了double hashing來模擬多個hash函數,固然這裏不是用來解決衝突的。ide

和線性再探測(linearprobing)同樣,Double hashing從一個hash值開始,重複向前迭代,直到解決衝突或者搜索完hash表。不一樣的是,double hashing使用的是另一個hash函數,而不是固定的步長。函數

給定兩個獨立的hash函數h1和h2,對於hash表T和值k,第i次迭代計算出的位置就是:h(i, k) = (h1(k) + i*h2(k)) mod |T|。ui

對此,Leveldb選擇的hash函數是:spa

Gi(x)=H1(x)+iH2(x)code

H2(x)=(H1(x)>>17) | (H1(x)<<15)blog

H1是一個基本的hash函數,H2是由H1循環右移獲得的,Gi(x)就是第i次循環獲得的hash值。【理論分析可參考論文Kirsch,Mitzenmacher2006】ci

說明bloomfliter 的原理以後來看下Leveldb是怎麼來實現它的, 使用CreateFilter建立一個bloomfliter

virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const {
    // Compute bloom filter size (in both bits and bytes)
    size_t bits = n * bits_per_key_;

    // For small n, we can see a very high false positive rate.  Fix it
    // by enforcing a minimum bloom filter length.
    if (bits < 64) bits = 64;

    size_t bytes = (bits + 7) / 8;
    bits = bytes * 8;

    const size_t init_size = dst->size();
    dst->resize(init_size + bytes, 0);
    dst->push_back(static_cast<char>(k_));  // Remember # of probes in filter
    char* array = &(*dst)[init_size];
    for (size_t i = 0; i < n; i++) {
      // Use double-hashing to generate a sequence of hash values.
      // See analysis in [Kirsch,Mitzenmacher 2006].
      uint32_t h = BloomHash(keys[i]);
      const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits
      for (size_t j = 0; j < k_; j++) {
        const uint32_t bitpos = h % bits;
        array[bitpos/8] |= (1 << (bitpos % 8));
        h += delta;
      }
    }
  }

函數KeyMayMatch ()在讀取數據的時候調用

virtual bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const {
    const size_t len = bloom_filter.size();
    if (len < 2) return false;

    const char* array = bloom_filter.data();
    const size_t bits = (len - 1) * 8;

    // Use the encoded k so that we can read filters generated by
    // bloom filters created using different parameters.
    const size_t k = array[len-1];
    if (k > 30) {
      // Reserved for potentially new encodings for short bloom filters.
      // Consider it a match.
      return true;
    }

    uint32_t h = BloomHash(key);
    const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits
    for (size_t j = 0; j < k; j++) {
      const uint32_t bitpos = h % bits;
      if ((array[bitpos/8] & (1 << (bitpos % 8))) == 0) return false;
      h += delta;
    }
    return true;
  }

計算key的hash值,重複計算階段的步驟,循環計算k個hash值,只要有一個結果對應的bit位爲0,就認爲不匹配,不然認爲匹配。

錯誤率估計

但Bloom Filter的這種高效是有必定代價的:在判斷一個元素是否屬於某個集合時,有可能會把不屬於這個集合的元素誤認爲屬於這個集合(false positive)。所以,Bloom Filter不適合那些"零錯誤"的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter經過極少的錯誤換取了存儲空間的極大節省。

吳軍的數學之美中對Bloom Filter的錯誤率有詳細解釋。

假設 Hash 函數以等機率條件選擇並設置 Bit Array 中的某一位,m 是該位數組的大小,k 是 Hash 函數的個數,那麼位數組中某一特定的位在進行元素插入時的 Hash 操做中沒有被置位的機率是:

那麼在全部 k 次 Hash 操做後該位都沒有被置 "1" 的機率是:

若是咱們插入了 n 個元素,那麼某一位仍然爲 "0" 的機率是:

於是該位爲 "1"的機率是:

如今檢測某一元素是否在該集合中。標明某個元素是否在集合中所需的 k 個位置都按照如上的方法設置爲 "1",可是該方法可能會使算法錯誤的認爲某一本來不在集合中的元素卻被檢測爲在該集合中(False Positives),該機率由如下公式肯定:

相關文章
相關標籤/搜索