rocksdb

RocksDB 基础操作教程

打开一个数据库

#include <cassert>
#include "rocksdb/db.h"

rocksdb::DB* db;
rocksdb::Options options;
options.create_if_missing = true;
options.error_if_exists = true;
rocksdb::Status status = rocksdb::DB::Open(options, "/tmp/testdb", &db);
assert(status.ok());
// ...

通过 options 制定一些属性,然后用 rocksdb::DB::Open打开。RocksDB 会把使用的配置保存在 OPTIONS-xxxx 文件中。

注意上面返回的那个 status 变量,在 RocksDB 中所有会遇到错误的函数都会返回这个变量,可以用来检查有没有出错。

rocksdb::Status s = ...;
if (!s.ok()) cerr << s.ToString() << endl;

关闭数据库,只需要简单得把指针释放就可以了:delete db.

读写数据库

基本的 Put, Get, Delete:

std::string value;
rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &value);
if (s.ok()) s = db->Put(rocksdb::WriteOptions(), key2, value);
if (s.ok()) s = db->Delete(rocksdb::WriteOptions(), key1);

注意其中每次都检查了操作是否成功。

每次 Get 操作都会导致至少一次的 memcpy, 如果不想要这种浪费的话,可以使用 PinnableSlice 操作。

PinnableSlice pinnable_val;
rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &pinnable_val);

原子操作

使用WriteBatch来构成一个原子性的操作。什么是原子性操作总不用多说吧。原子操作不仅保证了原子性,而且一般来说对性能也有帮助。

#include "rocksdb/write_batch.h"
...
std::string value;
rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &value);
if (s.ok()) {
  rocksdb::WriteBatch batch;
  batch.Delete(key1);
  batch.Put(key2, value);
  s = db->Write(rocksdb::WriteOptions(), &batch);
}

同步与异步读写

这块没看明白。

默认的是异步读写,如果使用了sync这个标志,那么就是同步读写了

rocksdb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ...);

异步读写经常会比同步读写快上 1000 倍,但是当机器 down 掉的时候,会丢失最后的几个写入。不过通常来说,可以认为异步读写安全性也是够的。

除了可以使用异步读写以外,还可以使用 WriteBatch 来批量读写。

并发

一个数据库同时只能被一个进程读写。但是一个 db 实例的Get操作都是线程安全的,而WriteBatch等操作可能需要其他一些同步机制

Merge 操作符

Merge 操作符就是一个原子性的”读取-更改-写入”的操作。比如说:

  1. 常见的 i++ 操作,如果不是原子性的,就可能会有问题。
  2. 读取一个队列,并向一个队列的结尾增加一个元素,如果不是原子性的,也可能有问题。

Iterators

遍历所有的 key

rocksdb::Iterator* it = db->NewIterator(rocksdb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
  cout << it->key().ToString() << ": " << it->value().ToString() << endl;
}
assert(it->status().ok()); // Check for any errors found during the scan
delete it;

遍历 [start, limit) 之间的值

for (it->Seek(start);
      it->Valid() && it->key().ToString() < limit;
      it->Next()) {
  ...
}
assert(it->status().ok()); // Check for any errors found during the scan

反向遍历

for (it->SeekToLast(); it->Valid(); it->Prev()) {
  ...
}
assert(it->status().ok()); // Check for any errors found during the scan

Snapshot(快照)

Snapshot 提供了当前系统在某一点的一个只读的状态表示。

rocksdb::ReadOptions options;
options.snapshot = db->GetSnapshot();
... apply some updates to db ...
rocksdb::Iterator* iter = db->NewIterator(options);
... read using iter to view the state when the snapshot was created ...
delete iter;
db->ReleaseSnapshot(options.snapshot);

注意这里通过 snapshot 读到的都是在做 snapshot 那个时间点的数据库的值。

Slice

上面提到的 iter->key()iter-value() 的返回值都是 rocksdb::Slice 类型的。Slice 仅仅是一个包含了长度和指针的 bytearray. 它本身并不储存值,这样也就避免了拷贝。

Slice 和 string 的互相转换:

rocksdb::Slice s1 = "hello";

std::string str("world");
rocksdb::Slice s2 = str;

std::string str = s1.ToString();
assert(str == std::string("hello"));

未完待续。..

参考

  1. https://github.com/facebook/rocksdb/wiki/Merge-Operator

RocksDB 基础概念教程

RocksDB 起源于 LevelDB, 并且从 HBase 中吸取了不少代码 [1]. RocksDB 设计的初衷是能够利用好 SSD 和内存的高性能,而且可以通过配置来承载高强度的读或者高强度的写。

RocksDB 是一个嵌入式的 key-value 数据库,并且所有的键都是有序的。RocksDB 支持的常用操作有 Get(Key), Put(Key), Delete(Key), Scan(Key).

RocksDB 中三个最基础的结构分别是 memtable, sstfile 和 logfile. memtable 是一个内存数据结构,新的写入首先写入到 memtable, 然后有可能写入到 logfile 中。logfile 是一个在硬盘上顺序写入的文件。当 memtable 存满了之后,它会被 flush 到 sstfile, 然后相应的 logfile 就可以安全的删除了。sstfile 中的数据为了方便查找 key 排序。

RocksDB 的功能

Get 可以从数据库中读出一个 kv 对,MultiGet可以从数据库中读出多个 kv 对。数据库中的所有数据在逻辑上都是有序的,一个应用可以指定一个 key 的比较方法,从而让所有的 key 按这种方法有序。可以通过使用 Iterator API 可以对数据库做一个 RangeScan.

使用 Put 方法可以更新一个数据,使用Write 可以原子性的更新多个数据。这两个操作都会直接覆盖老数据。

RocksDB 使用 checksum 来校验数据的损坏。一般来说校验是按照 block 来进行的。一个 block 如果被写入到硬盘之后就不会再变化了。

整个数据库写入的吞吐取决于 compaction 的速率,据观测,在 SSD 上多线程 compaction 的速率是单线程的 10 倍。整个数据库被存储为一系列的 sstfile. 当一个 memtable 满了之后,他会被写入到一个 Level0 的 sstable 上。在写入到 L0 的过程中,RocksDB 会把 memtable 中的重复的已删除的键全部都清理掉。一些文件会被周期性的读入合并到一起,这个行为叫做 compaction.

RocksDB 支持两种形式的 compaction. 其中一种叫做 Universal Style Compaction. 在这种模式下,所有的文件都存在 L0 模式,并且按照时间排序。这时候一个 compaction 会把时间上相连的两组文件合并并组成一个新的文件,再放回到 L0. 所有的文件都有可能有重复的键。

另一种模式 Level Style Compaction. 数据按层存储,也就是 L0…Ln. 最新的数据存储在 L0, 最老的数据存储在 Lmax 层。在 L0 的文件可能会有交叉的 key, 但是在其它层就不会有。compaction 发生的时候,去除 Ln 的一个文件,和在 Ln+1 层所有有相同 key 的文件,把他们合并之后作为一个新的文件存储在 Ln+1 层。通常来说 USC 比 LSC 产生比较小的写入放大,但是比较大的空间放大。

MANIFEST 文件存储了数据库的状态。

程序可以通过定义 compaction filter 来实现,key 的 TTL, 清洗数据等功能。

RocksDB 支持压缩。典型的配置是 L0-L2 没有压缩,中间层使用 snappy 算法压缩,Lmax 使用 zlib 压缩。

RocksDB 会把所有的 transaction 都存贮在 logfile 中,重启的时候 RocksDB 会再去处理这些 logfile. 这些 logfile 可以和 sstfile 存放在不同的目录,比如为了性能把 sstfile 存放在性能更高的存储上,而把 logfile 存放在性能低一点的存储上。

[1] https://github.com/facebook/rocksdb/wiki/RocksDB-Basics