type
status
date
slug
summary
tags
category
icon
password
拆解事务型系统(Decomposing Transactional Systems)
每一个事务型系统都要完成四件事:
- 执行事务(execute)
- 排序事务(order)
- 验证事务(validate)
- 持久化事务(persist)
执行事务
执行事务指的是运行事务体,产生预期的读写操作。不同系统在如何执行事务体上仍有相当大的差异:
- 写操作可以在这一阶段立即落到存储,也可以先缓存在本地,最后一次性提交。
- 同一个事务出于不同目的可能被执行不止一次。
排序事务
排序事务意味着为事务分配某种“发生时间”的概念:
- 可能是版本号、时间戳、日志序列号(LSN),或者更复杂的“先后发生”描述。
- MVCC 数据库通常会给出两个版本:初始读版本和最终提交版本。
- 这里我们主要关注选择提交版本的那个时刻——数据库宣称所有读写在此刻原子性地发生。
验证事务
验证事务旨在执行并发控制,或在少数情况下执行特定领域语义的校验:
- 如果事务因为系统定义的原因(例如与现有事务存在可串行化冲突)而需要被拒绝,就在此阶段发生。
- 若验证发生在排序之后,它会检查分配的顺序是否仍然有效;
- 若验证发生在排序之前,它会给出一系列可接受的提交版本,由排序阶段最终选定一个。
持久化事务
持久化事务让事务变得耐久,通常是写入磁盘:
- 有些系统在执行过程中就会逐步把写操作刷盘;
- 关键点在于:所有写操作以及标记事务已提交的提交记录都完成持久化时,事务才算真正耐久。
- 这一阶段常与系统的复制机制和原子提交协议挂钩,两者界线有时并不清晰。
所有上述四个步骤都必须在系统向客户端确认事务结果之前完成。然而,这些步骤的顺序可以任意调整,也可以并发执行。不同的系统通过重新排列这些步骤,达到了各自不同的权衡与设计目标。
例子(Examples)
经典 乐观并发控制(OCC)数据库会先执行事务并记录其读集、写集。
执行完成后,系统分配一个提交版本,并检查并发事务是否与之冲突。
若未发现冲突,事务就被持久化并被确认已提交。
经典 OCC 的步骤拆解OCC │ Execute │ Order │ Validate │ Persist │
经典 悲观并发控制(PCC)数据库在执行事务期间获取锁,以排斥其他可能冲突的事务。
当事务执行完毕后,它取得提交版本,然后将数据持久化到磁盘,最后释放锁。
经典 PCC 的步骤拆解 [1]PCC │ Execute │───────────────Validate───────────────│ Order │ Persist │
[1] 上述图示仅用来展示步骤之间的相对先后关系,并不暗示任何性能或效率。图条更细或步骤更并行,并不一定更好。
现在,让我们看看几个真实世界中的案例。
➔ FoundationDB
FoundationDB [2] 是一个分布式、事务型数据库,将传统单体数据库拆解为一组可独立扩展的微服务。论文中描述的事务处理流程如下:
客户端 首先联系集群中的某个 Proxy 以获得读版本(即时间戳)。Proxy 随后向 Sequencer 请求一个读版本,该版本保证不小于之前任何已发放的事务提交版本,并将该读版本返回给客户端。客户端随后可使用该读版本向 StorageServer 发起多次只读请求,获取该版本下的数据。事务期间产生的写操作在本地缓冲,无需再联系集群。提交阶段:客户端将读写集合(即键范围)连同写操作一起发送给某个 Proxy,等待 commit 或 abort 的回复。如果事务无法提交,客户端可能选择从头重试。
Proxy 以三步提交事务:向 Sequencer 申请一个大于所有现存读/提交版本的 提交版本(Sequencer 以每秒一百万的速率递增)。将事务信息发送给分片的 Resolver,使用 FoundationDB 的乐观并发控制算法检测读写冲突。若所有 Resolver 均返回 无冲突,事务继续;否则标记为 abort。对于已确认提交的事务,将其发送给一组 LogServer 进行持久化。当所有指定的 LogServer 回复成功后:Proxy 将 已提交版本 报告给 Sequencer(确保后续事务的读版本在此之后),并回复客户端。同时,StorageServer 持续从 LogServer 拉取变更日志并将已提交的更新写入磁盘。
从论文中的拆解可见:
- 客户端执行 事务。
- Proxy 获取 提交版本。
- Resolver 进行 冲突检测。
- 事务被持久化。
以上步骤按顺序串行完成。
FoundationDB 的步骤拆解FoundationDB │ Execute │ Order │ Validate │ Persist │
熟悉的图景!FoundationDB 在宏观流程上与经典 OCC 数据库一致。若你只想深入理解系统中的某一个组件(例如分布式冲突检测的 Resolver),完全可以暂时用最简化的乐观数据库实现来替换其他子系统(例如把 LogServer 简化为单机顺序写 WAL)。
[2] Jingyu Zhou, Meng Xu, Alexander Shraer, Bala Namasivayam, Alex Miller, Evan Tschannen, et al.
FoundationDB: A Distributed Unbundled Transactional Key Value Store, SIGMOD 2021, 2653‑2666.
➔ Spanner
Spanner [3] 是 Google 的旗舰级数据库,被广泛部署在 Google 内部;其分区 Paxos + 分布式事务的架构也启发了后来一大批 NewSQL 系统 [4]。
以下内容节选自论文 §4.2.1 Read‑Write Transactions 对事务协议的描述,并按「执行 / 排序 / 验证 / 持久化」四步进行拆解。
执行 / 验证
- 和 Bigtable 类似,事务中的写操作在客户端缓冲,直到提交才发送。
- 因此,事务内的读看不到自己写入的内容。
- 读操作期间,客户端向相应数据分片的领导副本发起请求;领导副本获取读锁后返回最新数据。
- 事务打开期间,客户端会定期发送 keep‑alive,防止参与分片超时。
- 当客户端完成所有读且写缓冲完毕后,就启动两阶段提交:选择一个协调者分片,将「协调者身份 + 缓冲写集」发送给每个参与分片的领导者。由客户端驱动 2PC 避免了跨广域网重复传输数据。
排序(prepare / commit 决议)
- 参与分片(非协调者):
- 先获取写锁;
- 选择一个 prepare 时间戳(须大于该分片之前发出的所有时间戳以保持单调性);
- 通过 Paxos 记录 prepare;
- 将该 prepare 时间戳汇报给协调者。
- 协调者分片:
- 同样先获取写锁,但跳过 prepare 阶段;
- 在收到所有参与者的 prepare 时间戳后,选择全局 commit 时间戳 s,要求
- s ≥ 所有 prepare 时间戳,
- s > TT.now().latest(协调者收到 commit 消息时的物理时间上界),
- s > 协调者此前分配给其他事务的任何时间戳;
- 通过 Paxos 记录 commit(或在等待参与者超时后记录 abort)。
验证(commit‑wait 规则)
- 任何协调者副本在真正应用 commit 前,都会等待到 TT.after(s),以满足论文 §4.1.2 的 commit‑wait 规则;预计等待 ≥ 2 ε,通常与 Paxos 通信并行。
- 之后,协调者把 commit 时间戳发送给客户端及所有参与者;各参与者通过 Paxos 记录结果并在同一时间戳应用写入,然后释放锁。
作者拆解
- 执行与验证交织:读锁在执行阶段即被持有。
- 写操作在客户端缓冲,等提交时再发送。
- 发起 2PC,检查所有参与者是否可提交。
- 协调者在 prepare 阶段收齐最小必要时间戳后,决定最终 commit 版本。
- 事务被持久化。
- 最后释放读写锁。
Spanner 的流程示意Spanner │───────── Validate ─────────││ Execute │ Order │ Persist │
尽管描述显得复杂得多,Spanner 的整体事务流程仍与经典悲观并发控制数据库非常相似。因此,阅读论文时不妨把问题简化为:「它是如何实现与 SERIALIZABLE MySQL 等价的?」然后逐段比较两者在每个步骤上的差异。
[3] James C. Corbett, et al. 2012. Spanner: Google’s Globally‑Distributed Database. OSDI.
[4] 顺带一提,比较 CockroachDB、TiDB、YugabyteDB 各自与 Spanner 的差异,亦是一段颇有洞见的旅程。
➔ TAPIR
TAPIR⁵ 是一个严格可串行化(strictly serializable)的数据库,它宣称在 Spanner 基础上通过新颖的复制协议,提供更低的延迟和更高的吞吐量。TAPIR 的核心协议在论文 §5.2.1 中描述如下:
事务执行(Execute)
- Write(key, object)
客户端将 key 和 object 暂存在写集合(write set)中,直到提交才发送,操作立即返回。
- Read(key)
- 如果 key 已在事务的写集合中,客户端直接返回写集合中的 object;
- 如果此次事务之前已读过该 key,返回缓存的结果;
- 否则,客户端向某个副本发送
Read(key)
请求。
- 副本响应
收到
Read
后,副本返回对应的 object 及其版本号 version(version 是写入该版本的事务的时间戳)。- 客户端处理
客户端收到响应后,将
(key, version)
加入事务的读集合(read set),并将 object 返回给应用层。当应用调用
Commit
或 Abort
后,执行阶段结束。排序与校验(Order & Validate)
为了提交事务,TAPIR 客户端会协调所有参与分片(负责该事务读写 key 的各分片),为该事务分配一个与严格串行顺序一致的唯一时间戳。流程如下:
- 选取候选时间戳
客户端生成一个提议时间戳(timestamp),为保证全局唯一,通常使用本地时钟与客户端 ID 的元组。
- Prepare 共识
客户端通过 IR(一致性复制层)向所有分片并发发起
Prepare(txn, timestamp)
,其中 txn
包含事务 ID、读集合和写集合。- 副本处理 Prepare
- 若事务日志已存在该
txn.id
,则返回PREPARE-OK
(若事务已提交)或ABORT
(若已中止); - 否则,若
txn.id
已在本地的 prepared 列表中,也返回PREPARE-OK
; - 否则,副本执行 TAPIR 的 OCC 校验,检测读写集合在该 timestamp 下的冲突(详见论文图 8)。
- 汇总结果
- 若所有分片均回复
PREPARE-OK
,客户端发送Commit(txn, timestamp)
; - 若任一分片回复
ABORT
或ABSTAIN
,客户端发送Abort(txn, timestamp)
; - 若有分片回复
RETRY
,客户端更换时间戳重试(限制重试次数)。
持久化与完成(Persist)
- Commit 接收
- 将事务记录写入本地事务日志;
- 在版本化存储中应用写集合;
- 从 prepared 列表移除该事务(如存在);
- 向客户端回应完成。
分片收到
Commit
后:- Abort 接收
- 记录中止日志;
- 从 prepared 列表移除该事务(如存在);
- 向客户端回应完成。
分片收到
Abort
后:尽管描述较长,上述流程可简化为三步:
- 执行事务
- 客户端选取提交时间戳并运行 Prepare
- 各分片并行执行 OCC 校验并持久化到 prepare 日志
因此,TAPIR 的四步拆解示意图就是:

这也凸显了 TAPIR 的核心思想:将并发控制校验(Validate)与提交结果持久化(Persist)协议融为一体。
此外,TAPIR 为本文所用的事务系统拆解方法提供了启发:它那张清晰的流程图,常常是我在阅读复杂论文时回过头去看的“救命图”。

⁵ Irene Zhang, Naveen R. Kr. Sharma, Adriana Szekeres, Arvind Krishnamurthy, Dan R. K. Ports.
Building consistent transactions with inconsistent replication, SOSP ’15.
➔ Calvin
Calvin⁶ 是确定性数据库(deterministic database)的标志性系统,后续对其设计各方面进行改进的论文均保留了相同的整体特性。在论文 §3 “系统架构” 中,Calvin 的架构被描述为:
Calvin 的核心在于将系统分为三个独立的处理层
- 排序层(Sequencer):截获事务输入,将其放入全局事务输入序列——该序列即所有副本在执行时必须保持的事务顺序。排序层同时负责该输入序列的复制和日志记录。
- 调度层(Scheduler):使用确定性锁机制,按照排序层给定的全局顺序调度事务执行,同时借助一组执行线程实现并发执行。(虽然在图 1 中这些执行线程绘制在调度器组件下方,但在逻辑上它们属于调度层。)
- 存储层(Storage):负责所有物理数据布局。Calvin 事务通过简单的 CRUD 接口访问数据;任何支持相同接口的存储引擎都可方便地插入到 Calvin 中。
由此,Calvin 的事务处理流程可拆解为:
- 将事务按序写入全局日志。
- 将该日志持久化。
- 获取锁,以在并发环境中确保按全局顺序安全执行。
- 执行事务。
- 释放所有已获取的锁。
Calvin 的步骤拆解图Calvin │ Order │ Persist │ Validate │ Execute │
Calvin 是最著名的“在提交前不执行事务”数据库示例。它由此获得了两大好处:
- 提交阶段完全免疫于并发竞争,延迟和吞吐表现稳定;
- 同时也带来缺点:长事务会阻塞后续所有已提交事务的执行。
⁶ Alexander Thomson, Thaddeus Diamond, Shu‑Chun Weng, Kun Ren, Philip Shao, Daniel J. Abadi.
Calvin: fast distributed transactions for partitioned database systems, SIGMOD 2012.
➔ CURP
Exploiting Commutativity For Practical Fast Replication⁷ 中定义了一种一致的无序复制协议(Consistent Unordered Replication Protocol, CURP),允许客户端在操作尚未排序时就进行复制,只要这些操作在语义上可交换。以下节选自论文 §3.2 “普通操作流程”:
- 客户端–主节点交互
客户端向主节点(masters)发送更新 RPC 请求。如未收到响应则重试;若主节点宕机,可向其他服务器重试。
- 1 RTT 更新与持久化保证
为了实现 1 RTT 更新,主节点会在将数据复制到备份前就回复客户端。与此同时,客户端在等待主节点响应时,会并行地将该请求记录到 f 个见证者(witnesses)上。只有当所有 f 个见证者都接受后,客户端才确认该请求可以抵抗主节点宕机,然后才用主节点返回的结果完成操作。
- 见证者的冲突校验
见证者仅在新请求与其已保存的所有操作可交换时,才接受客户端的 record RPC;若不可交换,见证者因无法保证与主节点执行顺序一致,就会拒绝该 RPC。例如,若见证者已接受
x←1
,则不能接受 x←5
。- 见证者的独立性
- 如 §3.2.1 所述,客户端只有在所有 f 个见证者均接受其 record RPC 后,才能跳过与备份的同步;
- 每个见证者都只保证自身保存的操作可交换,且恢复时仅需选用其中一个见证者(§3.3)。
每个见证者独立运行,无需就操作的顺序或持久性达成一致。即使在异步网络中,RPC 到不同见证者的先后顺序各异,也不会破坏一致性:
- 主节点的传统角色
在 CURP 中,主节点(masters)与传统主备复制一致:接收、串行化并执行所有更新 RPC;如果操作更新了系统状态,主节点再将更新后的值或已排序操作日志同步(sync)给备份。
流程拆解
- 客户端从主节点读取数据,并将写操作同时发送给复制组的领导者和所有追随者。
- 各副本并行执行冲突检测,并将事务记录到本地。
- 向客户端回复后,最后完成事务排序。
CURP 的步骤拆解CURP │ Execute │ Validate │ Persist │ Order │
上图也恰好展示了 CURP 的独特之处:排序事务是它做的最后一步,也正是“最后才排序”造就了它的所有优势。
⁷ Seo Jin Park 和 John Ousterhout. 2019. Exploiting Commutativity for Practical Fast Replication. NSDI ’19.
➔ TicToc
TicToc⁸ 引入了一种新的事务协议:
- 它为每个数据项分配读时间戳(rts)和写时间戳(wts),
- 并利用这两组时间戳来延迟计算事务的最终提交时间戳。
这样做的好处是:
- 不需要集中式地分配时间戳;
- 能够提交那些传统时间戳排序方案会因为冲突而中止的事务。
协议流程(节选自 §3.2)
- 执行阶段(Execute)
- 在读阶段,DBMS 为每个事务维护读集合(read set)和写集合(write set)。
- 访问过的数据项:
- 将其拷贝到读集合;
- 若被修改,则写到写集合。
- 读/写集合中的每条记录均编码为
(tuple, data, wts, rts)
: tuple
:指向数据库中该元组的指针;data
:元组的值;wts
、rts
:分别是事务首次写入或读取该元组时所见版本的时间戳。- 对于读集合条目,TicToc 保证
wts ≤ rts
,即该条目在[wts, rts]
时间段内对本事务可见。
- 排序阶段(Order)
- 验证阶段第一步:按照主键顺序对写集合中的所有元组加锁,以防其他并发事务更新同一行。
- 这种固定的加锁顺序可保证与其他同时提交的事务之间不会产生死锁。
- 验证与排序(Validate & Order)
- 验证阶段第二步:根据读/写集合中已存的时间戳,计算提交时间戳 (
commit_ts
): - 对于只在读集合中出现的元组,
commit_ts ≥ wts
,以保证该元组版本在commit_ts
时依然有效; - 对于写集合中的元组,
commit_ts ≥ (previous rts + 1)
,以维护时间戳单调递增。
- 持久化阶段(Persist)
- 验证阶段最后一步:对读集合再做一次检查——若
commit_ts ≤ rts
,说明读到的版本在commit_ts
时依然有效; - 否则,该事务必须中止;或(若没有冲突)可将读集合中相应条目的
rts
延展到commit_ts
,使其在commit_ts
时合法。 - 通过所有验证后,事务进入写入阶段:将写集合中的所有更新持久化到数据库。
简化拆解
- 执行事务,并将写操作缓冲到本地。
- 执行期间记录每个元组的
wts
/rts
,逐步收窄可选的commit_ts
区间。
- 验证开始时,选择一个最靠前仍合法的
commit_ts
,检查是否有冲突。
- 验证通过后,将写入应用并持久化。
TicToc 的四步示意图TicToc │ Execute │──────── Validate ────────│ Persist ││Order_│
⁸ Xiangyao Yu, Andrew Pavlo, Daniel Sanchez, Srinivas Devadas.
TicToc: Time Traveling Optimistic Concurrency Control, SIGMOD 2016.
作业(Homework)
鉴于此,这里随意列举⁹了几种其他事务处理系统,供你拿来练习;它们在事务处理方式上各不相同:
- CockroachDB:The Resilient Geo‑Distributed SQL Database
- 建议先从 CockroachDB 的事务层文档入手,之后再作为后续步骤研究其并行提交(parallel commit)和读重启(read restart)优化。
本篇博文中的流程图均来自我的 Concurrent Operation Diagram Generator 工具。你可以在该页面上使用它生成自己的图示,也可以查看本博文源码了解如何用文本描述这些图。
有些数据库系统的作者已经为你“做完作业”并在自己的博客里分享了系统分解,去看看吧:
构造事务系统(Composing Transactional Systems)
对这样一套分解方法的有趣用法是:反过来提出问题。
- 任选一种你“编造”的执行(execute)、排序(order)、验证(validate)、持久化(persist)这四步的排列或交错方式,画出对应的图示;
- 然后回答:我需要如何设计一个数据库,才能让它恰好分解成这个图示?
在所有可能的先后顺序和并发执行的组合里,至少能产生 74 种不同类型的数据库。我们只覆盖了其中一小部分;每种不同的步骤顺序,都会带来事务处理上的某些独特特性。如果你想构建并发表一种全新的事务系统:
- 找到一个文献里尚未充分探索过的步骤顺序;
- 设计一个按该方式执行事务的系统;
- 然后分析它在性能和语义上的优缺点。
▼ 所有可能的步骤顺序一览
下面列出了执行(Execute)、排序(Order)、验证(Validate)、持久化(Persist)四个步骤的全部组合(共 74 种):
还有使用并发分组(用
{}
表示同时进行的步骤)的各种变体,例如:(以上即为完整的 74 种可能,你可以挑选、混排,设计自己的“另类”事务系统!)
⁹ 欢迎将你最喜欢的“怪诞”事务处理论文发给我,一起加入到作业名单里!