布隆過濾器使用bit數組映射關鍵字key,對於在一個超大的集合中判斷是否存在某個key能夠起到很好的效果。但是缺點很明顯:容易誤報。也就是本來不存在的key,可能告訴你它存在。
一、布隆過濾器
根據上圖來說明布隆過濾器的原理 :
1)布隆過濾有一個m位(這裏是10個)的bit數組(或者稱bitmap),bit數組初始化爲全0,並且有k個(這裏是3個)hash函數。
2)當我們分別存儲abc,hello,world三個關鍵字,然後分別對這些key進行hash(每個hash都執行),得出結果如上圖第三張表所示,然後對bit數組對應的bit位置1,置1後的結果如第二張表所示。
3)當我們對現有數據進行查詢的時候,比如查詢myworld,經過hash計算後得出0, 2,7,我們發現bit數組下標爲2時,這一位爲0,說明myworld一定不存在。再比如查詢myabc,經過hash計算後得出0, 5,7,經過bit數組對比,發現0,5,7都是1,但是myabc卻不存在,所以myabc屬於誤報!
總結:
1)布隆過濾器,對於不存在關鍵值能給出肯定結果,但是對於存在關鍵值只能說可能存在(存在一定誤報)。這個主要是因爲hash函數特徵所決定的。
2)對於那麼如何確定是否真的存在還是誤報呢?應該交給業務模塊再次進行確定查找。
3)如果保證誤報率最低呢?有一個公式:k = (m/n)*ln2。(k爲hash函數個數,m爲bit數組大小,n爲插入元素個數,ln2約等於0.69),當存在k個hash函數時誤報率最低。至於爲什麼是這個公式,大家可自行查詢相關資料。
二、leveldb中的布隆過濾器
2.1、構造方法
private:
size_t bits_per_key_;
size_t k_; /* 表示需要k_個hash函數 最大30個*/
public:
explicit BloomFilterPolicy(int bits_per_key)
: bits_per_key_(bits_per_key) {
// We intentionally round down to reduce probing cost a little bit
k_ = static_cast<size_t>(bits_per_key * 0.69); // 0.69 =~ ln(2)
if (k_ < 1) k_ = 1;
if (k_ > 30) k_ = 30;
}
2.2、CreateFilter
/**
* 生成過濾器
* @param keys 數組
* @param n 數組keys元素個數
* @param dst 保存bitmap
*/
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_; //表示bit數組中包含多少位
// 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;//8bit對齊
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 (int i = 0; i < n; i++) {
// Use double-hashing to generate a sequence of hash values.
// See analysis in [Kirsch,Mitzenmacher 2006].
// 對一個key進行兩次hash
uint32_t h = BloomHash(keys[i]);
const uint32_t delta = (h >> 17) | (h << 15); // Rotate right 17 bits
//對於一個hash值(這裏的hash可能很大數字),通過for循環模擬k個hash場景
for (size_t j = 0; j < k_; j++) {
const uint32_t bitpos = h % bits;//對bit數組求餘 獲取存儲的bit
array[bitpos/8] |= (1 << (bitpos % 8));
h += delta;
}
}
}
2.3、KeyMayMatch
/**
* 匹配key是存在當前過濾器中
* @param key 關鍵值
* @param bloom_filter 過濾器
* @return false 表示不存在
* true 表示key可能存在 是否真的存在還需要對key進行比較
*/
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;
}
三、布隆過濾器在leveldb中的應用
那麼布隆過濾器在leveldb應用到什麼方面呢?看過之前的博客,應該有印象的,在介紹存儲結構中一個的block叫做filter block。這部分處理就是布隆過濾器應用之處,可參考《leveldb深度剖析-存儲結構(2)》。下面具體看看是如何應用的。
3.1、開啓過濾策略
leveldb接口默認不開啓過濾策略(不是很清楚爲啥?),在創建對象Options時候指定了爲NULL(options.cc)。
Options::Options()
: comparator(BytewiseComparator()),
create_if_missing(false),
error_if_exists(false),
paranoid_checks(false),
env(Env::Default()),
info_log(NULL),
write_buffer_size(4<<20), // 4M
max_open_files(1000),
block_cache(NULL),
block_size(4096),
block_restart_interval(16), //重啓點間隔爲16
max_file_size(2<<20), //2M
compression(kSnappyCompression),
reuse_logs(false),
filter_policy(NULL) {
}
所以我們在創建Options對象需要單獨設置一下filter_policy對象纔可以。
3.2、添加key
std::string keys_; // Flattened key contents
std::vector<size_t> start_; // Starting index in keys_ of each key
/**
* 將key添加到FilterBlock中
* @param key InternalKey 不包含value
*/
void FilterBlockBuilder::AddKey(const Slice& key) {
Slice k = key;
start_.push_back(keys_.size());//記錄每個key的偏移位置
keys_.append(k.data(), k.size());//將key追加到字符串keys_末尾
}
3.3、Finish
Finish主要是格式化之前保存的key的,這裏就會涉及布隆過濾器的。
Slice FilterBlockBuilder::Finish() {
if (!start_.empty()) {
GenerateFilter();//生成過濾器數據
}
// Append array of per-filter offsets
// 經過上面GenerateFilter操作後result_保存操作結果
const uint32_t array_offset = result_.size();
for (size_t i = 0; i < filter_offsets_.size(); i++) {
PutFixed32(&result_, filter_offsets_[i]);
}
PutFixed32(&result_, array_offset);
result_.push_back(kFilterBaseLg); // Save encoding parameter in result
return Slice(result_);//返回後 寫入ldb文件
}
3.4、GenerateFilter
void FilterBlockBuilder::GenerateFilter() {
const size_t num_keys = start_.size();//start_保存這這個block存儲的key 多個
if (num_keys == 0) {
// Fast path if there are no keys for this filter
filter_offsets_.push_back(result_.size());
return;
}
// Make list of keys from flattened key structure
start_.push_back(keys_.size()); // Simplify length computation
tmp_keys_.resize(num_keys);
for (size_t i = 0; i < num_keys; i++) {
const char* base = keys_.data() + start_[i];
size_t length = start_[i+1] - start_[i];
tmp_keys_[i] = Slice(base, length);
}
// Generate filter for current set of keys and append to result_.
// 調用布隆過濾器生成數據
filter_offsets_.push_back(result_.size());
policy_->CreateFilter(&tmp_keys_[0], static_cast<int>(num_keys), &result_);
tmp_keys_.clear();
keys_.clear();
start_.clear();
}
3.5、查找匹配
查詢的時候需要創建FilterBlockReader對象纔可以進行查詢,調用FilterBlockReader的KeyMayMatch方法即可,具體如下:
bool FilterBlockReader::KeyMayMatch(uint64_t block_offset, const Slice& key) {
uint64_t index = block_offset >> base_lg_;
if (index < num_) {
uint32_t start = DecodeFixed32(offset_ + index*4);
uint32_t limit = DecodeFixed32(offset_ + index*4 + 4);
if (start <= limit && limit <= static_cast<size_t>(offset_ - data_)) {
Slice filter = Slice(data_ + start, limit - start);
return policy_->KeyMayMatch(key, filter);//InternalFilterPolicy::KeyMayMatch 布隆過濾器
} else if (start == limit) {
// Empty filters do not match any keys
return false;
}
}
return true; // Errors are treated as potential matches
}
這裏就很明顯是調用布隆過濾器進行查詢匹配。那麼上層業務是如何調到這裏呢?
/**
* 在table cache中查找k
* @param options 選項
* @param k 待查找key
* @param arg 回調函數參數
* @param saver 回調函數
*/
Status Table::InternalGet(const ReadOptions& options, const Slice& k,
void* arg,
void (*saver)(void*, const Slice&, const Slice&)) {
Status s;
Iterator* iiter = rep_->index_block->NewIterator(rep_->options.comparator);//這裏index_block類型是Block
iiter->Seek(k);//查找 在索引塊中查找 Block.cc
if (iiter->Valid()) {
Slice handle_value = iiter->value();
FilterBlockReader* filter = rep_->filter;
BlockHandle handle;
/**
* 如果存在過濾模塊 則先在過濾模塊中通過布隆過濾器進行確定 如果布隆過濾器不存在 那麼文件中一定不存在
* if 返回true表示不存在,if返回false表示可能存在 需要再次進行文件中查找即進入else中
*/
if (filter != NULL &&
handle.DecodeFrom(&handle_value).ok() &&
!filter->KeyMayMatch(handle.offset(), k)) {
// Not found
} else {//在數據塊中進行查找 iiter->value存儲的是BlockHandle
Iterator* block_iter = BlockReader(this, options, iiter->value());
block_iter->Seek(k);
if (block_iter->Valid()) {//表示找到
(*saver)(arg, block_iter->key(), block_iter->value());
}
s = block_iter->status();
delete block_iter;
}
}
if (s.ok()) {
s = iiter->status();
}
delete iiter;
return s;
}
四、總結
其實布隆過濾器思想比較簡單,我們需要了解相關特性即可。