LevelDB 源码剖析:环境搭建实战使用常用优化
Posted 凌桓丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LevelDB 源码剖析:环境搭建实战使用常用优化相关的知识,希望对你有一定的参考价值。
文章目录
环境搭建
# 下载源码
git clone https://github.com/google/leveldb.git
# 下载依赖第三方库(benchmark、googletest)
git submodule update --init
# 执行编译
cd leveldb/
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug .. && cmake --build .
# 头文件加入系统目录
cp -r ./include/leveldb /usr/include/
cp build/libleveldb.a /usr/local/lib/
实战使用
创建、关闭数据库
创建数据库或打开数据库,均通过 Open
函数实现。Open
为一个静态成员函数,其函数声明如下所示:
static Status Open(const Options& options, const std::string& name,
DB** dbptr);
- const Options & options:用于指定数据库创建或打开后的基本行为。
- const std::string & name:用于指定数据库的名称。
- DB* dbptr*:定义了一个DB类型的指针的指针,该指针作为Open函数操作后传给调用者使用的DB类型的实际指针。
- Status:当操作成功时,函数返回
status.ok()
的值为 True,*dbptr 分配了不为 NULL 的实际指针地址;若其中的操作存在错误,则status.ok()
的值为 False,并且对应的 *dbptr 为 NULL。
关闭数据库非常简单,只需要使用 delete
释放 db 即可。
代码示例如下:
#include"leveldb/db.h"
#include<string>
#include<iostream>
using namespace std;
int main()
leveldb::DB* db;
leveldb::Options op;
op.create_if_missing = true;
//打开数据库
leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
if(!status.ok())
cout << status.ToString() << endl;
exit(1);
//关闭数据库
delete db;
return 0;
数据读、写、删除
数据库中的读、写与删除操作分别用 Get
、 Put
、Delete
这3个接口函数实现。接口定义如下:
Status Get(const ReadOptions& options, const Slice& key,
std::string* value);
Status Put(const WriteOptions&, const Slice& key,
const Slice& value);
Status Delete(const WriteOptions&, const Slice& key);
- ReadOptions& options:代表实际读操作传入的行为参数。
- WriteOptions& options:代表实际写操作传入的行为参数。
这里需要注意的是 Delete
并不会直接删除数据,而是在对应位置插入一个 key 的删除标志,然后在后续的Compaction 过程中才最终去除这条 key-value 记录。
代码示例如下:
#include"leveldb/db.h"
#include"leveldb/slice.h"
#include<iostream>
using namespace std;
int main()
leveldb::DB* db;
leveldb::Options op;
op.create_if_missing = true;
//打开数据库
leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
if(!status.ok())
cout << status.ToString() << endl;
exit(1);
//写入数据
leveldb::Slice key("hello");
string value("world");
status = db->Put(leveldb::WriteOptions(), key, value);
if(status.ok())
cout << "key: " << key.ToString() << " value: " << value << " 写入成功。" << endl;
//查找数据
status = db->Get(leveldb::ReadOptions(), key, &value);
if(status.ok())
cout << "key: " << key.ToString() << " value: " << value << " 查找成功。" << endl;
//删除数据
status = db->Delete(leveldb::WriteOptions(), key);
if(status.ok())
cout << "key: " << key.ToString() << " 删除成功。" << endl;
//关闭数据库
delete db;
return 0;
批量处理
针对大量的操作,LevelDB 不具有传统数据库所具备的事务操作机制,然而它提供了一种批量操作的方法。这种批量操作方法主要具有两个作用:一是提供了一种原子性的批量操作方法;二是提高了整体的数据操作速度。
LevelDB 针对批量操作定义了 WriteBatch 的类型。WriteBatch 有 3 个非常重要的接口:数据写(Put)、数据删 除(Delete)以及清空批量写入缓存(Clear),具体定义如下所示:
class LEVELDB_EXPORT WriteBatch
public:
//...
void Put(const Slice& key, const Slice& value);
void Delete(const Slice& key);
void Clear();
//...
;
当我们想将 WriteBatch 中的数据写入 DB 时,只需要调用 Write
接口,其主要用于处理之前保存在 WriteBatch 对象中的所有批量操作,其详细接口定义如下所示:
Status Write(const WriteOptions& options, WriteBatch* updates);
- 注意:一旦我们写入完成后,就会调用 updates 中的
Clear
来清空之前保存的批量操作。
代码示例如下:
#include"leveldb/db.h"
#include"leveldb/slice.h"
#include"leveldb/write_batch.h"
#include<iostream>
using namespace std;
int main()
leveldb::DB* db;
leveldb::Options op;
op.create_if_missing = true;
//打开数据库
leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
if(!status.ok())
cout << status.ToString() << endl;
exit(1);
//批量写入
leveldb::Slice key;
string value;
leveldb::WriteBatch batch;
for(int i = 0; i < 10; i++)
value = ('0' + i);
key = "k" + value;
batch.Put(key, value);
status = db->Write(leveldb::WriteOptions(), &batch);
if(status.ok())
cout << "批量写入成功" << endl;
//批量删除
for(int i = 0; i < 10; i++)
key = "k" + ('0' + i);
batch.Delete(key);
status = db->Write(leveldb::WriteOptions(), &batch);
if(status.ok())
cout << "批量删除成功" << endl;
//关闭数据库
delete db;
return 0;
迭代器遍历
针对 DB 中所有的数据记录,LevelDB 不仅支持前向的遍历,也支持反向的遍历。在 DB 对象类型中,通过调用NewIterator
创建一个新的迭代器对象,该接口具体定义如下:
Iterator* NewIterator(const ReadOptions&);
- ReadOptions& option:用于指定在遍历访问过程中的相关设置。
这里有一个需要注意的地方,这里返回的迭代器不能直接使用,而是需要先使用对应的 Seek
操作偏移到指定位置后才能进行对应的迭代操作。
代码示例如下:
#include"leveldb/db.h"
#include"leveldb/slice.h"
#include"leveldb/iterator.h"
#include<iostream>
using namespace std;
int main()
leveldb::DB* db;
leveldb::Options op;
op.create_if_missing = true;
//打开数据库
leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
if(!status.ok())
cout << status.ToString() << endl;
exit(1);
leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
//正向遍历
cout << "开始正向遍历:" << endl;
for(it->SeekToFirst(); it->Valid(); it->Next())
cout << "key: " << it->key().ToString() << " value: " << it->value().ToString() << endl;
//逆向遍历
cout << "开始逆向遍历:" << endl;
for(it->SeekToLast(); it->Valid(); it->Prev())
cout << "key: " << it->key().ToString() << " value: " << it->value().ToString() << endl;
//范围查询
cout << "开始范围查询:" << endl;
for(it->Seek("k4"); it->Valid() && it->key().ToString() < "k8"; it->Next())
cout << "key: " << it->key().ToString() << " value: " << it->value().ToString() << endl;
delete it;
//关闭数据库
delete db;
return 0;
常用优化方案
压缩
当每一个块写入存储设备中时,可以选择是否对块进行压缩后再存储,以及 Options 参数的 compression 成员变量决定是否开启压缩。默认情况下,压缩是开启的,且压缩的速度很快,基本对整体的性能没有太大影响
用户在调用时,可以用 kNoCompression 或 kSnappyCompression 对 compression 参数进行设定,从而确定块在实际存储过程中是否进行压缩。
leveldb::Options op;
op.compression = leveldb::kNoCompression; //不启用压缩
op.compression = leveldb::kSnappyCompression; //Snappy压缩
缓存
Cache的作用是充分利用内存空间,减少磁盘的 I/O 操作,从而提升整体运行性能。LevelDB 默认的 Cache 采用的是 LRU 算法,即近期最少使用的数据优先从 Cache 中淘汰,而经常使用的数据驻留在内存,从而实现对需要频繁读取的数据的快速访问。
LevelDB 中定义了一个全局函数 NewLRUCache
用于创建一个 LRUCache。
leveldb::Options op;
op.block_cache = leveldb::NewLRUCache(10 * 1024 * 1024);
//参数主要用于指定LevelDB的块的Cache空间,如果为NULL则默认为8MB
过滤器
由于 LevelDB 中所有的数据均保存在磁盘中,因而一次 Get 的调用,有可能导致多次的磁盘 I/O 操作。为了尽可能减少读过程时磁盘 I/O 的操作次数,LevelDB 采用了 FilterPolicy 机制。LevelDB 中 Options 对象类型的filter_policy 参数,主要用于确定运行过程中 Get 所遵循的 FilterPolicy 机制。
用户可以通过调用 NewBloomFilterPolicy
接口函数以创建布隆过滤器,并将其赋值给对应的 filter_policy 参数。
leveldb::Options op;
op.filter_policy = leveldb::NewBloomFilterPolicy(10);
命名
LevelDB 中磁盘数据读取与缓存均以块为单位,并且实际存储中所有的数据记录均以 key 进行顺序存储。根据排序结果,相邻的 key 所对应的数据记录一般均会存储在同一个块中。正是由于这一特性,用户针对自身的应用场景需要充分考虑如何优化 key 的命名设计,从而最大限度地提升整体的读取性能。
为了提升性能,命名规则是:针对需要经常同时访问的数据,其 key 在命名时,可以通过将这些 key 设置相同的前缀保证这些数据的 key 是相邻近的,从而使这些数据可存储在同一个块内。 基于此,那些不常用的数据记录自然会放置在其他块内。
以上是关于LevelDB 源码剖析:环境搭建实战使用常用优化的主要内容,如果未能解决你的问题,请参考以下文章
LevelDB 源码剖析整体架构与基本组件:ComparatorSliceStatusIteratorOption
LevelDB 源码剖析整体架构与基本组件:ComparatorSliceStatusIteratorOption
LevelDB 源码剖析整体架构与基本组件:ComparatorSliceStatusIteratorOption
LevelDB 源码剖析MemTable模块:SkipListMemTable持久化