大家好,今天开始RocksDB第三课之旅。整个课程来自于官网的wiki,加入了我自己的一些理解和例子。今天主要介绍的是读取、写入和并发。
读取
Rocksdb提供了四种方法用于查询/修改数据库,包括Put
、Delete
、Get
和MultiGet
方法。
std::string key1 = "id1";std::string key2 = "id2";std::string value1 = "hello world";std::string value2 = "I love rocksdb";std::string value;//打开数据库rocksdb::Status status = rocksdb::DB::Open(options, "/tmp/testdb", &db);//插入数据status=db->Put(rocksdb::WriteOptions(), key1, value1);status=db->Put(rocksdb::WriteOptions(), key2, value2);//读取数据status = db->Get(rocksdb::ReadOptions(), key1, &value);//删除数据status = db->Delete(rocksdb::WriteOptions(), key1);
复制
当前value的大小必须小于4GB。RocksDB还允许使用Single Delete
这个功能,官方文档说在特定场景下非常有用。
每个get请求至少需要使用一次从源地址到目标地址字符串值memcpy拷贝,如果源在块中缓存,你可以使用一个PinnableSlice
来避免额外的拷贝。
memcpy,在c和c++中函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中
PinnableSlice pinnable_val;rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &pinnable_val);
复制
假设数据块在内存中,用户使用db->Get函数,传进来的是一个string,这样需要将内存中的数据拷贝到传入进来的string中,就产生了memcpy,当value值很大时,memcpy 开销是非常大的。
如果用户使用db->Get函数传进来的是一个PinnableSlice
对象,会直接将数据地址赋给PinnableSlice
,也就是说用户最终拿到的值的地址其实就在这个Block中,这样就减少了一次数据拷贝。
源数据会在pinnable_val被销毁或者调用pinnable_val.Reset()的时候释放。
数据库读取多个键时,可以使用MultiGet
。
以更高性能的方式从单个列族读取多个键,它可以比循环调用 GET
更快,跨多个列族读取彼此一致的键。
接下来搞个demo测试一下,例子很简单,就是插入3条数据,用MultiGet
一次性读出来。
#include <cassert>#include <rocksdb/db.h>#include <iostream>using namespace std;int main(int argc, char** argv) { rocksdb::DB* db; rocksdb::Options options; options.create_if_missing = true; //打开数据库 rocksdb::Status status = rocksdb::DB::Open(options, "/tmp/testdb", &db); assert(status.ok()); //插入数据 db->Put(rocksdb::WriteOptions(), "key1", "value1"); db->Put(rocksdb::WriteOptions(), "key2", "value2"); db->Put(rocksdb::WriteOptions(), "key3", "value3"); //MultiGet读取数据 std::vector<rocksdb::PinnableSlice> values(3); std::vector<rocksdb::Status> statuses(3); std::vector<rocksdb::Slice> querySlices; querySlices.emplace_back("key1"); querySlices.emplace_back("key2"); querySlices.emplace_back("key3"); db->MultiGet(rocksdb::ReadOptions(),db->DefaultColumnFamily(),3,querySlices.data(),values.data(),statuses.data()); for(int i=0;i<values.size();i++) { cout<<values[i].ToString()<<endl; } //关闭数据库 status= db->Close(); delete db;}
复制
执行程序,结果输出如下。

写入
原子更新
假设有这样一个场景,第一个操作是删除某个key,第二个操作是插入另外一个key。他们两个key的value值是一样的。这两个操作要遵从原子性。也就是要么都成功,要么都失败。如果第一个操作失败,第二个操作成功的话,此时会导致虽然key不同,但是存入的值是相同的。这个问题可以通过WriteBatch
原子地写入一批更新。
#include <cassert>#include <iostream>#include <rocksdb/db.h>#include "rocksdb/write_batch.h"using namespace std;int main(int argc, char** argv) { rocksdb::DB* db; rocksdb::Options options; options.create_if_missing = true; //打开数据库 rocksdb::Status status = rocksdb::DB::Open(options, "/tmp/testdb", &db); assert(status.ok()); //插入数据 status=db->Put(rocksdb::WriteOptions(), "key1", "value1"); //原子更新 if (status.ok()) { rocksdb::WriteBatch batch; batch.Delete("key1"); batch.Put("key2", "test batch"); status = db->Write(rocksdb::WriteOptions(), &batch); } //读取最新数据 status = db->Get(rocksdb::ReadOptions(),"key1", &value); cout<<status.ToString()<<endl; status = db->Get(rocksdb::ReadOptions(),"key2", &value); cout<<status.ToString()<<endl; //关闭数据库 status= db->Close(); delete db;}
复制
WriteBatch
保存一个数据库编辑序列,这些批处理的修改会被按顺序应用于数据库。我们再次编译并执行程序,可以发现key1已经找不到了,而key2是可以读到数据的。这里就算意外崩溃也会全部回滚。

原子操作除了这个优点之外,WriteBatch
还可以将大量单独修改合并到一个批处理里面,加快批量更新。
同步写入
默认情况下,每次写入rocksdb
都是异步的,进程写入然后发送到操作系统后立即返回。从操作系统的内存到底层存储之间的传输是异步的。可以为特定的写入打开sync
选项,这样需要等操作系统内存刷新到持久存储之后才会返回。
rockdb::WriteOptions write_options; write_options.sync = true ;
db-> Put (write_options, ...);复制
非同步写入
对于非同步写入,RocksDB 仅在OS buffer
或者internal buffer
中缓冲WAL写入(设置options.manual_wal_flush = true
时),它要比同步写入快很多。但是它的缺点很明显,就是在机器崩溃的时候,最后几个更新会丢失。这里需要注意的一点是,仅仅写入进程崩溃(主机没重启)并不会导致任何损失,即使你的write_options.sync
参数设置为false。因为在从进程内存刷到操作系统之前,你的更新就被认定已经完成了。
官方文档这里理解起来有点绕,其实和大多数数据库一样,一般都是有写WAL日志的,你的进程内存的数据没刷到操作系统无所谓啊,只要写入到WAL日志,就认为已经写入完成了。
通常可以安全的使用非同步写入。例如将大量数据加载到数据库中的,你可以在crash之后,重启bulk load进程来处理丢失的更新。混合方案也是可行的,在每个单独的线程中调用DB::SyncWAL()
。
还有一种完全禁止写入WAL日志的方法,设置write_options.disableWAL
为true。这样写入就不会记录到WAL日志中,当写入进程崩溃的时候就会丢数据。
RocksDB默认会使用datasync()
去同步文件,在某些情况下可能要比fsync()
更快。如果要使用fsync()
,你可以设置Options::use_fsyn=true
。在ext3这样的文件系统上重启可能会丢失文件,因此应该将此设置为true。
高级
有关写入性能优化和影响性能的因素,可以参考Pipelined Write
和Write Stalls
。
并发
一个数据库在一个时间内仅仅只能由一个进程打开,rocksdb的实现方式是,从操作系统哪里申请到一个锁,以此来阻止错误的写操作。在单进程里面,同一个rocksdb::DB
对象可以被多个同步线程共享。举个例子,不同的线程可以同时对同一个数据库调用写操作,迭代遍历操作或者Get操作,而且不需要使用额外的同步锁(rocksdb实现会自动进行同步)。然而其他对象(比如迭代器,WriteBatch
)需要额外的同步机制保证线程同步。如果两个线程共同使用这些对象,他们必须使用自己的锁协议保证访问的同步。公共头文件中提供了更多详细信息。
在iterator.h
和write_batch.h
这些文件能找到类似的说明。

后记
RocksDB已经分享了三篇文章了。这个系列我会坚持写完。前两篇的链接:
Refenerce
https://github.com/facebook/rocksdb/wiki/Basic-Operations