|
| 1 | +--- |
| 2 | +title: LevelDB 源码阅读:批量写的优雅设计 |
| 3 | +tags: [C++, LevalDB] |
| 4 | +category: 源码剖析 |
| 5 | +toc: true |
| 6 | +description: |
| 7 | +--- |
| 8 | + |
| 9 | +LevelDB 支持写入单个键值对和批量写入多个键值对,这两种操作的处理流程本质上是相同的,都会被封装进一个 WriteBatch 对象中,这样做的目的是提高写操作的效率和保持操作的原子性。 |
| 10 | + |
| 11 | +在 LevelDB 中,WriteBatch 是通过一个简单的数据结构实现的,其中包含了一系列的写入操作。这些操作被序列化(转换为字节流)并存储在内部的一个字符串中。每个操作都包括操作类型(如插入或删除),键和值(对于插入操作)。当 WriteBatch 被提交给数据库时,其内容被解析并应用到 WAL 日志和 memtable 中。这意味着不管 WriteBatch 中包含多少操作,它们都将作为一个整体进行处理和日志记录。 |
| 12 | + |
| 13 | +如果一次只写入一个键值对,内部也是独立的 WriteBatch 来处理,这个键值对会立即被写入 WAL 并更新到 memtable。如果想利用批量写入的性能优势,则需要在**应用层聚合这些写入操作**。例如,我们可以设计一个缓冲机制,收集一定时间内的写入请求,然后将它们打包在一个 WriteBatch 中提交。这种方式可以减少对磁盘的写入次数和上下文切换,从而提高性能。 |
| 14 | + |
| 15 | +<!-- more --> |
| 16 | + |
| 17 | +这里 WriteBatch 的实现主要涉及到 4 个文件: |
| 18 | + |
| 19 | +1. include/leveldb/write_batch.h:对外暴露的接口文件,定义了 WriteBatch 类的接口。 |
| 20 | +2. db/write_batch_internal.h:内部实现文件,定义了 WriteBatchInternal 类,提供了一些操作 WriteBatch 的方法。 |
| 21 | +3. db/write_batch.cc:WriteBatch 类的实现文件,实现了 WriteBatch 类。 |
| 22 | +4. db/write_batch_test.cc:WriteBatch 类的测试文件,用于测试 WriteBatch 的功能。 |
| 23 | + |
| 24 | +## 类的设计 |
| 25 | + |
| 26 | +我们先来看 `write_batch.h` 文件,这里定义了 WriteBatch 类对外暴露的一些接口。 LevelDB 代码中的注释十分清晰,不过这里先省略注释: |
| 27 | + |
| 28 | +```c++ |
| 29 | +class LEVELDB_EXPORT WriteBatch { |
| 30 | + public: |
| 31 | + class LEVELDB_EXPORT Handler { |
| 32 | + public: |
| 33 | + virtual ~Handler(); |
| 34 | + virtual void Put(const Slice& key, const Slice& value) = 0; |
| 35 | + virtual void Delete(const Slice& key) = 0; |
| 36 | + }; |
| 37 | + |
| 38 | + WriteBatch(); |
| 39 | + |
| 40 | + // Intentionally copyable. |
| 41 | + WriteBatch(const WriteBatch&) = default; |
| 42 | + WriteBatch& operator=(const WriteBatch&) = default; |
| 43 | + |
| 44 | + ~WriteBatch(); |
| 45 | + void Put(const Slice& key, const Slice& value); |
| 46 | + void Delete(const Slice& key); |
| 47 | + void Clear(); |
| 48 | + size_t ApproximateSize() const; |
| 49 | + void Append(const WriteBatch& source); |
| 50 | + Status Iterate(Handler* handler) const; |
| 51 | + |
| 52 | + private: |
| 53 | + friend class WriteBatchInternal; |
| 54 | + |
| 55 | + std::string rep_; // See comment in write_batch.cc for the format of rep_ |
| 56 | +}; |
| 57 | +``` |
| 58 | +
|
| 59 | +`WriteBatch::Handler` 是一个抽象基类,定义了处理键值对操作的接口,只包括 Put 和 Delete 方法。这样的设计允许 WriteBatch 类实现与**具体存储操作**解耦,使得 WriteBatch 不必直接知道如何将操作应用到底层存储(如 MemTable)。通过继承 Handler 类,可以创建多种处理器,它们可以以不同的方式实现这些方法。比如: |
| 60 | +
|
| 61 | +1. MemTableInserter: 定义在 `db/write_batch.cc` 中,将操作应用到 MemTable 中。 |
| 62 | +2. WriteBatchItemPrinter:定义在 `db/dumpfile.cc` 中,将操作打印到文件中。 |
| 63 | +
|
| 64 | +另外还有一个 `friend class WriteBatchInternal` 作为 WriteBatch 的友元类,能够访问其私有和受保护成员。WriteBatchInternal 主要用来封装一些内部操作,这些方法不需要对外暴露,只在内部用到。通过将内部操作方法隐藏在 WriteBatchInternal 中,保持了对象的接口清晰,并且可以自由地修改内部实现而不影响到使用这些对象的代码。 |
| 65 | +
|
| 66 | +## 实现细节 |
| 67 | +
|
| 68 | +WriteBatch 的具体实现在 `db/write_batch.cc` 文件中,类中用private成员 `std::string rep_` 来存储序列化后的操作。存储数据协议如下: |
| 69 | +
|
| 70 | +``` |
| 71 | ++---------------+---------------+----------------------------------------+ |
| 72 | +| Sequence | Count | Data | |
| 73 | +| (8 bytes) | (4 bytes) | | |
| 74 | ++---------------+---------------+----------------------------------------+ |
| 75 | + | | | |
| 76 | + v v v |
| 77 | + +-------+ +-------+ +-------+ |
| 78 | + |Record1| |Record2| ... |RecordN| |
| 79 | + +-------+ +-------+ +-------+ |
| 80 | + | | |
| 81 | + v v |
| 82 | + +-----------------+ +-----------------+ |
| 83 | + | kTypeValue | | kTypeDeletion | |
| 84 | + | Varstring Key | | Varstring Key | |
| 85 | + | Varstring Value | | | |
| 86 | + +-----------------+ +-----------------+ |
| 87 | + |
| 88 | +Varstring (可变长度字符串): |
| 89 | ++-------------+-----------------------+ |
| 90 | +| Length (varint32) | Data (uint8[]) | |
| 91 | ++-------------+-----------------------+ |
| 92 | +``` |
| 93 | +
|
| 94 | +rep_ 字符串的前 12 个字节分别是 8 个字节的 sequence number 和 4 个字节的操作数 count。接下来是一个或多个操作记录,每个记录包含一个操作类型和键值对。操作类型是一个字节,用来区分 Put 和 Delete 操作。键和值都是可变长度的字符串,格式为 varstring。 |
| 95 | +
|
| 96 | +### sequence number |
| 97 | +
|
| 98 | +rep_ 头部 8 个字节代表64位的数字sequence(序列号),用来确保数据库操作的顺序性和一致性。`sequence number` 是为每个写操作分配的唯一标识符,用于记录写操作的顺序,这对于多版本并发控制(MVCC)和事务日志(WAL)至关重要。 |
| 99 | +
|
| 100 | +- **操作顺序性**:sequence number 确保了写操作的顺序性。LevelDB 是一个键值存储,但支持相同键的多个版本。sequence number 用来区分同一个键的不同版本,确保读取操作总是能访问到正确版本的数据。 |
| 101 | +- **多版本并发控制(MVCC)**:通过使用 sequence number,LevelDB 能够支持快照隔离,允许读取操作访问数据的旧版本,而同时有新的写操作发生,而不会相互干扰。 |
| 102 | +- **恢复和一致性**:在系统崩溃后,sequence numbers 允许 LevelDB 从 WAL 日志中恢复,确保只重放那些尚未持久化到 SST 文件的写操作。 |
| 103 | +
|
| 104 | +这里设置 sequence number 的逻辑 `DBImpl::Write` 方法中,首先获取当前最大序列号,然后为 WriteBatch 分配一个新的序列号。如果 WriteBatch 包含多个操作,那么这些操作会**连续地分配序列号**。在写入 WAL 日志时,会将 WriteBatch 的序列号写入到日志中,这样在恢复时可以根据序列号来恢复操作的顺序。写入 memtable 之后,会更新当前最大序列号,以便下次分配。 |
| 105 | +
|
| 106 | +### 具体实现 |
| 107 | +
|
0 commit comments