通过 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
会同时继承 ColumnFamilyOptions
和 DBOptions
,所以,对于只有一个列族的实例仍然可以使用 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()
函数查看具体的失败原因。