使用:授人以渔——LevelDB的功能特性
本篇将全面介绍LevelDB的功能特性,我们将用原生的C++描述接口,也可以使用plyvel库来交互式的测试LevelDB。
打开数据库
LevelDB每一个数据库有一个name,对应一个目录,所有的数据库文件都在这个目录里。通过Open可以打开或者新建一个数据库,得到数据库的引用,通过这个引用来操作数据库。
可以这样打开一个数据库:
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
name指定数据库目录,options指定了打开数据库的选项,db获取了数据库的引用。这里使用了create_if_missing选项,当数据库不存在时会创建数据库。
关闭
只需要delete数据库实例即可:
delete db
基础API
LevelDB的基础API有三个,分别是Get、Put和Delete,表示查询一个键、插入一个键和删除一个键。LevelDB没有更新操作,因为更新就是简单地插入一个Kv,会覆盖之前的值。
db->Put(leveldb::WriteOptions(), "hello", "LevelDB");
std::string value;
db->Get(leveldb::ReadOptions(), "hello", &value);
db->Delete(leveldb::WriteOptions(), "hello");
同步写
数据库系统经常要抉择的一个问题:性能还是可靠性。如果每次写入,都调用类似于sync的操作,能保证写入的数据不会丢失,但是这是一个耗时的操作,会大大地降低吞吐量。如果不使用同步写的话,虽然吞吐量会很高,但是系统宕机可能会丢数据。
这就需要根据自己的场景进行抉择了。LevelDB默认不使用同步写,将数据write后,不调用sync就返回了,数据很有可能还在缓冲区里。可以手动开启同步写:
leveldb::WriteOptions options;
options.sync = true;
leveldb::DB* db;
db->Put(options, "hello", "LevelDB");
原子更新
有时候需要一些更新同时生效,也就是要支持多个操作的原子性。比如写key1,再写入key2,如果在写入key1时宕机了,就没有key2了,这时候可以使用原子更新:
leveldb::WriteBatch batch;
batch.Delete(key1);
batch.Put(key2, value);
s = db->Write(leveldb::WriteOptions(), &batch);
可以把Delete和Put加入到一个WriteBatch中,一次性写入数据库。
原子更新除了提供原子性外,还可以提高性能,因为可以将多个操作批量写入。
并发
当一个进程打开一个LevelDB数据库时,会获取这个数据库的一个文件锁,其它进程就没法获取这个文件锁了。所以一个LevelDB数据库只支持一个进程同时访问,但是这一个进程里面可以同时有多个线程并发访问。对于leveldb::DB里的很多方法,都是线程安全的,在这些方法内都有加锁的步骤。但是对于其它的一些对象,比如WriteBatch,如果多线程并发访问,需要自己同步。
迭代器
LevelDB里大量使用了迭代器,可以对Data Block、SSTable、MemTable和整个数据库进行迭代。
比如可以迭代整个数据库:
leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
cout << it->key().ToString() << ": " << it->value().ToString() << endl;
}
或者迭代[start, limit)这个范围:
for (it->Seek(start);
it->Valid() && it->key().ToString() < limit;
it->Next()) {
...
}
比较器
LevelDB实际上是一个SortedMap,需要定义键之间比较的规则。前面使用了默认的比较规则,也就是基于字节串的比较。也可以提供自己的比较规则:
CaseInsensitiveComparator cmp;
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
options.comparator = &cmp;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
这里定义了一个不区分大小写的比较规则,然后打开数据库。打开一个已存在的数据库时的比较规则需要和创建时的比较规则相同或者兼容,这个很好理解,如果两次不兼容,那么排序对于第二次就是不对了。LevelDB新建数据库时,会向MANIFEST写入一个比较器的名称,下次打开时会检查名称是否相同,来判断兼容性。
Snapshot
LevelDB支持快照功能。快照是一个一致性视图,当创建一个快照时,就给那个时刻的数据库状态打了个快照,以后的更新插入删除在这个快照下是不可见的,类似于MVCC的功能。
leveldb::ReadOptions options;
options.snapshot = db->GetSnapshot();
... 对db做一些修改 ...
leveldb::Iterator* iter = db->NewIterator(options);
... 使用的是还是快照创建的时候的状态 ...
delete iter;
db->ReleaseSnapshot(options.snapshot);
注意快照不再使用时,需要马上释放,防止不需要的数据长久被占用,无法清理。
数据块
LevelDB将相邻的数据存储到一个Data Block里,多个Data Block组成一个SSTable。LevelDB里压缩、读取和缓存的单位都是Data Block。默认的块大小是4K,块越大,顺序读效率越高,块越小,随机读效率越高。
leveldb::Options options;
options.block_size = 8192;
leveldb::DB* db;
leveldb::DB::Open(options, name, &db)
压缩
压缩是以CPU时间换取IO时间的一种方式。压缩以后,数据变小,磁盘IO就变小了,而LevelDB采用的snappy压缩速度很快,CPU占用不多。默认情况下,压缩是开启的,很少有情况不需要开启压缩。
leveldb::Options options;
options.compression = leveldb::kNoCompression;
leveldb::DB* db;
leveldb::DB::Open(options, name, &db)
缓存
SSTable里的Data Block要被访问时,需要先从磁盘读取出来,然后解压缩。LevelDB提供了缓存,可以缓存解压后的Data Block,减少磁盘IO。
LevelDB提供了一个LRU Cache,给缓存设置一定的空间大小,并且缓存最近使用的Data Block。也可以提供自己的缓存策略,只需要实现Cache接口就行。LevelDB默认情况下使用了一个8MB的LRU Cache。
leveldb::Options options;
options.block_cache = leveldb::NewLRUCache(100 * 1048576); // 100MB cache
leveldb::DB* db;
leveldb::DB::Open(options, name, &db);
当迭代整个数据库时,会把所有的热数据都淘汰出缓存,这时候可以选择迭代的时候,不将数据加入到缓存中:
leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
...
}
布隆过滤器
除了缓存可以提高读取的效率,布隆过滤器也可以提高读取的效率。当需要读取一个键时,就算这个键不在一个Data Block,依然需要读出这个Data Block,才知道这个键是否存在。有了布隆过滤器,可以先读取布隆过滤器,如果告诉说这个键不存在,就不再需要读取这个Data Block了。
leveldb::Options options;
options.filter_policy = NewBloomFilterPolicy(10);
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
NewBloomFilterPolicy参数10表示,每个键将使用10bit的空间构造布隆过滤器。10bit的情况下,如果布隆过滤器说一个键不存在,那么这个键一定不存在,如果说这个键存在的话,99%的概率是存在的,1%的概率是不存在的,假阳率是1%。10是个比较好的参数,这时布隆过滤器不需要占据太多空间,但是假阳率也比较低,继续提高对假阳率的改善并不显著。
布隆过滤器的数据是要写入SSTable的,当一个SSTable打开后,布隆过滤器的数据是常驻内存的,直到SSTable被关闭。布隆过滤器也是一种空间换时间的方式。
数据校验
可以在打开一个数据库时做校验:
leveldb::Options options;
options.paranoid_checks = true;
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
也可以在读取数据时做校验:
leveldb::ReadOptions options;
options.verify_checksums = true;
std::string value;
db->Get(options, "hello", &value);
小结
这一篇详细介绍了LevelDB的能力,也就是LevelDB可以做什么。这样就可以在选型时确定LevelDB是否合适,LevelDB可以提供哪些功能供我们使用。
了解了What以后,将了解Why,下一节将介绍LevelDB的工作原理。