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;


数据读、写、删除

数据库中的读、写与删除操作分别用 GetPutDelete 这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持久化

LevelDB 源码剖析MemTable模块:SkipListMemTable持久化

LevelDB 源码剖析MemTable模块:SkipListMemTable持久化