rocksdb介绍之数据写入流程

本文深入剖析RocksDB的数据写入流程,包括写入入口、WriteBatch、多线程并发控制及WriteToWAL等步骤。重点解析了WriteBatch的内容结构及其在内存与硬盘间的流转,同时探讨了并发写入时的线程协调机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

rocksdb是一个kv数据库,其介绍可以参考http://vpha.rd.tp-link.net/phame/post/view/3566/rocksdb_%E5%AD%98%E5%82%A8%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D/

本文主要介绍rocksdb写入数据的流程与相关代码,因为是边学习边写的,可能有一些疏漏请读者见谅。

二、写入入口

rocksdb的写入操作大致可以分为4中:put, merge, deletion, range deletion。

put指的是插入操作,插入一个kv对。
merge是更新操作,修改一个值,例如可以+1。
deletion是删除操作,删除一个kv对。
range deletion是删除操作,删除k在一定范围内的所有kv对。

本文主要看比较基础的put操作。

一个比较简单的写入例程如下,插入了一个kv对"key1" “value”:

std::string kDBPath = "/tmp/rocksdb_simple_example";

int main() {
  DB* db;
  Options options;

  Status s = DB::Open(options, kDBPath, &db);
  s = db->Put(WriteOptions(), "key1", "value");
  delete db;
  return 0;
}

实际写入时,数据库内部会使用WriteBatch将"key1" "value"这个kv对进行封装,再调用实际的写入接口。
WriteBatch也可以存放多个写入请求,可以由外部进行组合:

std::string kDBPath = "/tmp/rocksdb_simple_example";

int main() {
  DB* db;
  Options options;

  Status s = DB::Open(options, kDBPath, &db);
  {
    WriteBatch batch;
    batch.Delete("key1");
    batch.Put("key2", value);
    s = db->Write(WriteOptions(), &batch);
  }
  delete db;
  return 0;
}

以上步骤完成后,数据就写入到硬盘了,但首先是写入到wal中,并在memtable中进行缓存,并没有写入到sst文件中,需要等待后续进行flush操作时才可以写入到sst文件。

二、整体写入流程

在这里插入图片描述

图片来源于网络。

可能有多个线程同时调用rocksdb的write,此时会选择一个线程作为leader,选择一些batch组成一个group,一起写入到wal,而其他线程则作为follower,等待leader完成wal的写入,之后各个线程并发写入memtable。
此过程中,可能有新的write请求,或先挂在队列上,需要等待上一个leader完成wal的写入后才能进行后续处理。

三、WriteBatch

以下是实际运行过程中,使用gdb打印处理的WriteBatch内容

(gdb) p *my_batch
$2 = {
  <rocksdb::WriteBatchBase> = {_vptr.WriteBatchBase = 0x555555ce36e8 <vtable for rocksdb::WriteBatch+16>}, 
  save_points_ = std::unique_ptr<rocksdb::SavePoints> = {get() = 0x0}, 
  wal_term_point_ = {
    size = 0, 
    count = 0,
    content_flags = 0
  }, 
  content_flags_ = {
    <std::__atomic_base<unsigned int>> = {static _S_alignment = 4, _M_i = 2},
    <No data fields>
  }, 
  max_bytes_ = 0, 
  is_latest_persistent_state_ = false,
  prot_info_ = std::unique_ptr<rocksdb::WriteBatch::ProtectionInfo> = {get() = 0x0}, 
  rep_ = "\000\000\000\000\000\000\000\000\001\000\000\000\001\003key\006value", 
  timestamp_size_ = 0
}

数据主要存放在rep_变量中,是一个字符串,官方的说明注释如下:

// WriteBatch::rep_ :=
//    sequence: fixed64
//    count: fixed32
//    data: record[count]
// record :=
//    kTypeValue varstring varstring
//    kTypeDeletion varstring
//    kTypeSingleDeletion varstring
//    kTypeRangeDeletion varstring varstring
//    kTypeMerge varstring varstring
//    kTypeColumnFamilyValue varint32 varstring varstring
//    kTypeColumnFamilyDeletion varint32 varstring
//    kTypeColumnFamilySingleDeletion varint32 varstring
//    kTypeColumnFamilyRangeDeletion varint32 varstring varstring
//    kTypeColumnFamilyMerge varint32 varstring varstring
//    kTypeBeginPrepareXID varstring
//    kTypeEndPrepareXID
//    kTypeCommitXID varstring
//    kTypeRollbackXID varstring
//    kTypeBeginPersistedPrepareXID varstring
//    kTypeBeginUnprepareXID varstring
//    kTypeNoop
// varstring :=
//    len: varint32
//    data: uint8[len]

再看rep_的值"\000\000\000\000\000\000\000\000\001\000\000\000\001\003key\006value"
可以拆分成:
sequence: “\000\000\000\000\000\000\000\000“,转化成数字就是0,是一个序号
count: ”\001\000\000\000”,转化成数字就是1,只有一个kv对
data: “\001\003key\006value”,只有一个kv对,可以进一步拆分成具体内容
type: “\001”,就是kTypeValue,对应 kTypeValue varstring varstring,也就是跟者两个varstring,分别是key和value
key: “\003key”,3表示key的长度
value: “\006value”,6表示value的长度,这里包括了\0的长度,这是因为写入的时候指定的长度是6,而写入key的时候指定的长度是3所以不包含\0

四、JoinBatchGroup

写数据基本在函数DBImpl::WriteImpl中进行,进行的第一个主要流程是JoinBatchGroup。

JointBatchGroup:

  1. 首先调用WriteThread::LinkOne函数,将请求挂到链表上,这个链表可能会包含多个请求,后续会通过一次io请求一起写入到wal中。挂到链表上的同时,会判断是否是该链表上的第一个请求,如果是第一个请求,那么这个请求就会作为leader,leader负责写wal,其他请求等待wal写完之后再继续。
  2. 如果不是leader,就会调用WriteThread::AwaitState函数,等待leader的唤醒。AwaitState的实现也比较复杂,主要是为了优化性能,leveldb中是直接使用条件变量的,rocksdb做了优化。参考https://www.cnblogs.com/cobbliu/articles/8511269.html

这里有个技巧是使用了CAS,多线程可以无锁并发执行。

五、leader

JoinBatchGroup后,确定了leader,下面看leader的处理流程。

PreprocessWrite

1.检查wal大小,如果超过了配置的最大大小,需要调用SwitchWAL;
2.检查memtable大小,如果超过了配置的最大大小,需要调用HandleWriteBufferFull
3.调用ScheduleFlushes,
4.检查是否需要执行DelayWrite,也就是发生了write stall,通常是因为flush或者compaction不及时
5.检查是否需要等待log sync完成,需要则等待直到logs_.front().getting_synced变为true。?为什么写之前等待

SwitchWal:

触发的原因是wal的大小超过了配置的上限,该配置可以通过修改option中的max_total_wal_size进行修改。一般来说,应该是先触发memtable满进行flush的,这个条件触发的应该比较少。

首先选择哪些column family需要进行处理,也就是判断wal中包含哪些column family的内容,判断条件是cfd->OldestLogToKeep() <= oldest_alive_log,OldestLogToKeep记录的是包含该column family的wal的最小序号。

然后对挑选出来的column falimy执行SwitchMemtable,使得新数据写入到新的Memtable中,以便旧的Memtable可以进行flush。
SwitchMemtable:
1.创建一个新的wal
2.将当前的memtable放入到immutable memtable链表中,再创建一个新的memtable
3.调用InstallSuperVersionAndScheduleWork,更新SuperVersion。并且调用MaybeScheduleFlushOrCompaction检查是否要进行flush或者compaction,这里应该是需要进行flush的,但flush的执行不是再这里,而是由专门的flush线程进行。也可不是这里,后面会再生成一个flush请求。

最后,创建了一个flush请求,等待调度执行。

HandleWriteBufferFull

首先选择哪些column family需要进行处理,判断条件是!cfd->mem()->IsEmpty(),也就是memtable中有数据

然后对挑选出来的column falimy执行SwitchMemtable

最后,创建了一个flush请求,等待调度执行。

与SwitchWal的执行流程比较相似。

DelayWrite和log sync先不介绍,不是主干逻辑。

EnterAsBatchGroupLeader

在这里插入图片描述

图片来源:https://blog.csdn.net/qq_43479736/article/details/109056437

每个write_batch会用一个writer类进行封装,writer类会组成一个链表,链表的最新元素记录在指针WriteThread::newest_writer_。
这个链表是可以随时插入新节点的,在写入过程中也可以插入,leader会选择尽量多的Writer组成一个Write Group,批量写入到Wal。

EnterAsBatchGroupLeader的工作,就是选择可以组合到一起的batch,构建group供后续处理。

WriteToWAL

下一步的主要流程是调用WriteToWAL将group写入到wal中。

首先调用MergeBatch将group中的所有batch合为一个,合并的方法其实就是把所有batch的rep_合并成一个字符串。

然后调用log_writer->AddRecord,将合并的batch中的内容加入到log_writer的dest_成员中,此时并没有写入到硬盘,还是在内存中。然后AddRecord函数中,会判断manual_flush_,如果没有设置manual_flush_,那么就需要写入硬盘,调用dest_->Flush()写入硬盘。

memtable

写完wal之后,再把数据写入到内存的memtable中。

我们主要看可以并发写入的memtable的写入流程,使用的数据结构是skiplist。

首先,调用LaunchParallelMemTableWriters,将write_group中的所有writer设为状态STATE_PARALLEL_MEMTABLE_WRITER,这样follower可以并发写入。

然后是写入leader自己的memtable,memtable内容较多,具体内容先不介绍了,后续可以单独讲一篇。

六、follower

follower在JoinBatchGroup,没有被分配到leader角色,需要等待leader完成一些任务后(一般情况就是写完wal)再进行后续的处理,等待是调用AwaitState函数进行的:

    AwaitState(w, STATE_GROUP_LEADER | STATE_MEMTABLE_WRITER_LEADER |
                      STATE_PARALLEL_MEMTABLE_WRITER | STATE_COMPLETED,
               &jbg_ctx);

一般来说,使用的memtable是可以并行写入的,在leader的介绍中,说明了调用LaunchParallelMemTableWriters时,会给follower设置STATE_PARALLEL_MEMTABLE_WRITER状态,此时follower就会被唤醒了,然后进入写memtable的流程,这个流程与leader的流程是类似的。

### 回答1: "fatal: failed to load library 'libcurl-4.dll" 是一个错误信息,通常出现在使用某些软件或应用程序时。这个错误表示系统无法加载 libcurl-4.dll 动态链接库文件。 libcurl-4.dll 是一个用于支持网络通信的动态链接库,它提供了各种网络协议和功能的实现。当软件或应用程序需要使用 libcurl-4.dll 时,操作系统会尝试从指定的路径加载该库文件。然而,如果指定的路径无效或者库文件本身存在问题,就会出现 "fatal: failed to load library 'libcurl-4.dll" 错误。 要解决这个问题,可以尝试以下步骤: 1. 检查 libcurl-4.dll 的路径:确保该文件存在于软件或应用程序所需的路径下。如果文件丢失或被删除,需要重新安装相关软件或应用程序。 2. 检查系统环境变量:有时操作系统无法找到 libcurl-4.dll 是因为环境变量没有正确设置。可以在系统的环境变量中添加正确的路径,并确保路径包含在 PATH 变量中。 3. 更新 libcurl-4.dll:如果库文件本身损坏或过时,可以尝试下载最新的 libcurl-4.dll 版本,并替换当前使用的文件。 4. 检查软件或应用程序的依赖关系:某些软件或应用程序需要其他库文件的支持,如果其中任何一个丢失或损坏,都会导致 "fatal: failed to load library 'libcurl-4.dll" 错误。可以重新安装相关软件或应用程序,或者尝试修复其他缺失的库文件。 如果上述方法仍然无法解决问题,还可以查询相关软件或应用程序的官方支持渠道,寻求更详细的帮助和解决方案。 ### 回答2: 当你在使用或运行某个程序时,如果出现了“fatal: failed to load library 'libcurl-4.dll'”的错误提示,意味着你的计算机缺少了“libcurl-4.dll”这个库文件或者该库文件无法被正确加载。 这个错误通常是由以下几种原因引起的: 1. 缺失库文件:你的计算机上可能没有安装或者该库文件被意外删除了。解决这个问题的方法是下载并安装最新版本的 libcurl-4.dll,或者从可靠的来源获取之前正常工作的文件。 2. 不兼容的库文件版本:你的程序要求的是特定版本的 libcurl-4.dll,但你的计算机上安装了不匹配的版本。在这种情况下,你可以尝试升级你的库文件到与程序要求的版本相匹配,或者尝试与程序兼容的较新版本。 3. 环境变量设置错误:你的计算机可能没有正确设置环境变量,导致操作系统无法找到 libcurl-4.dll 文件的位置。你可以通过检查环境变量设置并添加正确的路径来解决这个问题。 4. 程序文件损坏:有时,libcurl-4.dll 文件本身可能被损坏。你可以尝试重新下载并替换该文件,确保你使用的是完整且未损坏的文件。 总之,解决这个错误的关键在于确保计算机上正确安装了所需的库文件,并且文件能够被正确加载。如果以上方法无效,你可以尝试重新安装或者更新相关的程序,或者咨询专业人士以获得更详细的帮助。 ### 回答3: 这个错误表示在运行某个程序时,系统无法加载'libcurl-4.dll'库文件。这个库文件是用于支持网络通信功能的,通常与网络连接、数据传输等操作相关。这个错误可能有以下几种原因和解决方法: 1. 缺少库文件:最常见的情况是缺少'libcurl-4.dll'文件。解决方法是重新安装或更新相关程序或软件。可以尝试下载并安装最新版本的 libcurl 库,并确保它位于正确的目录下。 2. 版本不匹配:可能库文件的版本与程序或软件的要求不匹配。可以尝试使用与程序或软件兼容的特定版本的'libcurl-4.dll'库文件。 3. 文件损坏:如果库文件损坏或被破坏,就会导致加载失败。可以尝试重新下载库文件,或者从可信的来源获得原始文件并替换。 4. 环境变量设置错误:在某些情况下,系统无法正确查找到库文件的路径。可以通过设置正确的环境变量来解决这个问题。确保将库文件所在的目录添加到系统的PATH环境变量中。 5. 冲突或错误的系统配置:某些软件或驱动程序可能会导致冲突或干扰,从而无法加载库文件。可以尝试重新安装相关程序,或者更新操作系统和驱动程序来解决冲突问题。 总之,'fatal: failed to load library 'libcurl-4.dll'的错误通常可以通过重新安装或更新相关程序、替换库文件、设置正确的环境变量或解决系统冲突来解决。如果问题仍然存在,可以考虑咨询技术支持或论坛等资源以获取更多帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值