LSM-Tree - LevelDb布隆过滤器(二)

简介: LSM-Tree - LevelDb布隆过滤器

LSM-Tree - LevelDb布隆过滤器(一)https://developer.aliyun.com/article/1394986


其他内容

单元测试

作者编写的单元测试可以更为直观的看到具体效果,路径为:/leveldb-main/util/bloom_test.cc


// Copyright (c) 2012 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "gtest/gtest.h"
#include "leveldb/filter_policy.h"
#include "util/coding.h"
#include "util/logging.h"
#include "util/testutil.h"
namespace leveldb {
  static const int kVerbose = 1;
  static Slice Key(int i, char* buffer) {
  EncodeFixed32(buffer, i);
    return Slice(buffer, sizeof(uint32_t));
  }
class BloomTest : public testing::Test {
  public:
  BloomTest() : policy_(NewBloomFilterPolicy(10)) {}
  ~BloomTest() { delete policy_; }
  void Reset() {
    keys_.clear();
    filter_.clear();
  }
  void Add(const Slice& s) { keys_.push_back(s.ToString()); }
  // 
  void Build() {
    std::vector<Slice> key_Slices;
    for (size_t i = 0; i < keys_.size(); i++) {
      key_Slices.push_back(Slice(keys_[i]));
    }
    filter_.clear();
    policy_->CreateFilter(&key_Slices[0], static_cast<int>(key_Slices.size()),
    &filter_);
    keys_.clear();
    if (kVerbose >= 2) DumpFilter();
  }
  size_t FilterSize() const { return filter_.size(); }
  // 打印
  void DumpFilter() {
    std::fprintf(stderr, "F(");
    for (size_t i = 0; i + 1 < filter_.size(); i++) {
      const unsigned int c = static_cast<unsigned int>(filter_[i]);
      for (int j = 0; j < 8; j++) {
      std::fprintf(stderr, "%c", (c & (1 << j)) ? '1' : '.');
    }
    }
    std::fprintf(stderr, ")\n");
  }
  // 匹配
  bool Matches(const Slice& s) {
    if (!keys_.empty()) {
      Build();
    }
    return policy_->KeyMayMatch(s, filter_);
  }
  double FalsePositiveRate() {
    char buffer[sizeof(int)];
      int result = 0;
      for (int i = 0; i < 10000; i++) {
      if (Matches(Key(i + 1000000000, buffer))) {
      result++;
    }
  }
  return result / 10000.0;
}
private:
  const FilterPolicy* policy_;
  std::string filter_;
  std::vector<std::string> keys_;
};
TEST_F(BloomTest, EmptyFilter) {
  ASSERT_TRUE(!Matches("hello"));
  ASSERT_TRUE(!Matches("world"));
}
TEST_F(BloomTest, Small) {
  Add("hello");
  Add("world");
  ASSERT_TRUE(Matches("hello"));
  ASSERT_TRUE(Matches("world"));
  ASSERT_TRUE(!Matches("x"));
  ASSERT_TRUE(!Matches("foo"));
}
static int NextLength(int length) {
  if (length < 10) {
    length += 1;
  } else if (length < 100) {
    length += 10;
  } else if (length < 1000) {
    length += 100;
  } else {
    length += 1000;
  }
  return length;
}
TEST_F(BloomTest, VaryingLengths) {
  char buffer[sizeof(int)];
  // Count number of filters that significantly exceed the false positive rate
  int mediocre_filters = 0;
  int good_filters = 0;
  for (int length = 1; length <= 10000; length = NextLength(length)) {
  Reset();
  for (int i = 0; i < length; i++) {
  Add(Key(i, buffer));
}
  Build();
ASSERT_LE(FilterSize(), static_cast<size_t>((length * 10 / 8) + 40))
<< length;
// All added keys must match
for (int i = 0; i < length; i++) {
  ASSERT_TRUE(Matches(Key(i, buffer)))
  << "Length " << length << "; key " << i;
}
// Check false positive rate
double rate = FalsePositiveRate();
  if (kVerbose >= 1) {
    std::fprintf(stderr,
    "False positives: %5.2f%% @ length = %6d ; bytes = %6d\n",
    rate * 100.0, length, static_cast<int>(FilterSize()));
  }
  ASSERT_LE(rate, 0.02); // Must not be over 2%
  if (rate > 0.0125)
    mediocre_filters++; // Allowed, but not too often
  else
    good_filters++;
}
if (kVerbose >= 1) {
  std::fprintf(stderr, "Filters: %d good, %d mediocre\n", good_filters,
  mediocre_filters);
}
  ASSERT_LE(mediocre_filters, good_filters / 5);
}
// Different bits-per-byte
} // namespace leveldb

c++语法

补充: 个人并没有学过C++,所以这部分补充一些不理解的关键字和语法含义。

explicit

C++ 参考手册如下解释:

  • explicit修饰的构造函数不能被隐式调用。
  • 禁止类对象之间的隐式转换。

这篇文章我们关注的就是第一点:构造函数被explicit修饰后, 就不能再被隐式调用了

这里用了网上相关的案例:


#include<cstring>
#include<string>
#include<iostream>
class Explicit
{
    private:
    public:
        Explicit(int size)
        {
            std::cout << " the size is " << size << std::endl;
        }
        Explicit(const char* str)
        {
            std::string _str = str;
            std::cout << " the str is " << _str << std::endl;
        }
        Explicit(const Explicit& ins)
        {
            std::cout << " The Explicit is ins" << std::endl;
        }
        Explicit(int a,int b)
        {
            std::cout << " the a is " << a  << " the b is " << b << std::endl;
        }
};
int main()
{
    Explicit test0(15);
    Explicit test1 = 10;// 隐式调用Explicit(int size)
    Explicit test2("RIGHTRIGHT");
    Explicit test3 = "BUGBUGBUG";// 隐式调用Explicit(const char* str)
    Explicit test4(1, 10);
    Explicit test5 = test1;
}

虽然程序没有错误,但是把一个int类型或者const char*类型的变量赋值给Explicit类型的变量看起来总归不是很好,并且一旦使用出错很难排查,所以这时候构造函数被explicit修饰后, 就不能再被隐式调用了,添加关键字之后的效果不演示了,加上之后整个程序是无法通过编译的。

吐槽:很神经病的东西,在过去的版本中使用隐式调用提高编码效率,结果后面发现坑太大挖坑自己填。

resize函数

直接看下面的案例更容易理解:

  • myvector.resize(5); 将原来有10个数的vector数组,调整为5个数的长度,多余的数删掉,释放内存。 5 < 10 减小数组长度
  • myvector.resize(8,100); 将5个数长度的vector数组的长度调整为8,不够的数用100来填补,即增加了3个100。 8 > 5 增大数组长度,指定填充元素
  • myvector.resize(12); 将8个数长度的vector数组的长度调整为12,用0默认填补,即增加了4个0。 12 > 8 增大数组长度,未指定填充元素

数学推导

推导部分给想要更加深入了解的人,可以直接记住上面的结论,看不懂也没关系。

下面的大部分内容来自论文根据 LNCS 4168 - Less Hashing, Same Performance: Building a Better Bloom Filter (harvard.edu) 翻译。

false position:误判率,也就是随着哈希和为1的bit位增加导致的误判率上升。

根据 bloom filter 的组成,对一个指定的 bit,其被设置为0、1的概率分别为:


P(1) = 1/m
P(0) = 1 - 1/m

k 个 hash 函数,该 bit 设置为 0 的概率为:


P'(0) = P(0) ** k = (1 - 1/m) ** k

再经过 n 个 key,该 bit 设置为 0 的概率为


P''(0) = P'(0) ** n = (1 - 1/m) ** kn

根据自然对数e的公式:

image.png

我们可以近似计算前面的P''(0)

image.png

关于自然对数 e 的值,可以看下面的内容:

image.png

当检测某个实际不存在的 key 时,满足条件:

其对应的 k 个 bit 恰好都设置为了1,此时即 false positive 的场景。

概率为:

image.png

问题是,怎么最小化 false_positive 呢?

为了简化描述,先定义 p (即P''(0):某个 bit 设置为0的概率):

image.png

根据公式推导:

image.png

底数是 e,为固定值,那么最小化 false_positive_rate 即为最小化指数

image.png

根据之前的计算结果,我们可以做下面的变形:

image.png

最终得到结果 g:

image.png

根据对称性,当 p = 1/2 时,f 取得最小值。

image.png

此时k、f最小值为:

image.png

最终的推导结果:

image.png

考虑到 p 为设置为0的概率,因此可以认为 m 有一半设置为1,一半设置为0时,误判率最低

false position和m/n、k 的组合关系表例子可以下面的截图:

image.png

总结

Bloom Filter 通常用于快速判断某个元素是否在集合中。其本质上容忍一定的错误率来换取时空的高效性。

对于LevelDB的意义:在哈希表的基础上省下了冲突处理部分,LevelDB 在实现时使用了某种优化:利用一个哈希函数来达到近似 k 个哈希函数的效果。这样做实现了高效的并发写入同时不会牺牲过多的性能。

LevelDB除开哈希函数和针对并发写入的优化部分之外,其他部分都非常贴合布隆过滤器的理论基础,也是优秀的学习案例,作为C++版本的过滤器生产案例应用也是一个不错的参考范本。

最后,有问题找布隆准没错。

参考资料

下面的资料绝对能让你吃透布隆过滤器。

公众号请“阅读原文”获取访问链接。

相关文章
|
存储 缓存 算法
数据库必知词汇:布隆过滤器(Bloom Filter)
布隆过滤器(Bloom Filter)是由Burton Bloom 在1970年提出的,其后在P2P上得到了广泛的应用。一个空的布隆过滤器是一个m位的位数组,所有位的值都为0。定义了k个不同的符合均匀随机分布的哈希函数,每个函数把集合元素映射到位数组的m位中的某一位。Bloom filter算法可用来查询某一数据是否在某一数据集合中。其优点是查询效率高、可节省空间。但其缺点是会存在一定的错误。因此Bloom filter 算法仅仅能应用于那些同意有一定错误的场合。可使用Bloom filter 算法的场合包含字典软件、分布式缓存、P2P网络和资源路由等等。
1387 0
|
7月前
|
存储 算法 Serverless
了解数据库中的布隆过滤器原理
【5月更文挑战第17天】本文介绍布隆过滤器是一种空间高效的的数据结构,用于判断一个元素是否可能在一个集合中。它包含一个位图和多个哈希函数。
72 1
了解数据库中的布隆过滤器原理
|
7月前
|
NoSQL Redis 数据库
LSM-Tree - LevelDb Skiplist跳表
LSM-Tree - LevelDb Skiplist跳表
86 0
|
7月前
|
存储 缓存 NoSQL
LSM-Tree - LevelDb了解和实现
LSM-Tree - LevelDb了解和实现
74 0
|
7月前
|
存储 NoSQL 安全
LSM-Tree - LevelDb布隆过滤器(一)
LSM-Tree - LevelDb布隆过滤器
112 0
LSM-Tree - LevelDb布隆过滤器(一)
|
7月前
|
NoSQL Redis 数据库
LSM-Tree - LevelDb Skiplist跳表
LSM-Tree - LevelDb Skiplist跳表
62 0
LSM-Tree - LevelDb Skiplist跳表
|
7月前
|
存储 缓存 NoSQL
LSM-Tree - LevelDb了解和实现
LSM-Tree - LevelDb了解和实现
65 0
|
NoSQL 算法 分布式数据库
LSM-Tree - LevelDb Skiplist跳表(上)
LSM-Tree - LevelDb Skiplist跳表(上)
302 0
|
存储 缓存 NoSQL
LSM-Tree - LevelDb了解和实现
LSM-Tree - LevelDb了解和实现
312 0
|
消息中间件 存储 NoSQL
LSM-Tree - LevelDb Skiplist跳表(下)
LSM-Tree - LevelDb Skiplist跳表(下)
289 0