第 13
篇 把多 CF 共享 WAL 钉住了。WriteBatch +
单次 DB::Write 已保证
批内原子性(第 4
篇),但 不
保证「读-改-写」在并发下无冲突:两个线程各自读旧值、各自提交,后写者覆盖先写者,除非上层自己做锁或版本校验。
RocksDB 在 TransactionDB /
OptimisticTransactionDB 上提供 BEGIN /
COMMIT / ROLLBACK 式
API,在提交时做冲突检测。本文是系列 第 14
篇,沿 Wiki Transactions 与
WritePrepared Transactions 说明悲观与乐观路径、三种
WritePolicy 边界,以及 TiKV 与单机 RocksDB
事务的分工(B 级前瞻,Percolator / Raft 留
db-frontier
HTAP 与第 18 篇续作)。
本文是「RocksDB 内核机制」系列第 14 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 13 篇 · Column Family 共享 WAL、独立 LSM 第 14 篇 · 事务与 OCC OptimisticTransactionDB、WritePrepared 边界 第 15 篇 · Checkpoint 与 Ingest 硬链接快照 第 16 篇 · 生产嵌入对照 Flink / TiKV key 编码
版本锚定:RocksDB 9.x(
utilities/transactions/、include/rocksdb/utilities/transaction_db.h)。TiKV 事务章节为 B 级前瞻(PingCAP 设计文档与博客),不声称覆盖 Percolator 实现细节;后续 HTAP 系列可修订本节。
一、WriteBatch 原子性 vs 事务语义
| 能力 | WriteBatch |
[Optimistic]TransactionDB |
|---|---|---|
| 多 key 原子提交 | 是 | 是(commit 时写入 batch) |
| 提交前对其他读者可见 | 否 | 否 |
| 并发冲突检测 | 否 | 是(悲观锁或乐观校验) |
| Read-your-own-writes | 需 WriteBatchWithIndex |
内建(Transaction::Get) |
| 2PC Prepare | 否 | TransactionDB + WritePrepared |
Wiki Transactions(A 级):Transactions provide a way to guarantee that a batch of writes will only be written if there are no conflicts.
Flink RocksDB state 默认 不用 RocksDB 事务 API——一致性靠 checkpoint barrier 与算子单线程 per key 语义(stream/14 交付语义)。理解 RocksDB 事务仍有助于读 TiKV 存储层与 MyRocks 等 真 2PC 嵌入。
二、TransactionDB:悲观锁与
WriteCommitted
TransactionDB::Open 创建带锁表的 DB。Wiki
要点(A 级):
- 所有写入(含 非事务
Put/Delete/Merge)内部走事务路径做冲突检测。 - 写前
TryLock;锁失败则返回错误。 - 默认 WritePolicy = WriteCommitted:数据 仅在 Commit 后 写入 MemTable,读路径可假设 MemTable 中可见数据已提交。
TransactionDB* txn_db;
TransactionDBOptions txndb_opts;
Status s = TransactionDB::Open(options, txndb_opts, path, &txn_db);
Transaction* txn = txn_db->BeginTransaction(write_options);
s = txn->Put("key", "value");
s = txn->Commit();
delete txn;适用:写冲突频繁、需要
GetForUpdate 建立读-写冲突、或 2PC
Prepare/Commit 的数据库引擎(如
MySQL on RocksDB)。代价:锁 striping 开销(Wiki Under
the hood:PointLockManager 每 CF 16 条
stripe)。
三、OptimisticTransactionDB:提交时
OCC
乐观事务 Prepare
阶段不加锁;Commit 时通过
CheckCallback
校验自首次写入以来是否有其他写者修改相同 key(Wiki
OptimisticTransactionDB,A 级)。
OptimisticTransactionDB* txn_db;
OptimisticTransactionDB::Open(options, path, &txn_db);
DB* db = txn_db->GetBaseDB();
OptimisticTransaction* txn = txn_db->BeginTransaction(write_options);
txn->Put("key", "value");
Status s = txn->Commit(); // 冲突则 Non-OK,无 key 落盘| 对比 | TransactionDB |
OptimisticTransactionDB |
|---|---|---|
| 锁 | 写前加锁 | 无锁,commit 时检测 |
| 非事务写 | 也走锁 | 与事务写并发,commit 可能失败 |
| 典型 workload | 高冲突 OLTP | 低冲突、非事务写多 |
| 内存 | 锁表 | 复用 memtable 历史做冲突检测 |
Wiki 建议:若 大量事务争抢同一 key,改用
TransactionDB;若
非事务写远多于事务,乐观路径可能更省锁开销。
冲突检测数据源:TransactionUtil::CheckKeysForConflicts
对比 key 的 latest sequence
与事务开始写入时记录的 sequence;依赖 memtable 中
足够的历史
buffer——max_write_buffer_size_to_maintain
过小会导致 误报冲突(Commit 失败)(Wiki
Tuning / Memory Usage)。
四、读语义:GetForUpdate、SetSnapshot、Snapshot
Read-your-own-writes
事务内 Get / MultiGet 先见
本事务未提交写,再查 DB(Wiki 示例,A
级)。
读-写冲突
仅 GetForUpdate 把读过的
key 设为 commit 前提;普通 Get
不 阻止其他线程改同一 key 后仍 commit
成功。
SetSnapshot()
默认:冲突检测以 key
在本事务内首次写入时刻 为界。调用
SetSnapshot() 后,改为
事务开始快照 为界——快照之后的外部写入会导致
commit 失败(即使外部写发生在本事务写同一 key 之前)。
ReadOptions::snapshot
与 第 7 篇 相同:指定 读哪个 sequence 截面;不 单独决定 commit 能否成功。
五、WritePrepared:缩短 2PC Commit 路径
WriteCommitted 把 MemTable 写入压在 Commit 阶段,2PC 串行 commit 时延迟明显。Wiki WritePrepared Transactions(A 级)引入更早落盘:
| WritePolicy | MemTable 写入时机 | 读路径额外负担 |
|---|---|---|
| WriteCommitted | Commit | 低:MemTable 可见即已提交 |
| WritePrepared | Prepare | 需判断 prepare_seq 是否已 commit |
| WriteUnprepared | Put 阶段(更激进) | 更高 |
WritePrepared 概要(A 级,Wiki):
Prepare:batch 写 WAL + MemTable,整批共用prepare_seq(复用 sequence number)。Commit:WAL 写 commit marker,commit_seq为提交时间戳;CommitCache 记录prepare_seq → commit_seq。- 读路径:
IsInSnapshot(prepare_seq, snapshot_seq)查 CommitCache /delayed_prepared_/old_commit_map_判断可见性。 - Abort:对每 key 写 rollback 记录 抵消 prepared 数据(悲观锁保证每 key 仅一个 pending write)。
双写队列:two_write_queues=true
时,主队列写 Prepare(WAL+MemTable),次队列写 Commit
marker;第 4
篇 提到的 nonmem_write_thread_
与此相关。Snapshot 取 last published
sequence,而非两队列中较大的 raw sequence。
边界(本篇不展开实现):
- CommitCache eviction 与 长事务 backup
只读快照
的交互(
old_commit_map_)。 - WriteUnprepared 进一步降低 Prepare 缓冲,内存与正确性 trade-off 更陡。
- 非 2PC 普通写可能被拆成「写 MemTable + 发布 sequence」两步(双队列副作用,Wiki Atomic Commit)。
六、与 Snapshot / MVCC 的关系
第 7 篇 的 Snapshot = sequence 上界 仍是非事务读的基础。事务层在其上叠加:
flowchart TB
subgraph ReadPath["读路径"]
SN["ReadOptions::snapshot"]
WP["WritePrepared IsInSnapshot"]
GET["DBImpl::GetImpl + GetContext"]
end
SN --> GET
WP --> GET
subgraph WritePath["写路径"]
WC["WriteCommitted: commit 才进 mem"]
WPr["WritePrepared: prepare 进 mem"]
end
snapshot_impl.h 中
min_uncommitted_(WritePrepared
用)参与 未提交 prepared 数据 的裁剪(第 7 篇
已标注字段)。Compaction 不能删除 仍被 Snapshot 或
prepared 事务引用 的旧版本——与
GetSnapshot 阻止版本回收的逻辑同族。
七、TiKV 与 Percolator:层级对照(A 级文献 + B 级映射)
证据分层:Percolator 协议本身有 A 级论文;TiKV 「如何落盘」对照来自公开文档(B 级),不是 TiKV 源码 walkthrough。Raft、PD、Coprocessor 全文不在本系列。
7.1 Percolator:分布式 2PC 的奠基
Peng & Dabek, Large-scale Incremental Processing Using Distributed Transactions and Notifications(OSDI 2010,俗称 Percolator)给出 Google 网页索引增量系统的核心事务模型:
| 概念 | Percolator | RocksDB TransactionDB 同名物 |
|---|---|---|
| 锁 / 预写 | 锁列 + primary / secondary lock | GetForUpdate / 点锁(单机) |
| Prepare | 写 lock + data(未 commit) | WritePrepared:数据先入 MemTable |
| Commit | 写 commit-ts / 清锁 | CommitCache + 提交队列 |
| 时间戳 | 全局 TSO 式 commit_ts | 序列号 sequence(单机) |
| 复制 | Bigtable 行复制 | 无(需外加 Raft) |
关键差一层:Percolator 解决 跨机多分片 的原子提交;RocksDB 事务解决 单进程 LSM 上的冲突检测与可见性。名称上的 Prepare/Commit 不能 互换。
7.2 TiKV 三层叠加
| 层次 | 机制 | 与本文 RocksDB 事务 |
|---|---|---|
| Region 副本 | Raft log + apply | Raft 提供复制与共识,不是 RocksDB 事务 |
| 分布式 txn | Percolator 风格 2PC、锁列、commit TS(TiKV 文档;B 级落地) | 语义同族,锁与 TS 编码在 key 空间 |
| 本地引擎 | RocksDB CF、MVCC 时间戳 | 内部 key 含 TS;可见性由 TS + snapshot
决定,而非应用直接调 BeginTransaction |
flowchart TB
APP["应用 / TiDB"] --> PERC["Percolator 2PC + TSO"]
PERC --> RAFT["Raft Region"]
RAFT --> ROCK["RocksDB Put/Get<br/>CF + seq/TS 编码"]
APP2["单机 MyRocks / 自研"] --> TXN["TransactionDB / OCC"]
TXN --> ROCK2["同一套 DBImpl 写路径"]
7.3 工程关联与开放问题
- TiKV bulk load / BR 可能触发
IngestExternalFile与 write stall(第 15 篇)。 - Flink 不 使用 RocksDB 事务 API;TiKV 不能 用 RocksDB OCC 替代 Percolator。
- 开放问题:高 skew 下 OCC(OptimisticTransactionDB)的 abort 率何时劣于悲观锁?文献与工业经验分裂,靠 冲突率 POC,无统一阈值。Percolator 在热点锁上的 tail 仍是分布式 HTAP 开放问题(db-frontier/12)。
八、选型简表
| 场景 | 建议 |
|---|---|
| 仅需批写原子 | WriteBatch |
| 低冲突读-改-写 | OptimisticTransactionDB |
高冲突或 GetForUpdate |
TransactionDB |
| MyRocks / 2PC 引擎 | TransactionDB + WritePrepared /
WriteUnprepared |
| Flink state | checkpoint 一致性,非 RocksDB 事务 |
| TiKV | Percolator(OSDI 2010)+ Raft(见 §七) |
九、小结
- 事务 在 WriteBatch 原子性之上增加 冲突检测 与 Read-your-own-writes。
OptimisticTransactionDB在 commit 时 OCC,适合低冲突;需调max_write_buffer_size_to_maintain保历史。TransactionDB悲观锁,非事务写也参与锁;支持 2PC 与 WritePrepared。- WritePrepared 将 MemTable 写入提前到 Prepare,读路径用 CommitCache + IsInSnapshot 区分已提交与 prepared 数据。
- TiKV 事务在分布式层实现(Percolator OSDI 2010 奠基);RocksDB 事务 API 是 Region 本地引擎能力,不能 一一等同。
下一篇进入 Checkpoint / Backup / External Ingest——Flink 增量 checkpoint 与 bulk load 的运维接口。
上一篇:Column Family
下一篇:Checkpoint 与 External Ingest
返回 系列目录
参考资料
- RocksDB Wiki, Transactions(TransactionDB、OptimisticTransactionDB、GetForUpdate、SetSnapshot、Under the hood;A 级)。
- RocksDB Wiki, WritePrepared Transactions(WriteCommitted/Prepared/Unprepared、CommitCache、IsInSnapshot、two_write_queues;A 级)。
- Peng, Daniel & Dabek, Frank, Large-scale Incremental Processing Using Distributed Transactions and Notifications(Percolator),OSDI 2010(分布式 2PC 奠基;A 级)。
- RocksDB 9.x
源码:
utilities/transactions/(transaction_db_impl.*、optimistic_transaction.*、write_prepared_txn.*);include/rocksdb/utilities/transaction_db.h(A 级)。 - RocksDB 9.x
源码:
db/db_impl/db_impl_write.cc(CheckCallback、two_write_queues_);db/snapshot_impl.h(min_uncommitted_)(A 级)。 - 第 4 篇 WAL;第 7 篇 Snapshot;第 1 篇 TiKV 生态位。
- PingCAP TiKV 文档, Distributed Transaction(落地映射;B 级,不能单独支撑协议正确性)。
- db-frontier/12 HTAP(分布式事务开放问题入口)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【RocksDB 内核机制】Get 与 Snapshot:SuperVersion 与 sequence number
从 DBImpl::GetImpl 层级查找路径出发,拆解 LookupKey、sequence number 编码、SuperVersion 引用与 Snapshot 可见性边界;对照 PostgreSQL MVCC 的 txn id 语义差异。
【RocksDB 内核机制】生产嵌入对照:Flink · TiKV · Kafka Streams
从 RocksDB 内核侧补全 Flink KeyGroup 前缀与增量 checkpoint SST 上传机制,对照 TiKV Region 引擎与 Kafka Streams changelog 容错;不重复 stream/12 作业侧全文,给出 Docker 复现步骤而不伪造 checkpoint 指标。
【RocksDB 内核机制】LevelDB · WAL · Compaction · Column Family · 生产嵌入
补全存储引擎三角最后一角:从 LevelDB 基线与 RocksDB 架构演进,到 WAL/MemTable/SST 写路径、Get/Iterator 读路径、Leveled/Universal compaction 与 write stall,再到 Column Family、事务、Checkpoint 与 Flink/TiKV 嵌入对照。
【RocksDB 内核机制】LSM 生态全景:存储引擎三角与 RocksDB 生态位
闭合存储引擎三角的最后一角:对照 PG B-Tree 与列存 scan,定位 LSM 写优化引擎在 TiKV、Flink、Kafka Streams、ClickHouse Embedded 中的嵌入方式,并划分本系列与 lsm-tree DIY、storage/31–32、stream/12 的分工边界。