levelDB简单介绍


该介绍文档,翻译自leveldb在github上面的介绍文档。https://github.com/google/leveldb/blob/master/doc/index.md

LevelDB

LevelDB库,提供持久化的键值存储(KV数据库)。键和值的类型是任意的字节数组。在键值存储中,通过用户自定义的对比函数对健进行排序。

打开数据库

LevelDB数据库的名称对应于文件系统目录。数据库中的所有内容都保存在这个目录之下。接下来的示例显示怎么打开一个数据库,如果需要的话创建数据库:

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

leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
assert(status.ok());
...
如果你想在数据库已经存在时报出错误,那么在调用leveldb::DB::Open之前添加下面的这行代码
options.error_if_exists = true;

Status

你可能已经在上面的示例代码中注意到了leveldb::Status 类型。leveldb的大部分函数都返回该类型的值,用于表示可能遇到的错误。你可以通过检查这个结果是否ok,也可以打印相关联的错误消息:

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

关闭数据库

当你用完数据库之后,只要删除数据库对象即可。例如:

... open the db as described above ...
... do something with db ...
delete db;

读和写

LevelDb提供了Put、Delete和Get方法来修改/查询数据库。例如下面的示例代码,移动key1的值到key2

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

原子更新

注意到上面的示例,如果进程在插入了key2之后,但是在删除key1前被杀,那么在数据库中就会存在多个不同的key上有相同的值的情况。这类问题可以通过使用WriteBatch类来 避免,它会原子地提交这些更新的集合。

#include "leveldb/write_batch.h"
...
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) {
  leveldb::WriteBatch batch;
  batch.Delete(key1);
  batch.Put(key2, value);
  s = db->Write(leveldb::WriteOptions(), &batch);
}
    WriteBatch包含要对数据库进行的一系列编辑,这些批次内的编辑会被有序地提交。既然我们在Put志气调用Delete,那么如果key1和key2是相同的,我们最终也不会错误地丢弃该值。

    除了原子性的好处,WriteBatch也可以用来加快批量更新,只要将很多个不同单独的修改放入同一个批次里面。

同步写

默认地,LevelDB的每笔写都是异步的。在将进程的数据推送到操作系统之后,写就返回。从操作系统的内存传输到底层存储层是异步进行的。但是sync标记可以为特定的写入打开,以保住写的操作直到数据最终被写入到持久层存储上才返回。(在Posix的系统上,这个可以通过在写操作返回前调用fsync(...)、或者fdatasync(...)、又或者msync(...,MS_MSYNC)等)来实现

leveldb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ...);
     异步写通常比同步写快千倍。异步写的缺点就是机器的崩溃会导致最新的一些提交丢失。请注意,仅写入过程时的崩溃(即不重启)也不会造成数据丢失,即使sync是false,因为会在更新完成之前,将该更新从进程内存推送到操作系统。

    异步写通常可以安全使用。例如,当加载大量的数据到数据库时,你可以通过在崩溃之后重新启动大容量加载来处理丢失的更新。也有一个可行的混合方案,每第N次同步写,其他次异步写,在进程崩溃之后,只要批量加载上次同步写之后的更新即可。(同步写可以更新标记,该标记描述崩溃时从何处重新启动)

    WriteBatch提供了异步写入的替代方法。将多笔更新放在相同的WriteBatch,然后通过同步写方式一起提交(例如,write_option.sync被设置成true)。同步写的额外成本将在批处理中的所有写入中分摊。    

并发

数据库在一个时间内只允许被一个进程打开。为避免误用,LevelDB在实现中从操作系统中获取了一把锁。在一个进程中,相同的leveldb::DB对象在在多个并行线程中安全共享。例如,不同的线程能写入或者获取迭代器,又或者调用Get方法在相同数据库中而不用额外的同步(leveldb实现将自动执行所需的同步)。然而其他对象(像迭代器和WriteBatch)需要额外的同步手段。如果两个线程共享这样一个对象,他们必须使用自己的锁策略来保护这些对象的访问。更多的细节可以查看公共的头文件。

遍历

接下来的代码示例,演示了怎么打印数据库中所有的键值对。

leveldb::Iterator* it = db->NewIterator(leveldb::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)这样的key的范围遍历
for (it->Seek(start);
   it->Valid() && it->key().ToString() < limit;
   it->Next()) {
  ...
}
你也可以通过反向遍历的方式处理数据库里的条目。(警告:反向迭代可能比正向迭代慢一点)
for (it->SeekToLast(); it->Valid(); it->Prev()) {
  ...
}

快照

    快照在键值存储的整个状态上,提供了一致的只读视图。ReadOptions::snapshot可能非NULL,这预示了读应当在DB状态的特定版本里进行。如果ReadOptions::snapshot是NULL,读取将在当前状态下的隐式快照进行。

  快照通过调用  DB::GetSnapshot()方法创建:

leveldb::ReadOptions options;
options.snapshot = db->GetSnapshot();
... apply some updates to db ...
leveldb::Iterator* iter = db->NewIterator(options);
... read using iter to view the state when the snapshot was created ...
delete iter;
db->ReleaseSnapshot(options.snapshot);
请注意,当快照不再需要时,它应该通过使用DB::ReleaseSnapshot接口来释放。这允许在实现中,摆脱为了支持快照的读而维护的状态。

Slice

    it->key()it->value()的返回值都是leveldb::Slice 类型的实例。Slice结构包含了长度成员和指向外部字节数组的指针成员。返回Slice是相比返回std::string更便宜的选择,因为我们不需要潜在拷贝大量的key和value。此外,leveldb方法不返回C风格的空字符结尾的字符串,因为leveldb的key和value允许包含'\0'字节。

    c+=风格的字符串和以空字符结尾的C-风格字符串可以简单地转换成Slice对象:

leveldb::Slice s1 = "hello";

std::string str("world");
leveldb::Slice s2 = str;
     Slice对象也可以简单地转换回C++的字符串:

std::string str = s1.ToString();
assert(str == std::string("hello"));
     但是使用Slice时要谨慎,因为它取决于调用者,以确保在Slice使用过程中,外部的字节数组依然存活。例如,下面的代码是有bug的:

leveldb::Slice slice;
if (...) {
  std::string str = ...;
  slice = str;
}
Use(slice);
     当if语句块结束,str就会被销毁,从而Slice的后端空间就会消失。

比较器

之前的示例代码使用的都是默认的排序函数,通过字节字典排序。你可以在打开数据库时,提供自定义的比较器。例如,假设每个数据库的key包含两个整数,我们应该先按第一个数排序,第一个数相同时,使用第二个数进行排序。首先,我们定义表示这些规则的leveldb::Comparator的子类:

class TwoPartComparator : public leveldb::Comparator {
 public:
  // Three-way comparison function:
  //   if a < b: negative result
  //   if a > b: positive result
  //   else: zero result
  int Compare(const leveldb::Slice& a, const leveldb::Slice& b) const {
    int a1, a2, b1, b2;
    ParseKey(a, &a1, &a2);
    ParseKey(b, &b1, &b2);
    if (a1 < b1) return -1;
    if (a1 > b1) return +1;
    if (a2 < b2) return -1;
    if (a2 > b2) return +1;
    return 0;
  }

  // Ignore the following methods for now:
  const char* Name() const { return "TwoPartComparator"; }
  void FindShortestSeparator(std::string*, const leveldb::Slice&) const {}
  void FindShortSuccessor(std::string*) const {}
};
     现在,我们使用自定义的对比器创建数据库:


TwoPartComparator 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);





评论

发表评论