ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LevelDB WriteBatch 内 Delete 与 Put 的顺序保证:为什么 Delete 要先于 Put

LevelDB WriteBatch 内 Delete 与 Put 的顺序保证:为什么 Delete 要先于 Put LevelDB WriteBatch 内 Delete 与 Put 的顺序保证为什么 Delete 要先于 Put【免费下载链接】leveldbLevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.项目地址: https://gitcode.com/GitHub_Trending/leveldb4/leveldb在 C 中使用 leveldb 做移动或覆盖类更新时——例如把 key1 上的值挪到 key2或者更新某个已存在的 key——很容易写成Put(key2, value)加Delete(key1)两条独立调用。但这两条操作既不原子顺序也不是随意的先 Put 后 Delete 和先 Delete 后 Put对同一个 key 会得到完全不同的最终状态。本文基于项目文档与源码说明如何用WriteBatch正确完成这类更新、批内编辑为何严格按添加顺序生效以及为什么 Delete 要先于 Put。问题两条独立写操作的两个坑doc/index.md 的 Reads And Writes 一节给出了把 key1 的值移动到 key2 的朴素写法std::string value; leveldb::Status s db-Get(leveldb::ReadOptions(), key1, value); if (s.ok()) s db-Put(leveldb::WriteOptions(), key2, value); if (s.ok()) s db-Delete(leveldb::WriteOptions(), key1);文档紧接着在 Atomic Updates 一节指出了它的风险如果进程在Putkey2 之后、Deletekey1 之前死亡同一个值就会残留在多个 key 之下原文the same value may be left stored under multiple keys。此外如果 key1 与 key2 是同一个 key两条调用里 Put 与 Delete 的先后直接决定值是否会被整体丢掉——而这正是WriteBatch要解决的场景。WriteBatch编辑按添加顺序生效准备工作与文档一致打开或创建数据库并在代码中引入头文件leveldb/write_batch.h。#include leveldb/db.h #include leveldb/write_batch.h leveldb::DB* db; leveldb::Options options; options.create_if_missing true; leveldb::Status status leveldb::DB::Open(options, /tmp/testdb, db); assert(status.ok());include/leveldb/write_batch.h 头注释给出批内语义的权威定义updates 按加入 batch 的顺序应用The updates are applied in the order in which they are added to the WriteBatch并附了一个关键示例// 头注释中的文档示例 batch.Put(key, v1); batch.Delete(key); batch.Put(key, v2); batch.Put(key, v3); // 该 batch 写完后key 的值是 v3这个示例说明对同一个 key后加入的操作覆盖先加入的操作而不是batch 里出现一次 Delete 就把 key 删掉。doc/index.md的说法与之对应WriteBatchholds a sequence of edits to be made to the database, and these edits within the batch are applied in order。移动值的推荐写法就是文档给出的标准路径std::string value; leveldb::Status s db-Get(leveldb::ReadOptions(), key1, value); if (s.ok()) { leveldb::WriteBatch batch; batch.Delete(key1); batch.Put(key2, value); s db-Write(leveldb::WriteOptions(), batch); }注意这里Delete(key1)在Put(key2, value)之前加入 batch。文档原句解释了这个顺序的必要性Note that we called Delete before Put so that if key1 is identical to key2, we do not end up erroneously dropping the value entirely。拆开看key1 key2 时Delete 先加入序较早、Put 后加入序较晚最终生效的是 Put值被保留若反过来先 Put 后 Delete生效的是 Delete值就整个丢了。顺序为何能决定结果序列号机制后操作覆盖先操作在实现层靠内部键的序列号保证涉及三处源码批写入 memtable 时内部实现 db/write_batch.cc 中的MemTableInserter按序遍历 batch 的每一条记录每条 Put/Delete 写入一条内部键并把序列号sequence_加 1// 摘自 db/write_batch.cc 的 MemTableInserter void Put(const Slice key, const Slice value) override { mem_-Add(sequence_, kTypeValue, key, value); sequence_; } void Delete(const Slice key) override { mem_-Add(sequence_, kTypeDeletion, key, Slice()); sequence_; }同一个 user key 下的多个版本按序列号区分。比较函数 db/dbformat.cc 中的InternalKeyComparator::Compare先按 user key 比较user key 相等时按序列号降序排列decreasing sequence number即序列号大的较新的操作排在前面。读取时db/memtable.cc 中的MemTable::Get对目标 user key 只取排序后的第一条也就是最新记录类型为kTypeValue则返回值类型为kTypeDeletion则返回Status::NotFound。所以Delete 先于 Put的效果是Put 的序列号更高Get 先命中 Put值保留Put 先于 Delete则 Get 先命中 Deletekey 变成不存在。项目测试 db/write_batch_test.cc 的Multiple用例也验证了序列号按添加顺序分配SetSequence(batch, 100)之后三条操作 Put(foo,bar)、Delete(box)、Put(baz,boo) 分别得到序列号 100、101、102。验证写入结果写完后按常规用Get校验。include/leveldb/db.h对DB::Get的注释说明key 不存在时返回的 status 满足Status::IsNotFound()返回 true。对应到移动场景的验证// Write 成功后 leveldb::Status s2; std::string v2; s2 db-Get(leveldb::ReadOptions(), key2, v2); // key2 ! key1 时s2.ok() 且 v2 等于从 key1 读到的 value leveldb::Status s1; std::string v1; s1 db-Get(leveldb::ReadOptions(), key1, v1); // key1 ! key2 时Status::IsNotFound(s1) 为 true说明 key1 已删除 // key1 key2 时应读到 Put 的 value而不是 NotFound两个分支合起来覆盖了标题问题的核心同一 key 上 Delete 与 Put 谁后加入谁就是最终状态文档示例中的 v3 结果与这里的校验逻辑是同一语义。限制与可选配置原子性文档对WriteBatch的定位是 atomically apply a set of updates即批内编辑要么整体应用。文档同时说明即使只崩溃了写进程未重启机器异步写入的数据也已从进程内存推入操作系统不会丢失需要数据落盘到持久存储才返回时设置write_options.sync true且同步写的额外开销可被批内多条写摊薄amortized across all of the writes in the batch。并发doc/index.md的 Concurrency 一节指出同一个leveldb::DB对象可安全地被多线程共享但WriteBatch这类对象可能需要外部同步include/leveldb/write_batch.h 头注释的表述一致多个线程可以无同步地调用 const 方法但只要有任何线程调用非 const 方法所有访问同一WriteBatch的线程都必须使用外部同步。用途说明文档还提到WriteBatch可用于把大量单条修改放入同一批以加速批量更新speed up bulk updates这是当前场景之外的另一个用途本文不展开。小结围绕为什么 Delete 要先于 Put可以落成三条可核对的结论WriteBatch的编辑按加入顺序应用对同一 key 后加入的操作生效实现上每条操作在写入 memtable 时拿到递增的序列号读取只认序列号最大的那条记录因此在移动/覆盖同一 key 时先batch.Delete(old_key)再batch.Put(new_key, value)写完后用Get分别确认新 key 能读到值、旧 key 返回IsNotFound的 status。【免费下载链接】leveldbLevelDB is a fast key-value storage library written at Google that provides an ordered mapping from string keys to string values.项目地址: https://gitcode.com/GitHub_Trending/leveldb4/leveldb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进