Leveldb 使用说明文档
Leveldb 使用说明文档
原作者:Jeff Dean, Sanjay Ghemawat
翻译:乌合之众solym@sohu.com
英文原文地址https://rawgit.com/google/leveldb/master/doc/index.html
leveldb库提供持久性键值存储。 键和值可以是任意字节数组。 根据用户指定的比较函数,在键值存储器内对键进行排序。
打开一个数据库
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中可能遇到错误的大多数函数都返回此类型的值。 您可以检查这样的结果是否正确,并打印相关的错误消息:
leveldb::Status s = ...;
if (!s.ok()) cerr << s.ToString() << endl;
关闭数据库
使用完数据库后,只需删除数据库对象。 例:
... open the db as described above ...
... do something with db ...
delete db;
读和写 Reads And Writes
数据库提供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);
原子更新 Atomic Updates
注意,如果进程在Put Key2 之后、Delete key1之前结束(挂了),会使得value被同时保存在多个key下。使用WirteBatch
类进行原子更新一组操作,可以避免这些问题:
#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
也可以用于通过将大量的单个操作
放入同一批次来加速批量更新。
异步写 Synchronous Writes
默认情况下,每次写入leveldb都是异步
的:将写入从进程推送到操作系统后立即返回。从操作系统内存到底层持久存储的传输是异步的。 对于特定的写操作,可以打开同步标志
,以使写操作不会返回,直到正在写入的数据真正写入永久存储器。 (在Posix系统上,这是通过在写操作返回之前调用fsync
(...)或fdatasync
(...)或msync
(...,MS_SYNC)实现的。
leveldb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ...);
异步写操作通常比同步写操作快一千倍。 异步写入的缺点是,机器的崩溃可能导致最后几个更新丢失。 请注意,只是写入进程(即不是机器重新启动)的崩溃不会导致任何损失 ,因为即使sync
为false
,更新从进程内存推送到操作系统在之后即认为完成。
通常可以安全地使用异步写入。 例如,将大量数据加载到数据库中时,可以通过在崩溃后重新启动批量加载来处理丢失的更新。 混合方案也是可能的,其中每第N次写入是同步的,并且在崩溃的情况下,批量加载刚好在由前一次运行完成的最后同步写入之后重新启动。 (同步写入可以更新描述崩溃时重新启动位置的标记。)
WriteBatch提供了异步写入的替代方法。 多个更新可以放置在同一WriteBatch
中,并使用同步写入(即,write_options.sync
设置为true
)一起应用。 同步写入的额外成本将在批次中的所有写入之间摊销。
并发 Concurrency
数据库只能由一个进程一次打开。leveldb实现从操作系统获取锁
以防止误用。 在单个进程中,同一个leveldb::DB
对象可以安全地由多个并发线程共享。 即,不同的线程可以写入或获取迭代器或在没有任何外部同步的情况下在同一数据库上调用Get
(leveldb实现将自动执行所需的同步)。
但是其他对象(如Iterator
和WriteBatch
)可能需要外部同步。如果两个线程共享这样的对象,它们必须使用自己的锁定协议保护对它的访问。 更多详细信息在公共头文件中可用。
迭代器 Iteration
以下示例演示如何在数据库中打印所有 键-值对 。
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)中的键:
for (it->Seek(start);
it->Valid() && it->key().ToString() < limit;
it->Next()) {
...
}
您还可以按相反的顺序处理条目。 (注意:反向迭代可能比前向迭代慢一些。)
for (it->SeekToLast(); it->Valid(); it->Prev()) {
...
}
快照 Snapshots
快照在键值存储的整个状态上提供一致的只读视图。ReadOptions::snapshot
可能是非NULL
的,表示read
操作应该在某个特定版本的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方法不返回以null结束的C风格字符串,因为leveldb键和值允许包含\0
字节。
C++ std::string和以null结尾的C风格字符串可以很容易地转换为Slice:
leveldb::Slice s1 = "hello";
std::string str("world");
leveldb::Slice s2 = str;
一个Slice可以很容易的转换回一个C++ string:
std::string str = s1.ToString();
assert(str == std::string("hello"));
使用Slice时要小心,因为它取决于调用者,以确保在Slice使用时,Slice所指向的外部字节数组保持有效。 例如,以下是buggy:
leveldb::Slice slice;
if (...) {
std::string str = ...;
slice = str; // str是一个临时对象,会被析构
}
Use(slice);
当出if语句范围时,str
将被销毁,slice
的内指针指向的内存将被释放。
比较器 Comparators
前面的例子使用了key的默认排序函数,它按字节顺序排列
。 然而,您可以在打开数据库时提供自定义比较器。 例如,假设每个数据库键由两个数字组成,我们应该按第一个数字排序,用第二个数字断开关系(这里实际上就是按这两个数字组合比较大小)。 首先,定义一个表示这些规则的leveldb::Comparator
的适当子类:
class TwoPartComparator : public leveldb::Comparator {
public:
// 三种比较结果:
// if a < b: 结果为负
// if a > b: 结果为正
// else: 结果为0
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; // a1==b1
if (a2 > b2) return +1; // a1==b1
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 { }
};
现在创建一个数据库,使用自定义的比较器comparator:
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);
...
后向兼容性 Backwards compatibility
比较器的Name
方法的结果在创建时附加到数据库,并在每次后续数据库打开时检查。 如果Name
更改,leveldb::DB::Open
调用将失败。
因此,当且仅当新的key格式
和比较函数
(comparison function)与现有数据库不兼容,并且可以丢弃
所有现有数据库的内容时,可对Name
修改。
然而,您仍然可以通过一些预先计划逐渐演变您的key格式。例如,您可以在每个键的末尾存储一个版本号(一个字节应该足矣满足大多数用途)。
当您希望切换到新的键格式(例如,在TwoPartComparator处理的键添加一个可选的第三部分),
(a)保持相同的比较器名称
(b)增加新键的版本号
(c)更改比较器函数,以便它根据在key中找到的版本号决定如何解析它们。
性能 Performance
性能可以通过改变在include/leveldb/options.h
定义的类型的默认值进行调整。
块大小 Block size
leveldb
将相邻的键
组合在一起成为同一个块,并且这样的块访问持久存储器的传送单元(一次读写大小)。
默认块大小约为4096
个未压缩字节。 如果应用程序主要对数据库内容进行批量扫描,可能希望增加此大小。性能测量表明有改进,那么对小value
进行大量定位读取的应用程序可能希望切换到较小的块大小。 使用小于1KB或大于几MB的块没有太大的好处。 还要注意,压缩对于更大的块大小将更有效。
压缩 Compression
每个块在被写入永久存储器之前被单独压缩。 压缩默认情况下处于打开状态,因为默认压缩方法非常快,并且会自动禁用不可压缩数据。 在极少数情况下,如果应用程序可能希望完全禁用压缩,应该这样做,基准显示性能改进:
leveldb::Options options;
options.compression = leveldb::kNoCompression;
... leveldb::DB::Open(options, name, ...) ....
缓存 Cache
数据库的内容存储在文件系统中的一组文件中,并且每个文件存储压缩块的序列。 如果options.cache为非NULL,它用于缓存常用的未压缩块内容。
#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;
请注意,高速缓存保存未压缩数据,因此应根据应用程序级数据大小确定大小,而压缩不会对其产生任何影响。(压缩块的缓存由操作系统缓冲区缓存或客户端提供的自定义Env实现)。
当执行批量读取时,应用程序可能希望禁用高速缓存,使得由批量读取处理的数据不会最终取代大部分高速缓存的内容。 设置每个迭代器选项可以实现这一点:
leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
...
}
键布局 Key Layout
注意,磁盘传输和高速缓存以一个块(Block
)为单位。 相邻的键(根据数据库排序顺序)通常放置在同一个块中。 因此,应用程序可以通过放置彼此靠近在一起的键,并将不常使用的键放置在键空间的单独区域中来提高其性能。
例如,假设我们在leveldb之上实现一个简单的文件系统。 我们可能希望存储的条目的类型是:
filename -> permission-bits, length, list of file_block_ids
file_block_id -> data
我们可能需要用一个字母前缀标识文件名keys
(比如/
),并用不同的字母标识file_block_id
的keys
(比如0
),以便只扫描元数据而不强迫我们提取和缓存庞大的文件内容。
过滤器 Filters
由于leveldb数据在磁盘上的组织方式,单个Get()
调用可能涉及从磁盘多次读取。 可选的FilterPolicy
机制可用于显着减少磁盘读取数。
leveldb::Options options;
options.filter_policy = NewBloomFilterPolicy(10);
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
... use the database ...
delete db;
delete options.filter_policy;
上述代码将基于BoolmFilter
的过滤策略与数据库相关联。 基于Boolm过滤器的过滤依赖于在每个Key
的存储器中保持一些数量的数据bits
(在这种情况下每个密钥是10位,因为这是我们传递给NewBloomFilterPolicy的参数)。 此过滤器将Get()
调用所需的 不必要磁盘读取次数 减少大约100倍。增加每个键的bits位将导致更大的减少,但是以更多的内存使用为代价。我们建议不适合将工作集放置在内存中的应用程序,多设置一些随机读取过滤策略。
如果您使用自定义比较器,则应确保您使用的过滤器策略与比较器兼容。 例如,考虑一个比较器,在比较键时忽略尾随空格。 NewBloomFilterPolicy
不能与这样的比较器一起使用。 因此,应用程序也应提供一个也忽略尾部空格的自定义过滤器策略。 例如:
class CustomFilterPolicy : public leveldb::FilterPolicy {
private:
FilterPolicy* builtin_policy_;
public:
CustomFilterPolicy() : builtin_policy_(NewBloomFilterPolicy(10)) { }
~CustomFilterPolicy() { delete builtin_policy_; }
const char* Name() const { return "IgnoreTrailingSpacesFilter"; }
void CreateFilter(const Slice* keys, int n, std::string* dst) const {
// 删除尾随空格后使用内置bloom filter代码
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 {
// 删除尾随空格后使用内置bloom filter代码
return builtin_policy_->KeyMayMatch(RemoveTrailingSpaces(key), filter);
}
};
Advanced应用程序可以提供不使用Bloom过滤器,但使用一些其他的机制来总结一组键的过滤策略。 有关详细信息,请参阅leveldb/filter_policy.h
。
检验和 Checksums
leveldb将校验和与其存储在文件系统中的所有数据相关联。 提供了两个单独的控件来对这些校验进行主动验证:
ReadOptions::verify_checksums
可以设置为true
,以强制代表特定读取从文件系统读取的所有数据的校验和验证。 默认情况下,不进行此类验证。Options::paranoid_checks
可以在打开数据库之前设置为true,以使数据库实现在检测到内部损坏时立即引发错误。 根据数据库的哪个部分已损坏,可能在打开数据库时或稍后由另一个数据库操作引发错误。 默认情况下,paranoid
检查关闭,以便即使数据库的持久存储器的部分已损坏也可以使用数据库。- 如果数据库损坏(或者当打开
paranoid checking
时失败),leveldb::RepairDB
函数可以用于尽可能多地恢复数据。
大致大小 Approximate Sizes
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);
前面的调用会将size[0]和size [1]设置为key
范围[a..c)和[x..z)使用的文件系统空间的大致字节数。
环境 Environment
由leveldb
实现发出的所有文件操作(和其他操作系统调用)都通过一个leveldb::Env
对象进行转调。老练的客户可能希望提供自己的Env
实现以获得更好的控制。 例如,应用程序可以在文件IO调用中引入人工延迟,以限制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, ...);
移植 Porting
leveldb可以通过提供由leveldb/port/port.h
导出特定平台的types/methods/functions
实现而被移植到新的平台。相关更多详细信息,请参阅leveldb/port/port_example.h
此外,新平台可能需要一个新的默认leveldb::Env
实现。 有关示例,请参见leveldb/util/env_posix.h
。
其它信息 Other Information
有关leveldb实现的细节可以在以下文档中找到: