RocksDB 基本介绍

2022-09-21 database

通过 CPP 开发的嵌入式 KV 数据库,源自 LevelDB 但是进行了极大的优化。

简介

嵌入式数据库 RocksDB 是 Facebook 基于 Google 的 LevelDB 1.5 版本开发的一种嵌入式 Key-Value 存储系统,并针对闪存做了特定的优化,可以充分利用闪存的性能,相比 LevelDB 在性能上有很大的提升。

区别于 LevelDB 的特性可以参考 RocksDB Features that are not in LevelDB 中的内容,不过如 Wiki 所示,新增的特性只是更新到 2016 为止。

安装

编译安装非常简单,最简单的可以只编译静态库、动态库以及一些常用的工具,也可以通过 make all 编译所有,如果直接使用 make 编译默认为 Debug 模式。

make DEBUG_LEVEL=0

make shared_lib
make install-shared INSTALL_PATH=/opt/rocksdb

make static_lib
make install-static INSTALL_PATH=/opt/rocksdb
make install

make release

编译完后,会在当前目录下生成 librocksdb.a 静态库,librocksdb.so.X.X.X 动态库以及部分符号链接。

在安装脚本中定义了 INSTALL_PATH 变量,用来定义安装路径的前缀,默认是 /usr/local 目录,此时会将头文件安装到 /usr/local/include/rocksdb 目录下,库安装到 /usr/local/lib 目录下。

如果如上指定了 INSTALL_PATH 则会安装到 /opt/rocksdb 目录下,那么在编译的时候就需要添加对应的头文件以及库文件路径。

当然,因为默认会安装头文件,所以手动复制安装库,例如:

cp librocksdb.so.6.4.6 /usr/lib
ln -s librocksdb.so.6.4.6 /usr/lib/librocksdb.so.6
ln -s librocksdb.so.6.4.6 /usr/lib/librocksdb.so

除了上述的头文件、动态库、静态库之外,还有一些常用的工具吧,包括了管理、调试、压测等,例如 ldb sst_dump db_stress 等等,可以通过类似 make ldb 的命令按需编译。

注意,貌似不同的工具之间相互有依赖,如果是安装后简单测试用,可以直接使用 make clean && make all 进行编译。

示例

如下是简单写入并读取数据的示例。

#include <cstdio>
#include <iostream>

#include "rocksdb/db.h"
#include "rocksdb/slice.h"
#include "rocksdb/options.h"

using namespace rocksdb;

int main(void)
{
        DB *db;
        Status rc;
        Options opt;

        opt.create_if_missing = true;
        rc = DB::Open(opt, "/tmp/rocks.db", &db);
        if (!rc.ok()) {
                std::cerr << rc.ToString() << std::endl;
                return -1;
        }

        //----- Write & Read
        Slice key("Hi");
        Slice value("HelloWorld");
        std::string data;

        // rc = db->Put(WriteOptions(), "Hi", "HelloWorld");
        rc = db->Put(WriteOptions(), key, value);
        if (!rc.ok()) {
                std::cerr << "Put failed" << rc.ToString() << std::endl;
                delete db;
                return -1;
        }

        // rc = db->Get(ReadOptions(), "Hi", &data);
        rc = db->Get(ReadOptions(), key, &data);
        if (rc.ok()) {
                std::cout << "Got value " << data.c_str() << std::endl;
        } else {
                std::cout << "Got value failed" << rc.ToString() << std::endl;
        }

        //----- Batch Operation
        WriteBatch bat;
        bat.Delete("Hi");
        bat.Put("Hey", value);
        rc = db->Write(WriteOptions(), &bat);
        assert(rc.ok());

        rc = db->Get(ReadOptions(), "Hi", &data);
        assert(rc.IsNotFound());

        rc = db->Get(ReadOptions(), "Hey", &data);
        assert(data == "HelloWorld");

        rc = db->Delete(WriteOptions(), key);
        if (!rc.ok()) {
                std::cerr << "Delete failed" << rc.ToString() << std::endl;
                delete db;
                return -1;
        }

        delete db;
        return 0;
}

通过 Options 定义了打开数据库时的配置,其中的部分选项可以在运行时修改,在打开数据库之后,可以通过 Put() Get() Delete() 之类的接口进行操作,返回为 Status 类型,封装了具体的返回信息。

也可以通过 WriteBatch 执行批量原子操作,如果上述的 Delete()Put() 操作的 Key 相同,实际上不会做任何修改操作。

可以通过如下方式进行编译。

g++ -std=c++11 -o rocksdbtest main.cpp -lrocksdb -lpthread -ldl
g++ -std=c++11 -o rocksdbtest main.cpp /usr/local/lib/librocksdb.a -lpthread -lz -ldl

RocksDB 包含了三类文件:A) MemFile 保存在内存中是最新的数据;B) SSTFile 当 MemFile 写满之后,会写入到磁盘文件 SSTFile 中,为了方便查找,数据是顺序保存的;C) LogFile 是顺序写入的日志。

ColumnFamily

在 3.0 版本中增加了对 Column Families 的支持,每个 KV 都与唯一的列族结合,如果不指定则使用默认的 default 列族,实际上,就是通过列族提供一种逻辑上的数据库分片方法。

实现时,每个 ColumnFamily 有自己的 MemTable 和 SST 文件,相互独立,而 WAL、Current、Manifest 则是共享的,这样就很方便修改列族的不同属性,快速添加删除列族。

列族支持如下的一些特性:

  • 支持跨列族原子写。
  • 提供跨列族的一致性视图。
  • 允许对不同的列族进行不同的配置。
  • 可以快速添加或删除列族。

接着看看如何通过 API 使用列族。

示例

简单的创建 ColumnFamily 并读取数据的过程。

#include <iostream>

#include "rocksdb/db.h"
#include "rocksdb/options.h"

using namespace rocksdb;

int main(void)
{
        DB *db;
        Status rc;
        Options opt;

        opt.create_if_missing = true;
        rc = DB::Open(opt, "/tmp/rocks.db", &db);
        assert(rc.ok());

        ColumnFamilyHandle *cf;
        rc = db->CreateColumnFamily(ColumnFamilyOptions(), "NewColumnFamily", &cf);
        assert(rc.ok());

        rc = db->Put(WriteOptions(), cf, "Hi", "FooBar");
        assert(rc.ok());

        std::string Value;
        rc = db->Get(ReadOptions(), cf, "Hi", &Value);
        assert(rc.ok());

        std::cout << "Got value " << Value << std::endl;

        rc = db->DestroyColumnFamilyHandle(cf);
        assert(rc.ok());

        delete db;
        return 0;
}

注意,上述的示例只能运行一次,因为创建了新的列族之后并没有删除,在打开 RocksDB 的时候,需要将所有的列族打开,否则在调用 DB::Open() 的时候会直接报 Status::InvalidArgument() 的错误。

使用详解

其中 Options 定义了 RocksDB 的行为,单个列族的选项可以通过 ColumnFamilyOptions 设置,整个 RocksDB 实例配置也可以在 DBOptions 中设置,而 Options 会同时继承 ColumnFamilyOptionsDBOptions,所以,对于只有一个列族的实例仍然可以使用 Options 配置。

在操作列族的时候,会使用到 ColumnFamilyHandle 的对象,可以将其理解为一个描述符,使用完之后需要将其删除。注意,仍然可以使用一个已经删除的列族句柄,只有当实例中所有的句柄都删除了,才会清理。

如下是打开多个列族的示例。

#include <iostream>

#include "rocksdb/db.h"
#include "rocksdb/options.h"

using namespace rocksdb;

int main(void)
{
        DB *db;
        Status rc;

        std::vector<ColumnFamilyDescriptor> cfs;
        cfs.push_back(ColumnFamilyDescriptor(kDefaultColumnFamilyName, ColumnFamilyOptions()));
        cfs.push_back(ColumnFamilyDescriptor("NewColumnFamily", ColumnFamilyOptions()));

        std::vector<ColumnFamilyHandle*> handles;
        rc = DB::Open(DBOptions(), "/tmp/rocks.db", cfs, &handles, &db);
        assert(rc.ok());

        rc = db->Put(WriteOptions(), handles[1], "Hi", "FooBar");
        assert(rc.ok());

        std::string Value;
        rc = db->Get(ReadOptions(), handles[1], "Hi", &Value);
        assert(rc.ok());

        WriteBatch batch; // atomic write
        batch.Put(handles[0], "Foo", "Hello");
        batch.Put(handles[1], "Bar", "World");
        batch.Delete(handles[0], "Hi");
        rc = db->Write(WriteOptions(), &batch);
        assert(rc.ok());

        rc = db->DropColumnFamily(handles[1]);
        assert(rc.ok());

        rc = db->Get(ReadOptions(), handles[1], "Bar", &Value);
        assert(rc.ok());
        std::cout << "Got value " << Value << std::endl;

        for (auto handle : handles) {
                rc = db->DestroyColumnFamilyHandle(handle);
                assert(rc.ok());
        }

        delete db;
        return 0;
}

在打开多个列族时,需要使用一个 ColumnFamilyDescriptors 类型的 Vector 来声明,该描述符中只包含了列族名称以及 ColumnFamilyOptions 选项,打开后会返回对于的句柄 Vector ,然后就可以通过句柄使用列族。

另外,如果是通过 DB::OpenForReadOnly() 接口以只读方式打开,那么就不需要声明所有列族,可以选择其中一个子集。

其它

注意,其中的 assert() 一般只用在调试代码时,生产环境建议不要使用,所以其中的异常判断只是为了简化,如果需要,可以通过 ToString() 函数查看具体的失败原因。