第 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 前瞻(B 级,草稿边界)
证据等级说明:以下对照来自 PingCAP TiKV 公开设计文档与 RocksDB 事务 Wiki 的 概念对齐,不是 TiKV 源码 walkthrough。Percolator 两阶段提交、Raft 复制、Coprocessor 不在本系列 18 篇范围内。
TiKV 在 单个 Region 内使用
RocksDB(或兼容实现)作本地 KV 引擎(第 1 篇 LSM
生态)。分布式事务在 Raft 之上 跑
Percolator 模型,与单机 TransactionDB
的关系可粗分为:
| 层次 | 机制 | 与本文 RocksDB 事务 |
|---|---|---|
| Region 副本 | Raft log + apply | Raft 提供复制与共识,不是 RocksDB 事务 |
| 分布式 txn | Percolator 2PC、锁列、commit TS | 语义类似 2PC,锁与 TS 编码在 key 空间 |
| 本地引擎 | RocksDB CF、MVCC 时间戳 | 内部 key 含 TS;可见性由 TS + snapshot
决定,而非应用直接调 BeginTransaction |
工程关联点(B 级,供第 16/18 篇展开):
- TiKV bulk load / BR 恢复可能触发
IngestExternalFile与 write stall(见 第 15 篇 与 PingCAP 社区文章对 ingest 的讨论)。 - RocksDB WritePrepared 的 prepare/commit 分离与 分布式 2PC prepare/commit 名称相近,但 层级不同:前者是单机引擎写策略,后者跨 Region/PD。
- Flink 不 使用 RocksDB 事务 API;TiKV 不能 用 RocksDB OCC 替代 Percolator——嵌入场景事务语义由 上层产品 定义。
本节随 TiKV 版本与 HTAP 续作 可能修订;读者应以 TiKV 官方 Transaction 文档为准。
八、选型简表
| 场景 | 建议 |
|---|---|
| 仅需批写原子 | WriteBatch |
| 低冲突读-改-写 | OptimisticTransactionDB |
高冲突或 GetForUpdate |
TransactionDB |
| MyRocks / 2PC 引擎 | TransactionDB + WritePrepared /
WriteUnprepared |
| Flink state | checkpoint 一致性,非 RocksDB 事务 |
| TiKV | Percolator + Raft(B 级前瞻) |
九、小结
- 事务 在 WriteBatch 原子性之上增加 冲突检测 与 Read-your-own-writes。
OptimisticTransactionDB在 commit 时 OCC,适合低冲突;需调max_write_buffer_size_to_maintain保历史。TransactionDB悲观锁,非事务写也参与锁;支持 2PC 与 WritePrepared。- WritePrepared 将 MemTable 写入提前到 Prepare,读路径用 CommitCache + IsInSnapshot 区分已提交与 prepared 数据。
- TiKV 事务在分布式层实现;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 级)。
- 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 生态位(A 级边界)。
- PingCAP TiKV 文档, Distributed Transaction(Percolator 模型;B 级前瞻,非本篇结论唯一依据)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【RocksDB 内核机制】LSM 生态全景:存储引擎三角与 RocksDB 生态位
闭合存储引擎三角的最后一角:对照 PG B-Tree 与列存 scan,定位 LSM 写优化引擎在 TiKV、Flink、Kafka Streams、ClickHouse Embedded 中的嵌入方式,并划分本系列与 lsm-tree DIY、storage/31–32、stream/12 的分工边界。
【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 内核机制】选型与存储栈阅读地图
用决策树收束 RocksDB 与 InnoDB、列存、湖仓的适用边界;给出存储引擎三角 + 数据平台全栈阅读地图,对接 query-engine/18 与 postgresql-kernel,并标注 HTAP/TiKV 续作入口。