leveldb1.5自带文档翻译

来源:互联网 发布:java编程思想新版 编辑:程序博客网 时间:2024/05/01 23:35

Leveldb概述

leveldb提供了持久的键值对的存储。key和value为任意的字节数组。键的存储是有序的,可以通过用户自定义的比较函数进行排序。

打开数据库

leveldb数据库的名字和文件系统目录是一致的。所有数据库的内容都存放在这个文件系统的目录下。下面的实例展示了如何打开leveldb数据库,如果没有则会自动创建。

#include <assert>
#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中,大部分函数会返回上面实例中提到的leveldb::Status类型。如果函数出错,可以检查结果是否正确或者打印相关的错误信息:

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

关闭数据库

当用完数据库的时候应当关闭数据库。例如:

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

读和写

数据库提供了插入、删除和查询的方法来修改或查询数据库。例如,下面的代码展示了将值从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下存在相同的value。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类顺序的执行一系列的操作。在例子中,要在插入之前调用删除操作。这样当key1和key2相同的时候,不需要结束错误的删除值的操作。

WriteBatch类除了原子性的好处之外,可以大大加快大批量更新操作的速度。

同步写

默认情况下,每一次写操作都是异步的:当把写操作提交之后函数就会返回。从内存到持久化存储的转化是异步的。同步标志可以控制写操作在数据已经执行了所有的持久化存储之后返回。(在Posix操作系统上,在写操作返回之前是通过调用fsync(...)、fdatasync(...)、msync(..., MS_SYNC)其中的一个方法实现)。

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

异步操作通过比同步操作快1000多倍。异步写的负面影响是机器故障可能会引起最近的少部分更新操作丢失。即使当sync标志位false的时候,写进程的错误(不是重启机器)也不会引起任何丢失。在写操作完成之前,更新操作已经提交给了操作系统。

异步写通常情况下是安全的。例如,在故障发生之后,当加载大批量的数据到数据库时,可以通过重新启动bulk load来处理丢失的更新操作。一种可能的混合模式是每N次写是同步的,在故障发生的时候,在最后一次同步写完成之后bulkload会重启。(同步写可以更新用来描述故障时从哪个地方重启的标志。)

WriteBatch 类提供了一种异步写的替代方案。WriteBatch通过多个更新语句一块执行来完成同步操作。同步写入的开销将会合在一块。

Concurrency

一个数据库可能在同一时刻仅被一个进程打开。leveldb需要一个操作系统的锁来阻止这种情况。在一个单独的进程中,相同的level::DB对象可以被不同的线程安全的访问。比如,不同的线程可以写入或者迭代或者查询相同的数据库而不需要额外的同步(leveldb会自动执行必要的同步工作)。其他的一些对象如Iterator和WriteBatch也可能需要额外的同步。如果两个线程共享同一个对象,线程必须通过锁机制来访问。更多详细的内容可以通过头文件查看。

IIteration

下面的例子展示了如何打印数据库中的所有键值对。

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;

 

下面的例子展示了如何访问key在[start,limit)范围内的情况:

for (it->Seek(start);
     it->Valid() && it->key().ToString() < limit;
     it->Next()) {
    ...
}

下面展示了逆序访问(注意:逆序访问会比正序访问要慢)

for (it->SeekToLast(); it->Valid(); it->Prev()) { ... }

 快照

快照提供了和整个键值对的存储一致的只读的视图。ReadOptions::snapshot如果不等于Null,可以用来指示读取一个特定的版本的数据库。如果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 whenthe snapshot was created ... delete iter;db->ReleaseSnapshot(options.snapshot);

当快照对象不再需要的时候,应该通过DB::ReleaseSnapshot接口释放。这样可以摆脱为了支持快照读取而维持的状态。

Slice

前面的it->key()和it->value()的返回值为leveldb::Slice类型的实例。Slice的结构非常简单,包含了长度和指向字节数组类型的指针字段。返回Slice的代价要比std::string类型小的多,因为不要拷贝键值对。另外,leveldb的方法不返回以'\0'结尾的C风格的字符串,因为leveldb允许键和值包含'\0'。

std::string和以'\0'结尾的C风格的字符串可以非常容易的转换为Slice类型:

leveldb::Slice s1 = "hello";std::string str("world"); leveldb::Slice s2 = str;

Slice可以非常容易的转换为std::string类型:

   std::string str = s1.ToString();

   assert(str ==std::string("hello"));

当在使用Slice的时候应该注意Slice指向的字节数组仍然有效。例如,下面的代码就有问题:

   leveldb::Slice slice;

   if (...) {

     std::string str = ...;

     slice = str;

   }

   Use(slice);

当超出了str的作用域之后,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 resultint 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 followingmethods 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);

  ...

向下兼容性

当数据库创建时,比较器的Name()方法的返回值和数据库相关,在其后的数据库打开都会被检查。如果Name()的返回值改变,leveldb::DB::Open()方法将会失败。因此,只有当新的key的格式和比较函数不能和现有的数据库匹配时才能改变Name()方法的返回值。现有数据库中的数据会被抛弃。

但是随着时间的推移,仍然可以安装预先计划的改变几个比特。例如,在每个key的最后保留一个版本号(对多数应用一个字节已经足够)。当改变key的格式的时候,(例如在TwoPartComparator的key中增加第三部分 ),(a)保持相同的比较器名字(b)为每一个key增加版本号(c)改变比较器函数,确保可以用key中的版本数字来决定如何解释key。

性能

可以通过改变include/leveldb/options.h中的默认值来调整性能。

Block大小

leveldb相邻的key一块放到同一个block中,block是持久化存储的单元。默认block大小为4096字节。经常处理大块查询的应用应当提高block的大小。经常处理大量的小value的应用程序使用更小的block,性能测试有提高时可以采用。block的大小小于1kb或者大于几mb没有性能优势。越大的block大小对压缩更有优势。

压缩

在写入硬盘之前每一个block都是被单独的压缩。压缩默认是开启的,因为压缩方法非常快,并且非压缩的数据将不可用。罕见情况下,如果不采用压缩对性能有明显提升,可以采用非压缩。

  leveldb::Options options;
  options.compression = leveldb::kNoCompression;
  ... leveldb::DB::Open(options, name, ...) ....

缓存

数据库的内容被存储在文件系统的一系列文件中,每个文件包含多个压缩的block。如果options.cache非空,可以用来缓存使用频率非常高的未压缩的block内容。

  #include "leveldb/cache.h"
 
  leveldb::Options options;
  options.cache = leveldb::NewLRUCache(100 * 1048576);  // 100MB cache
  leveldb::DB* db;
  leveldb::DB::Open(options, name, &db);
  ... use the db ...
  delete db
  delete options.cache;

需要注意的是,缓存中保存的是未压缩的数据,因此大小和数据的大小相同。

当执行大批量的读取时,应用程序应该放弃缓存,只有这样才不会取代原先缓存中的内容。迭代器选项fill_cache可以实现这种方式:

leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
  ...
}

Key layout

硬盘存储和传输的基本单元为block。通过数据库的key的排序方式相邻的key可能存放到同一个block中。因此应用程序可以通过将不常使用的key放到单独的区域来提高程序的性能。

假设基于leveldb实现的简单的文件系统,存储的键值对类型如下:

filename -> permission-bits, length, list of file_block_ids
   file_block_id -> data

在filename加上一个字符的前缀,比如’/’,在file_block_id加上另外一个字符作为前缀,比如’0’。这样缓存就不会强制缓存大数据的filename部分。

Filters

由于leveldb在硬盘上的数据组织方式,在一次的读取操作中可能会独到多条数据。可选的FilterPolicy 机制可以用来减少读取硬盘的次数。

leveldb::Options options;
options.filter_policy = NewBloomFilter(10);
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
... use the database ...
delete db;
delete options.filter_policy;

上面的例子中用到了基于Bloom filter的过滤器代理。基于Bloom filter的过滤器依赖于每个key中相同比特的数据(在上述例子中为10个比特)。这种方法将减少Get()方法对磁盘的不必要的读写。增加key的比特数将会节省更多的内存。建议如果数据不适合在内存中存放或者做大量的随机读取操作设置过滤器代理。

当使用定制的比较器时,需要确认过滤器代理和比较器是兼容的。例如,当比较器在比较key的时候忽略了key后面的空间,NewBloomFilter一定不能用这种比较器。相反,应用程序应当提供一个定制的同样忽略了key后面空间的过滤器代理。例如:

class CustomFilterPolicy : public leveldb::FilterPolicy {
   private:
    FilterPolicy* builtin_policy_;
   public:
    CustomFilterPolicy() : builtin_policy_(NewBloomFilter(10)) { }
    ~CustomFilterPolicy() { delete builtin_policy_; }
 
    const char* Name() const { return "IgnoreTrailingSpacesFilter"; }
 
    void CreateFilter(const Slice* keys, int n, std::string* dst) const {
      // Use builtin bloom filter code after removing trailing spaces
      std::vector<Slice> trimmed(n);
      for (int i = 0; i < n; i++) {
        trimmed[i] = RemoveTrailingSpaces(keys[i]);
      }
      return builtin_policy_->CreateFilter(&trimmed[i], n, dst);
    }
 
    bool KeyMayMatch(const Slice& key, const Slice& filter) const {
      // Use builtin bloom filter code after removing trailing spaces
      return builtin_policy_->KeyMayMatch(RemoveTrailingSpaces(key), filter);
    }
  };

更高级的应用程序可以使用不是基于bloom filter的过滤器而是基于一系列key的概括的过滤器。详情参见leveldb/filter_policy.h。

校验和

Leveldb对所有存储在硬盘上的数据都会进行校验。对校验的控制有两个单独的参数:

l  ReadOptions::verify_checksums强制对文件系统中读取的所有数据校验。默认情况下该校验不开启。

l  Options::paranoid_checks在打开数据库之前应当设置为true,这样当内部出现错误的时候可以立刻产生出来。当数据库打开或者做其他数据库操作的时候有问题的数据库的错误就会产生。默认情况下该选项为关闭状态,即使数据库有错误也仍然可用。

近似大小

GetApproximateSizes方法可以获取一个或几个key范围内所使用的文件系统的近似大小。

leveldb::Range ranges[2];
ranges[0] = leveldb::Range("a", "c");
ranges[1] = leveldb::Range("x", "z");
uint64_t sizes[2];
leveldb::Status s = db->GetApproximateSizes(ranges, 2, sizes);

上述代码会将key的范围为[a,c)所占用文件系统大小返回给sizes[0],将key的范围为[x,z)所占用文件系统大小返回给sizes[1]。

环境

所有文件操作(包括其他操作系统调用)都和leveldb::Env对象相关。高级的客户端通过自己的环境变量来得到更好的控制。例如,应用程序可以通过增加文件系统的人为延迟来避免leveldb对系统的其他工作的影响。

class SlowEnv : public leveldb::Env {
    .. implementation of the Env interface ...
};
 
SlowEnv env;
leveldb::Options options;
options.env = &env;
Status s = leveldb::DB::Open(options, ...);

移植

通过提供leveldb/port/port.h导出的类型/方法/函数的新平台的特殊实现,leveldb可以移植到新的平台。查看leveldb/port/port_example.h获取更多的详细信息。

另外,新平台需要实现新的默认的leveldb::Env,可以参考leveldb/util/env_posix.h。

其他信息

更多关于leveldb的详细信息可以查看下面的链接。