第 8 篇 说明了冲突如何被检测;本篇回答更强的问题:检测通过之后,全局顺序是否不仅可串行化,还与客户端感知的实时顺序一致——以及为何系统用硬性的约 5 秒事务上限换取恢复与元数据有界。
Developer Guide 写:FoundationDB 提供带外部一致性(external consistency)的可串行化事务。SIGMOD 2021 摘要与 §2.4.2 使用 strict serializability。Known Limitations 则把「超过五秒的长事务」列为设计限制,并指向 Anti-Features 哲学:核心刻意不做某些事,以便把正确性与运维问题关进有界盒子。
本文是「FoundationDB 内核」系列第 9 篇(共 18 篇)。上一篇:Resolver 与 OCC;下一篇:Transaction Log;→ 系列目录
版本锚定:FoundationDB 7.x。不把论文或他文中的延迟/吞吐表写成通用结论;本系列实验台账中超时边界实验若未执行,正文只陈述官方错误语义。
一、可串行化、严格可串行化与实时顺序
1.1 三层含义
| 术语 | 直观含义 |
|---|---|
| 可串行化(serializability) | 并发执行的结果等同于某一串行次序 |
| 严格可串行化 / 外部一致 | 该串行次序还尊重实时:若事务 \(A\) 在实时上完全结束后 \(B\) 才开始,则串行序中 \(A\) 在 \(B\) 前 |
| 快照隔离(SI) | 读一致快照,但允许写偏斜等异常 |
FDB 的默认目标是第一行加第二行。SIGMOD 2021 §2.4.2 的论证链是:
- Sequencer 为事务分配 read version 与 commit version;
- read version \(\ge\) 开始时已知的最大 commit version;
- commit version \(>\) 一切已有 read/commit version;
- commit version 定义串行历史(兼作 LSN);
- 事务在 read version 上所见包含此前已提交写的结果,且 OCC 保证读集在提交前未被并发写破坏。
因此「等价于按 commit version 排序的串行执行」,并且后开始的事务拿不到更旧的全局提交点作为自己的读点——这把实时顺序嵌进版本分配。
1.2 外部一致的反例思维
若系统只保证普通可串行化,允许如下实时序:客户端 \(C_1\) 提交 \(A\) 并返回成功;墙钟之后 \(C_2\) 才开始事务 \(B\),但 \(B\) 的等价串行位置仍可能插在 \(A\) 之前——只要最终状态像某次串行执行。严格可串行化禁止这种「时间倒流」的等价类。FDB 用「新事务 read version 不低于已知最大提交」堵住该洞;代价是所有写共享版本推进与世代健康。
1.3 客户端如何「看见」外部一致
Architecture Determine Read Version:GRV Proxy 向 Master 取最大已提交版本,并确认满足复制策略的一组 transaction log 仍存活(避免旧世代 Proxy 在恢复后继续发 read version)。客户端因此获得的 \(r\) 高于它(通过集群)应已知的提交。跨进程「侧信道」若绕过数据库传递版本,文档仍要求 read version 不低于客户端已知的提交——这是外部一致的客户端义务与系统机制的交接面。
flowchart TD
A["Txn A commits at version c_A"] --> B["Master tracks max committed"]
B --> C["Later Txn B requests read version"]
C --> D["GRV Proxy: r_B >= c_A"]
D --> E["B reads snapshot at r_B"]
E --> F["B commits at c_B > r_B"]
若 B 在实时上于 A 成功返回之后才开始,却读到不含 A 的快照,就破坏严格可串行化。FDB 用中心版本与 GRV 路径堵住这条缝;代价是写路径依赖 Sequencer / Master 世代健康(第 6、13 篇)。
二、OCC + MVCC:如何凑成 SSI 叙事
论文称 FDB 通过 OCC 与 MVCC 的组合实现 Serializable Snapshot Isolation(SSI)语义,并达到 strict serializability。读侧:Storage 保留短窗口多版本,按 read version 取快照。写侧:不持锁,提交时验证。与「先拿 commit 时间戳再验证」或「验证后再戳时间戳」的变体相比,FDB 先由 Sequencer 定 commit version,再做冲突检测,以便批处理版本分配与验证(§2.4.2)。
Snapshot read 是显式出口:同一事务内可以放弃部分读的严格性以减少冲突。默认路径不走这个出口。Atomic operations 则通过「提交时才逻辑读」消除一类 read-write 自冲突,但不削弱对其它事务读该 key 的保护。
2.1 先定版本再验证的含义
先分配 commit version 再做
OCC,意味着冲突检测看到的是已经排进全局序的候选事务。失败的事务仍占用过一个
commit version 号,但不会把写集提交进 TLog(Proxy
在冲突后直接
not_committed)。版本号稀疏前进是正常的;监控「版本速率」时不要把它直接当成成功
TPS。
与某些「验证通过后再贴时间戳」的 OCC 变体相比,FDB 的选择更利于 Proxy 批处理:一批事务共享一次 Sequencer 往返,再并行或流水进入 Resolver。代价是假阳性与版本空洞需要靠短窗口消化,而不是靠精细的逻辑锁表。
三、5 秒限制:不是疏忽,是有界性工具
3.1 官方语义
Known Limitations Long running transactions:
- 自事务内第一次读起约 5
秒后,后续需访问数据库的读通常触发
transaction_too_old; - 带写的提交可能得到
transaction_too_old或not_committed。
这与 Resolver 只保留约 5 秒写历史、Storage 保留约 5 秒可变版本(Architecture: Storage Servers)对齐。客户端必须在窗口内完成「读—计算—提交」,或把大逻辑拆成多个短事务(Known Limitations 给出分层 versioning、间接交换等 workaround 方向)。
3.2 与恢复有界性的关系
SIGMOD 2021 设计原则 Fail fast and recover fast:把故障收敛到统一恢复路径,并追求短 MTTR。§2.4.4:恢复需确定 Recovery Version(RV),并把旧世代日志中有界后缀拷到新 TLog;论文写明该范围「只包含几秒日志」。5 秒窗口使:
- Resolver 状态可丢弃重建(无状态角色 + 有界历史);
- Storage 回滚未进入持久引擎的内存多版本(论文:丢弃内存 MVCC,再从新 LS 拉);
- 恢复时拷贝的 log 后缀保持秒级体量,而不是「任意长事务留下的无限 redo」。
Architecture 还写:恢复会把时间快进约 90
秒,使进行中的客户端事务因 transaction_too_old
失败并在重试时找到新世代。这是用客户端可观察错误换取世代切换清晰,而不是悄悄续上旧
Proxy。
flowchart LR
subgraph Bound["Bounded 5s window"]
R["Resolver write history"]
M["Storage in-memory versions"]
L["TLog suffix to copy on recovery"]
end
Bound --> Rec["Recovery stays time-bounded"]
Long["Unbounded long txn"] -.->|would expand| Bound
distributed/39 已概括「5 秒限制服务恢复有界」这一设计意图;本篇用论文恢复章节与官方 Limitations 把它钉成机制,而不是产品口号。
3.3 10 MB 与其它硬限制(边界)
Known Limitations:单事务受影响数据不超过 10,000,000 字节;key ≤ 10 KB,value ≤ 100 KB。它们限制的是冲突检测与日志体积,与 5 秒一起构成「事务必须小而短」的契约。本篇不展开数据建模技巧,只标明:严格可串行化保证落在遵守这些契约的事务上。
3.4 Anti-Features 哲学上的位置
FoundationDB 文档把部分「不做」写成 Anti-Features:核心保持最小,把数据模型推到 Layer。长事务落在同一哲学:允许任意长读写交互会迫使 Resolver/MVCC/恢复状态无界,从而侵蚀「故障即恢复、恢复必快」的假设。5 秒因此不是单独抠出来的魔法常数宣传,而是与有界状态一致的产品选择;若业务必须分钟级快照,Known Limitations 建议在 Layer 做版本化,而不是要求核心取消窗口。
从选型角度预先回答一个常见问题:若业务天然是高冲突单行更新,FDB 默认 OCC 是否「选错」?答案不是绝对否——原子操作、打散 key、Layer 侧队列化都能把冲突率压回去——但若这些改造成本高于换用悲观模型或分片锁系统,应在第 18 篇的对照框架里显式权衡,而不是指望调一个 FoundationDB 旋钮恢复严格可串行化下的高争用吞吐。本篇把问题留在开放区,避免假装有通用阈值。
3.5 与 10 MB 限制一起读的「小事务」契约
时间上限与体积上限经常被分开吐槽,但它们服务同一目标:让冲突历史、日志扇出与恢复拷贝保持可预测。一个 4 秒内写满接近 10 MB 的事务,与一个读很多 key、几乎写满 5 秒窗口的事务,都会把 Resolver 与 TLog 推向压力区。Known Limitations 建议:单事务受影响数据超过约 1 MB 就应重新设计——这是可用性提示,不是硬错误阈值宣传。Layer 设计(Record Layer 索引维护等)必须把二级索引更新计入同一事务的体积预算,否则会在「业务看起来只改一行」时撞上限。
四、高冲突:开放问题而不是调参口号
论文坦承 OCC 不保证事务一定能提交,并依赖生产冲突率很低。Developer Guide Minimizing conflicts 给出工程处方:打散热点、原子操作、并行读以缩短窗口、谨慎使用 snapshot。
仍开放的是定量边界:
- 在高度倾斜的单 key 更新下,重试是否使有效吞吐塌陷到不可用?
- 假阳性冲突(多 Resolver)在何种分片与工作负载下变得可测?
- 与 Percolator 悲观锁、或确定性调度(Calvin 一类)相比,哪类负载应换模型?
本系列不提供未跑实验的曲线。选型收束见 第 18 篇;排障决策树见 第 17 篇。
stateDiagram-v2
[*] --> Running: read version granted
Running --> Committing: client commit
Committing --> Committed: OCC pass + TLog durable
Committing --> Retry: not_committed
Running --> AbortOld: transaction_too_old
Retry --> Running: client restarts txn
AbortOld --> Running: client restarts txn
Committed --> [*]
高冲突时状态机在 Retry
上打转:每一次重试都重新取 read
version、重新读、重新提交。有效工作随冲突率近似按 \((1-p)^n\) 衰减(\(p\) 为单次冲突概率,\(n\)
为逻辑操作被拆成的事务次数)——这是定性模型,不是拟合曲线。缓解手段改变的是
\(p\)
与窗口长度,而不是取消严格可串行化本身。
与 第 8 篇 的假阳性叠加时,应用看到的「冲突」可能高于真实数据依赖;排查时应区分热点 key、过大 read range、以及 Resolver 分片边界,而不是先怪 Sequencer「太慢」。
五、与 snapshot read / 原子操作的边界
严格可串行化是默认路径。两条官方提供的削弱或绕开局部冲突的机制,常被误读成「系统其实是 SI」:
- Snapshot read:该次读不进入 read conflict range,并发写可改变你「本应」保护的集合;文档要求你用显式 conflict range 补回真正在意的 key。
- Atomic operations:逻辑上含读,但不把当前值暴露给客户端,冲突语义接近「只写」;适合计数器,不适合「读出旧值做复杂分支」却仍想零冲突。
两者都不改变 Sequencer 版本的全序,也不取消 TLog 提交点;它们只移动 OCC 检查的集合。第 8 篇已拆 API;本篇强调:讨论严格可串行化时,必须声明事务是否全程使用默认读。
另外,只读事务不访问 Resolver、可本地结束(论文
§2.4.1),其「串行点」就是 read version 上的快照。它们享受
MVCC 的无锁读,却仍受 5 秒窗口约束——长只读同样会
transaction_too_old。
六、学术谱系、工程间隙与开放问题
谱系:严格可串行化 / linearizability 与数据库隔离的交叉长期由 Herlihy & Wing(linearizability)、以及数据库隔离文献(Adya、Berenson 等)共同刻画。FDB 选择「全局 Sequencer 版本 + OCC」而不是每分片 Raft 排序 + 两阶段锁,是对低争用 OLTP 与快速恢复的押注。与 Spanner 的 TrueTime 提交等待相比,FDB 不用不确定性窗口等待来换外部一致,而是用中心版本与短事务窗口——假设与故障模型都不同,不能直接比谁「更强」。
工程间隙:论文评价章节含生产延迟与重配置时间,但那些数字绑定特定集群与时期。把「严格可串行化」理解成「任意长、任意大事务的免费午餐」,会在 Limitations 上撞墙——这是文档已写明的产品边界,不是实现 bug。
开放问题:云盘尾延迟与 Storage
多版本窗口相互作用时,客户端是否更频繁撞
transaction_too_old,从而在「强隔离」下被迫把业务拆成弱一些的多事务协议?这需要受控实验;本篇只把问题立住。
七、常见误解
| 误解 | 事实 |
|---|---|
| 「可串行化已经包含实时顺序」 | 普通可串行化允许与实时相反的等价串行序;严格可串行化才绑实时 |
| 「5 秒是临时缺陷」 | Known Limitations 将其归为设计限制;服务于有界 MVCC/恢复 |
| 「严格可串行化所以可以长事务」 | 恰恰相反:强保证落在短事务契约上 |
| 「恢复快进 90 秒会丢已提交数据」 | 快进使旧事务报错重试;已 durable 提交由恢复版本协议保留(第 10、13 篇) |
| 「SI 与严格可串行化只是名字不同」 | SI 允许写偏斜等;FDB 默认读集参与冲突以挡住这类现象 |
| 「开了 snapshot 仍声称全程严格可串行化」 | Snapshot 局部削弱隔离;需在文档与代码审查中显式标记 |
八、小结
三句话小结:
- Sequencer 版本定义串行历史,GRV 路径保证新事务的读点不低于已知提交,OCC 保证读集有效,合起来对齐严格可串行化。
- 约 5 秒上限与 Resolver/Storage 的有界多版本窗口一致,使恢复拷贝的日志后缀与冲突状态保持秒级有界。
- 高冲突下 OCC 重试放大仍是开放的工程问题;处方是减读集与改数据模型,而不是指望取消时间上限。
下一篇进入 Transaction Log:复制、durable version、提交点,以及与 Storage 异步消费的解耦。
上一篇:Resolver 与 OCC
返回 系列目录
写偏斜对照实验若要在本系列补做,应同时跑「默认读」与「snapshot 读」两组,并记录冲突错误码分布;在实验台账完成前,正文只保留机制判断。
文末核对提示
本节补记:机制结论以 SIGMOD 2021 与 FoundationDB 7.x 官方
Architecture / Developer Guide / Known Limitations
为准;若源码字段名随小版本重命名,以对应 tag 的
fdbserver 实现为准,不把过时幻灯片当契约。
阅读本篇时建议同时打开系列 index 中的依赖图:本篇上承写路径与版本分配,下接恢复与排障;不要孤立地把单角色文档当成完整正确性证明。
本节补记:机制结论以 SIGMOD 2021 与 FoundationDB 7.x 官方
Architecture / Developer Guide / Known Limitations
为准;若源码字段名随小版本重命名,以对应 tag 的
fdbserver 实现为准,不把过时幻灯片当契约。
阅读本篇时建议同时打开系列 index 中的依赖图:本篇上承写路径与版本分配,下接恢复与排障;不要孤立地把单角色文档当成完整正确性证明。
参考资料
论文(A 级)
- Zhou et al. FoundationDB…. SIGMOD 2021(§2.4.2 严格可串行化、§2.4.4 恢复与 RV、设计原则 Fail fast)。
- Kung & Robinson. TODS 1981(OCC)。
- Berenson et al. A Critique of ANSI SQL Isolation Levels. SIGMOD 1995(隔离现象;对照 SI)。
- Herlihy, Maurice, Wing, Jeannette. Linearizability: A Correctness Condition for Concurrent Objects. TOPLAS 1990(外部一致 / 线性相关术语谱系)。
官方文档(A 级)
- FoundationDB 7.x, Developer Guide — 严格可串行化 / 外部一致性表述、snapshot 与原子操作。
- FoundationDB 7.x, Known Limitations — Long running transactions、事务大小。
- FoundationDB 7.x, Architecture — Determine Read Version、Resolvers、Recovery 快进。
- FoundationDB, Anti-Features — 核心最小集哲学(与长事务限制对照阅读)。
站内对读
- 第 8 篇 Resolver、第 10 篇 TLog、第 13 篇恢复。
- distributed/39(5 秒限制的选型表述)。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【FoundationDB 内核】Resolver 与 OCC:冲突范围、窗口与自动重试
拆解 FoundationDB Resolver 如何用 read/write conflict ranges 做乐观并发控制:冲突窗口落在 read version 与 commit version 之间,客户端自动重试;并与 TiKV Percolator 快照隔离对照机制差异,不写无实测性能结论。
【FoundationDB 内核】Client API 与事务模型:冲突范围、自动重试与 5 秒窗口
钉住 FoundationDB 客户端事务契约:读写缓冲、冲突范围、@transactional 自动重试,以及 5 秒上限如何约束应用设计——机制证明留给第 8–9 篇。
【FoundationDB 内核】Sequencer:read/commit version、逻辑单点与恢复
说明 Sequencer 如何分配 read version 与 commit version、为何是逻辑单点而非无高可用,以及世代恢复如何与版本推进衔接。
【FoundationDB 内核】Transaction Log:提交点、durable version 与异步解耦
拆解 FoundationDB Transaction Log(TLog)如何作为分布式 WAL:按复制策略同步 durable 后 Commit Proxy 才返回成功;说明 durable version、known committed version 与恢复版本的关系,以及 Storage Server 异步消费如何离开提交临界路径。