
数据库KV存储嵌入式数据库存储【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址https://gitcode.com/gh_mirrors/ro/rocksdb点击查看免费下载Merge Operator合并算子是 RocksDB 中最具特色的写入抽象之一应用可以通过DB::Merge()以增量操作merge operand的形式更新同一个 key而不必先读出旧值再写回。本文以仓库文档 docs/components/read_flow/09_merge_resolution.md 为骨架结合 db/merge_helper.h、db/merge_helper.cc、db/merge_context.h、db/db_iter.cc、db/memtable.cc 与 include/rocksdb/merge_operator.h 等源码系统讲解 Merge 解析merge resolution发生的三类场景——点查Get、迭代Iterator、Compaction以及背后的操作数收集、Full Merge / Partial Merge 分工、快照边界约束与性能调优要点。读完本文你将掌握操作数如何跨 memtable 与 SST 各层累积MergeContext的方向反转机制为何存在max_successive_merges与压缩期 partial merge 各自解决的性能问题以及如何借助统计指标定位 Merge 相关瓶颈。背景为什么需要读时解析而不是写时合并RocksDB 采用 LSM-Tree 结构写入只追加到 memtable数据按层下沉。若应用想维护一个计数器的值、在一个列表上追加元素、或者合并一次更新最简单的做法是读-改-写read-modify-write但这需要一次额外的点查且在高并发下容易放大写入延迟。Merge 提供了一条新路径写入侧应用直接调用DB::Merge(key, operand)RocksDB 把这条增量操作原样落盘不读取旧值读取侧当读到某个 key 时RocksDB 需要把这个 key 的所有历史 merge operand 收集起来再加上可能存在的基础值base valuePut写入的值或某条Delete边界一起交给用户自定义的MergeOperator由FullMergeV3()计算最终结果。因此把哪个值读出来是一个跨多层的解析过程同一 user key 的历史记录按新到旧排列可能分布在活跃 memtable、多个 immutable memtable 以及多个 SST 文件中。解析的本质是回答两个问题这个 key 一共积累了哪些操作数以及能否找到一个基础值来终止收集这正是本文要展开的核心流程。关键数据结构与入口文件的对应关系如下表也是本文后续各节的索引组件所在文件职责MergeContextdb/merge_context.h累积 merge operand管理顺序与 pinningMergeHelperdb/merge_helper.h封装 Full Merge / Partial Merge 与压缩期MergeUntil()SaveValue()/HandleTypeMergedb/memtable.ccmemtable 内的 Get 操作数收集GetContextdb/db_impl/db_impl.h实现位于 db/db_impl/db_impl.ccSST 层Version::Get()的 merge 状态跟踪DBIter::MergeValuesNewToOld()db/db_iter.cc正向迭代中的 merge 链解析MergeOperatorFullMergeV3/PartialMergeMultiinclude/rocksdb/merge_operator.h用户自定义合并逻辑的抽象接口MergeContext操作数收集的核心容器无论在哪一层做解析操作数都需要先被攒起来。MergeContext见 db/merge_context.h 中的class MergeContext就是承担这个任务的容器。它有两个核心设计点理解它们有助于读懂后续所有调用链。存储与顺序收集是新到旧呈现是旧到新从源码结构看MergeContext内部用一个std::vectorSlice作为operand_list_并配一个懒反转标志operands_reversed_方面细节存储std::vectorSlice底层由operand_list_持有收集方向新到旧PushOperand()默认走 backward 方向SetDirectionBackward()即新找到的操作数追加在新的一侧呈现方向旧到新GetOperands()即GetOperandsDirectionForward()把列表反转为按时间正序最早的操作数在前再传给 merge operatorPinning通过PushOperand(operand_slice, operand_pinned)的第二个参数控制若数据已被 pin如PinnedIteratorsManager或内部迭代器值被 pin直接保存Slice引用否则在copied_operands_中做一份拷贝避免悬垂引用收集时新到旧是很自然的——因为所有搜索路径memtable 从新到旧、SST 层从高层到底层本身就按新到旧遍历而MergeOperator的语义是按时间正序应用操作先Merge(key, op1)再Merge(key, op2)所以传递前需要反转。SetDirectionForward()/SetDirectionBackward()利用std::reverse就地翻转并记录方向避免重复反转。关键 API 一览void PushOperand(const Slice operand_slice, bool operand_pinned false); void PushOperandBack(const Slice operand_slice, bool operand_pinned false); size_t GetNumOperands() const; const std::vectorSlice GetOperands() const; // 旧 - 新传给 FullMerge const std::vectorSlice GetOperandsDirectionBackward() const; // 新 - 旧 void Clear();注意GetOperands()返回的引用在下次对同一MergeContext调用前有效如果需要长期持有必须自行拷贝源码注释明确说明了这一点。另外MergeContext还挂了一个get_merge_operands_options字段用于GetMergeOperands()这类只取操作数不做合并的 API。点查Get中的 Merge 解析一次Get()会依次搜索活跃 memtable → 各 immutable memtableMemTableListVersion→ 各层 SST 文件Version::Get()。MergeContext贯穿整个过程各层把找到的操作数接力式地往里追加。MemTable 层SaveValue()的逐条处理MemTable::Get()在 db/memtable.cc 中通过table_-Get(key, saver, SaveValue)触发查找SaveValue()db/memtable.cc按 entry 类型分派处理。与 merge 相关的分支逻辑如下kTypeMerge见 db/memtable.cc把该操作数推入merge_context并把*merge_in_progress置为true然后继续向后更旧方向扫描返回!found_final_value以继续遍历。这一分支不会终止查找。kTypeValue/kTypeValuePreferredSeqno见 db/memtable.cc如果此前已处于merge_in_progress状态ReadOnlyMemTable::HandleTypeValue会立即调用MergeWithPlainBaseValue()把 base value 与已收集的操作数做一次 full merge得到最终值然后found_final_value true停止扫描。kTypeDeletion/kTypeSingleDeletion/kTypeDeletionWithTimestamp/kTypeRangeDeletionHandleTypeDeletion会以无 base value方式解析——删除标记意味着操作数序列在此截断前面的更旧的历史对该 Get 不可见若 merge 尚未开始则直接返回不存在。kTypeWideColumnEntity若处于merge_in_progress走MergeWithWideColumnEntityBaseValue()db/memtable.cc把宽列实体作为 base value 参与合并。kTypeBlobIndexmemtable 中遇到 blob index 时若正在 merge 中会返回NotSupported说明 stacked BlobDB 不支持在 memtable 层以 merge 操作数继续向下合并。关键点在于memtable 内遍历是单次扫完的如果同一 memtable 中既有操作数又有 base valueSaveValue会在同一次查找内完成合并如果只有操作数而没有 base value则把merge_in_progress状态传递到下一层。跨 immutable memtable新到旧的接力MemTableListVersion持有的各 immutable memtable 在MemTableListVersion::Get()中被从新到旧依次查询。每个 memtable 都可能贡献若干操作数一旦某个 memtable 中出现 base valuePut立即触发合并并终止。这与 memtable 内的逻辑一致只是把同一张表内的扫描扩展为多张表之间的接力。SST 层GetContext的kMerge状态机当 memtable 全部耗尽仍未解析完成时查找进入Version::Get()db/version_set.cc逐层逐文件扫描 SST。这里的状态由GetContext跟踪db/db_impl/db_impl.h 中的class GetContextGetContext内部维护一个merge_context_和状态标记SST 中每遇到一条kTypeMerge就把操作数推入并把状态切到kMerge每遇到kTypeValue或kTypeWideColumnEntity、kTypeBlobIndex且在kMerge状态下就调用一次 full merge若所有文件都已耗尽、状态仍停留在kMerge说明该 key 从头到尾只有 merge 操作数、从未被Put或Delete覆盖——此时以无 base value方式调用TimedFullMerge()把nullptr作为 existing value 交给 merge operator让应用自行决定如何处理纯操作数场景。TimedFullMerge()带统计的 Full Merge 封装无论 base value 是普通值、宽列实体还是不存在最终都收敛到MergeHelper::TimedFullMerge()db/merge_helper.cc。源码中它以三个 tag 类型区分输入kNoBaseValue、kPlainBaseValue、kWideBaseValue。其内部会若update_num_ops_stats true来自用户读路径记录READ_NUM_MERGE_OPERANDS直方图db/merge_helper.cc用于观察单次读需要合并多少个操作数用StopWatchNano计时并调用merge_operator-FullMergeV3(merge_in, merge_out)db/merge_helper.cc同时累计MERGE_OPERATION_TOTAL_TIME统计若返回false累计NUMBER_MERGE_FAILURESdb/merge_helper.cc并按OpFailureScope决定错误处理默认kDefault会被解释为kTryMerge返回Status::Corruption(SubCode::kMergeOperatorFailed)若是kMustMerge场景则把状态转为Status::MergeInProgress()保留操作数原样写出成功后按MergeOperationOutputV3::NewValue的 variant 类型把结果写为kTypeValue或kTypeWideColumnEntity并检查结果不超过 4GB 上限BlockBuilder 对块大小的uint32_t假设。值得一提的实现细节FullMergeV3的输出new_value可以是三种形态——普通字符串、宽列NewColumns、或者直接返回某一个现有操作数Slice。最后一种场景在TimedFullMergeImpl中表现为result_operand直接引用操作数、避免一次拷贝是 RocksDB 对合并结果等于某个输入操作数场景的零拷贝优化。迭代Iterator中的 Merge 解析MergeValuesNewToOld()迭代与点查的一个本质区别是迭代器是顺序前进的它不能像 Get 那样任意跨层跳读只能沿着内部迭代器一条一条消费 entry。因此DBIter在读到一个kTypeMergeentry 时需要进入一个专门的解析状态机。正向迭代的合并流程DBIter::MergeValuesNewToOld()db/db_iter.cc实现了正向迭代下的 merge 链解析其步骤与文档中的四步流程一一对应Step 1先把当前迭代器位置上的第一个操作数推入merge_context_并调用TempPinData()临时 pin 住持有该操作数的 block避免后续Next()使值失效db/db_iter.cc。Step 2调用内部迭代器的Next()前进。这里依赖 InternalKey 的排序约定——user key 升序、sequence 降序所以同一 user key 的下一条记录必然是更旧的版本。Step 3对后续每个 user key 相同的 entry 分类处理db/db_iter.cc遇到的类型处理结果kTypeMerge再推入一个操作数继续前进继续收集kTypeValue/kTypeValuePreferredSeqno调用MergeWithPlainBaseValue()得到最终值迭代器停在 base value 之后返回kTypeBlobIndex调用MergeWithBlobBaseValue()先取回 blob 值再合并得到最终值返回kTypeWideColumnEntity调用MergeWithWideColumnBaseValue()得到最终值返回kTypeDeletion/kTypeSingleDeletion/kTypeDeletionWithTimestamp不合并直接跳过删除标记并停止进入无 base value解析遇到下一个 user key停止收集进入无 base value解析Step 4如果遍历到该 user key 的历史尽头iter_失效或遇到删除标记都没有发现 base value则调用MergeWithNoBaseValue()db/db_iter.cc以nullptr作为 existing value 执行 full merge。关键不变式无论收集过程中经历了多少层、多少个 entry最终传给MergeOperator的操作数顺序一定是时间正序旧到新。这正是MergeContext::GetOperands()在调用前完成方向反转的原因——这个不变式同时被点查路径、迭代路径和压缩路径共享是用户自定义 merge operator 能够正确实现的前提。Full Merge 与 Partial Merge 的分工Merge 解析有两种合并粒度分别服务于得到最终结果与减少操作数两个目标Full Merge基础值 全部操作数 → 最终值入口MergeOperator::FullMergeV3()include/rocksdb/merge_operator.h由MergeHelper::TimedFullMerge()统一封装调用产出一个最终值kTypeValue或kTypeWideColumnEntity场景读路径Get / Iterator在找到 base value 时使用也用于操作数收集齐全但没有 base value 的收尾解析如迭代走到历史尽头、SST 层所有文件耗尽。FullMergeV3是当前主接口它支持宽列输入输出默认实现会回退到FullMergeV2()——当无 base value 或 base value 是普通键值时直接转发当 base value 是宽列实体时对默认列执行FullMergeV2()合并、其余列保持不变见 include/rocksdb/merge_operator.h 注释。因此旧的应用只实现FullMergeV2也能正常工作。Partial Merge操作数 → 更少的操作数入口MergeOperator::PartialMergeMulti()include/rocksdb/merge_operator.h默认实现用两两合并的PartialMerge()include/rocksdb/merge_operator.h作为 helper建议直接重载PartialMergeMulti以一次合并多个操作数产出仍然是 merge 类型但数量变少场景Compaction 期间当没有找到 base value没遇到Put/Delete、也没到 key 历史起点时把已收集的操作数预先合并成一个触发条件操作数个数 ≥ 2如果应用重载了AllowSingleOperand()并返回true则单个操作数也可以触发include/rocksdb/merge_operator.hdb/merge_helper.cc价值把多个操作数压缩成一个减少存储占用和未来读取时的收集代价——相当于把读放大前置到后台压缩中消化。压缩路径中调用PartialMergeMulti的代码在 db/merge_helper.cc源码中同样用StopWatchNanoPERF_TIMER_GUARD(merge_operator_time_nanos)计时并累计MERGE_OPERATION_TOTAL_TIME。一个容易混淆的概念max_successive_merges文档特别强调max_successive_merges定义于 include/rocksdb/advanced_options.h不控制压缩期的 partial merge它是写路径的独立优化语义memtable 中同一 key 连续 merge 操作数达到该上限时RocksDB 在写入时主动读取一次该 key 的已有值把新操作数 旧值合并后以普通值而非操作数写入 memtable从而截断操作数链默认值0禁用可通过SetOptions()动态调整关联选项strict_max_successive_mergesinclude/rocksdb/advanced_options.h控制是否允许为守约而引入文件系统读——默认false即允许在需要读盘时放弃守约避免写路径阻塞在文件系统读上显式设为true则强制守约代价是 merge 写可能阻塞等待读盘。简言之写路径用max_successive_merges在 memtable 插入时趁热合并压缩路径用 partial merge 在后台把操作数压短。两者目标一致缩短操作数链、降低读放大但发生在完全不同的阶段。快照边界约束压缩为什么不能随意合并Compaction 在合并操作数时必须格外小心不能跨快照边界合并。原因很直观——快照的语义要求每个 reader 看到的是某一时刻的一致性视图持有旧快照的 reader 必须看到合并前的状态各个操作数原样存在持有新快照的 reader 则应看到合并后的状态。如果在两个操作数之间按 sequence number 计存在一个快照把它们合并成一条旧快照 reader 就无法还原出合并前的中间值了。MergeUntil()的压缩期实现Compaction 中的解析由MergeHelper::MergeUntil()db/merge_helper.cc承担。它从第一条 merge entry 开始向后遍历遇到以下情况之一即停止合并遇到损坏的 key且assert_valid_internal_key_为真时直接报错遇到Put/Delete此时做 full merge 收尾遇到不同的 user key遇到某个特定 sequence number——即快照边界compaction filter 返回REMOVE_AND_SKIP_UNTIL迭代到达末尾。快照检查的代码位于 db/merge_helper.cc当stop_before 0且ikey.sequence stop_before且snapshot_checker_为空或CheckInSnapshot(ikey.sequence, stop_before) ! kNotInSnapshot时说明该 entry 可能被某个更早的快照看到不能触碰立即停止。这正是文档所说用SnapshotChecker::CheckInSnapshot()判断操作数是否落在快照保护区间的实现。另外MergeUntil()还处理两类边缘情况at_bottom与确信看到 key 历史起点如果at_bottom true当前层是 bottommost level且Compaction::IsBottommostLevel()确认下方没有该 key并且遍历到下一个 user key 或迭代结束就确信看完了该 key 的完整历史此时可以用kNoBaseValue做一次 final full merge把 merge 类型转换为 Putdb/merge_helper.cc。源码注释特别提醒开启用户自定义时间戳user-defined timestamp时若full_history_ts_low非空且 key 时间戳 ≥full_history_ts_low该 key 不可被 GC不能贸然断定看到了起点。合并失败与OpFailureScope若 full merge 失败且失败范围为kMustMergeMergeUntil返回Status::MergeInProgress()把操作数原样保留输出交给下一轮压缩处理db/merge_helper.cc。关键不变式不能合并跨快照边界的操作数。对快照可见的每个独立操作数必须原样保留。这条约束直接解释了为什么压缩有时明明可以合并却不合并——VersionSet::SetupOtherInputs()会保证同一 level 的所有 merge 操作数一起被压缩合并失败只是把它们原样搬到下一层等待后续更合适的时机。性能影响与监控建议Merge 的读代价与操作数链的长度直接相关读放大一次Get()必须在所有层收集完操作数才能解析。链越长需要访问的 memtable / SST entry 越多。压缩缓解Compaction 通过 full merge把 merge 变成 Put或 partial merge把多个操作数变成一个显著缩短操作数链是控制读放大的主要手段。写路径缓解max_successive_merges在 memtable 写入时即触发合并适合同一 key 被高频 Merge 更新的场景。应用侧缓解MergeOperator::ShouldMerge()include/rocksdb/merge_operator.h允许应用在 Get 过程中提前判断是否应该停止继续收集例如操作数已足够多时从而限制点查需要访问的 level 数。注意源码注释指出该接口对迭代器无效且操作数以新到旧的反转顺序传入见 include/rocksdb/merge_operator.h 对 issue #3865 的说明。可观测的统计指标从源码中可以确认以下统计项与 merge 解析直接相关可在Options::statistics中开启后观测统计项记录位置含义NUMBER_MERGE_FAILURESdb/merge_helper.cc用户 merge operator 返回失败FullMergeV3返回false的次数MERGE_OPERATION_TOTAL_TIMEdb/merge_helper.cc、db/merge_helper.ccmerge operator 执行的总耗时full 与 partial 均计入READ_NUM_MERGE_OPERANDSdb/merge_helper.cc用户读路径Get单次合并的操作数直方图可量化操作数链长度内嵌 perf 指标merge_operator_time_nanosdb/merge_helper.cc单次 merge operator 调用的纳秒耗时PERF_TIMER_GUARD若NUMBER_MERGE_FAILURES持续增长通常意味着用户合并逻辑对数据假设失效例如遇到了没有 base value 的纯操作数序列、或数据损坏需要检查 merge operator 的实现若READ_NUM_MERGE_OPERANDS的均值显著偏大说明操作数链过长可以从max_successive_merges、压缩频率或ShouldMerge()三个方向入手调优。小结一条贯穿读路径的解析主线把上述内容串起来RocksDB 的 merge resolution 遵循一条清晰的主线收集无论是 Get、Iterator 还是 Compaction都沿着新到旧的顺序把kTypeMerge操作数推入MergeContext定向呈现调用用户算子前MergeContext把操作数反转为旧到新保证MergeOperator永远按时间正序应用终止条件遇到Put含kTypeValue、宽列实体、blob 值即 full merge 收尾遇到Delete或以无 base value 收尾Compaction 中额外受快照边界、at_bottom、compaction filter 的约束两种合并粒度Full Merge 产出最终值供读取Partial Merge 在压缩期压缩操作数链性能闭环写路径的max_successive_merges、读路径的ShouldMerge()、后台的压缩合并三者共同抑制操作数链增长而NUMBER_MERGE_FAILURES、READ_NUM_MERGE_OPERANDS等统计指标用于定位异常。理解这条主线后无论是实现自定义MergeOperator如计数器、JSON 字段更新、列表追加还是诊断为什么我的 merge 读变慢了都能从上述源码路径中找到对应的检查点。更进一步可结合 include/rocksdb/merge_operator.h 的接口注释与 db/merge_helper_test.cc若存在中的测试用例验证你对合并顺序、快照边界等语义的理解。赞分享数据库KV存储嵌入式数据库存储【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址https://gitcode.com/gh_mirrors/ro/rocksdb点击查看免费下载相关推荐Quickwit Split Compaction 架构解析从文档写入到 Split 合并的完整数据链路Quickwit Split Compaction 架构解析从文档写入到 Split 合并的完整数据链路 Quickwit 将文档写入不可变的小型单元 Spl搜索引擎可观测性日志分析链路追踪后端全文检索RocksDB 点查核心链路解析从 Version::Get() 到 SST 文件查找的完整流程RocksDB 点查核心链路解析从 Version::Get 到 SST 文件查找的完整流程 导读 本文深入剖析 RocksDB 读取路径Read Path数据库KV存储嵌入式数据库存储RocksDB 范围删除Range Deletion处理机制深入解析从 Tombstone 碎片化到点查与迭代器集成RocksDB 范围删除Range Deletion处理机制深入解析从 Tombstone 碎片化到点查与迭代器集成 范围删除 DeleteRange数据库KV存储嵌入式数据库存储创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考