跳转至

前缀搜索接口

如果你的 DB 或者列族的 options.prefix_extractor 选项有被声明,那么 rocksdb 就会在一个“前缀搜索”模式,具体会在下面解释。使用的例子如下:

Options options;

// <---- Enable some features supporting prefix extraction
options.prefix_extractor.reset(NewFixedPrefixTransform(3));

DB* db;
Status s = DB::Open(options, "/tmp/rocksdb",  &db);

......

auto iter = db->NewIterator(ReadOptions());
iter->Seek("foobar"); // Seek inside prefix "foo"
iter->Next(); // Find next key-value pair inside prefix "foo"

options.prefix_extractor 是一个 SliceTransform 类型的共享指针。通过调用 SliceTransform.Transform(),我们可以我们从一个 Slice 里面提取出一个子串,用来代表该 Slice,通常是前缀部分。在这个 wiki 页面,我们使用“前缀”来指代 options.prefix_extractor.Transform() 对一个 key 处理后的输出。你可以通过调用 NewFixedPrefixTransform(prefix_len),获得一个使用固定长度的前缀转换器,或者你可以根据需要实现自己的转换器,然后传给 options.prefix_extractor。

当 options.prefix_extractor.不是 nullptr,迭代器不能保证所有 key 都是按顺序迭代的,只能保证相同前缀的 key 是按顺序迭代的。当调用 Iterator.Seek(lookup_key) 时,RocksDB 会从 lookup_key 里面提取前缀。与全排序模式不同,如果数据库里面有一个或者多个 key 满足这个前缀,RocksDB 会把迭代器放在前缀相同,或者更大的 key 上。如果没有前缀等于或者大于 lookup_key 的前缀,或者在调用几次 Next 之后,根据 ReadOptions.prefix_same_as_start 是否为 true,我们处理完了相同前缀的所有 key,我们可能会反回 Valid()=false,或者是任何一个大于前面的 key 的 key。从 4.11 之后,我们支持前缀模式下使用 Prev,但是仅限于迭代器仍旧在该前缀的所有键值范围内的时候。Prev 的输出在迭代器已经超出范围的时候不保证正确。

当前缀模式被打开的时候,RocksDB 会为了快速定位相同前缀的 key 或者排除不存在的前缀,自由组织数据,或者构建搜索数据。这里有些为前缀模式进行优化的支持:数据块和 memtable 的 prefix bloom,基于哈希的 memtable,还有平表格式。一个示范设定:

Options options;

// Enable prefix bloom for mem tables
options.prefix_extractor.reset(NewFixedPrefixTransform(3));
options.memtable_prefix_bloom_bits = 100000000;
options.memtable_prefix_bloom_probes = 6;

// Enable prefix bloom for SST files
BlockBasedTableOptions table_options;
table_options.filter_policy.reset(NewBloomFilterPolicy(10, true));
options.table_factory.reset(NewBlockBasedTableFactory(table_options));

DB* db;
Status s = DB::Open(options, "/tmp/rocksdb",  &db);

......

auto iter = db->NewIterator(ReadOptions());
iter->Seek("foobar"); // Seek inside prefix "foo"

从 3.5 版本开始,我们支持通过一个配置,使得 rocksdb 即使在前缀模式,也能使用全顺序。调用 NewIterator 的时候,打开 ReadOption.total_order_seek=true 就可以打开这个功能。

ReadOptions read_options;
read_options.total_order_seek = true;
auto iter = db->NewIterator(read_options);
Slice key = "foobar";
iter->Seek(key);  // Seek "foobar" in total order

这个模式下,性能可能会变差。请注意,并不是所有的前缀搜索实现都支持这个功能。例如,平表的实现就不支持,所以如果你尝试这么使用,你会看到一个错误码返回。基于哈希的 mentable 会在使用这个功能的时候做一次昂贵的在线排序。prefix bloom 和使用哈希索引的基于块的表是支持这个模式的。

限制

SeekToLast 对前缀索引不友好。SeekToFirst 只对部分配置有支持。如果你的迭代器需要使用这两个操作,你应该使用全排序模式。

一个常见的使用前缀索引的 bug 是使用逆序迭代前缀模式。这个还没有支持。如果你需要经常使用逆序迭代,你可以重新对数据进行排序,然后把前缀迭代器的顺序反过来。你可以通过自己实现一个比较器,或者使用其他方法编码你的 key

API 变化(2.8->3.0)

这一节,我们会说明从 2.8 版本到 3.0 版本的 API 变化

修改前

全排序搜索

这就是你认为的传统的索引行为。搜索会在一个全排序的 key 空间进行,把迭代器定位到第一个大于或者等于你搜索的 key 的位置。

auto iter = db->NewIterator(ReadOptions());
Slice key = "foo_bar";
iter->Seek(key);

并不是所有表组织方式都支持全排序搜索。例如,新引入的平表格式,除非使用全排序模式打开(Options.prefix_extractor == nullptr),否则就只支持基于前缀的 seek。

使用 ReadOptions.prefix

这是最不灵活的搜索方式。创建迭代器的时候需要支持前缀。

Slice prefix = "foo";
ReadOptions ro;
ro.prefix = &prefix;
auto iter = db->NewIterator(ro);
Slice key = "foo_bar"
iter->Seek(key);

Options.prefix_extractor 需要提前设置。Seek 调用会被 ReadOptions 提供的前缀限制,这意味着,如果你希望搜索一个新的前缀,你需要重新创建迭代器。这种方式的好处是,不相关的文件可以在创建迭代器的时候被过滤掉。所以如果你希望搜索同一个前缀的不同 key,他的表现会比较好。然而,我们认为这个使用方式非常少见。

使用 ReadOptions.prefix_seek

这个模式比 ReadOption.prefix 更加灵活。创建迭代器的时候没有预过滤。这样,通过一个迭代器就可以用于搜索不同的 key 和前缀了。

ReadOptions ro;
ro.prefix_seek = true;
auto iter = db->NewIterator(ro);
Slice key = "foo_bar";
iter->Seek(key);

与 ReadOptions.prefix 一样,Options.prefix_extractor 是前置条件。

修改的内容

很显然,三种搜索模式让人感到混乱:

  • 一个模式要求另一个选项被设置(例如 Options.prefix_extractor)
  • 对于我们的用户而言,后面两种模式哪一个在不同的场景下比较合适,不明显。

这个修改希望解决这个问题,并且把事情变得简单明了:如果 Options.prefix_extractor 被设置,seek 默认使用前缀模式,反之亦然。动机很简单:如果 Options.prefix_extractor 存在,很显然,数据可以被切片,然后前缀搜索就自然匹配这种场景。使用方式就变的统一了:

auto iter = db->NewIterator(ReadOptions());
Slice key = "foo_bar";
iter->Seek(key);

迁移到新的用法

迁移到新的用法很简单。去除 Options.prefix 或者 Options.prefix_seek 的赋值,因为他们已经被弃用了。现在,直接搜索你的 key 或者前缀就好了。因为 Next 会穿过上限,走到不同的前缀去,你可能需要检查结束状态:

    auto iter = DB::NewIterator(ReadOptions());
    for (iter.Seek(prefix); iter.Valid() && iter.key().starts_with(prefix); iter.Next()) {
       // do something
    }