业务代码里最常见的 FoundationDB 用法,不是手写「拿版本 →
读存储 → 提交」,而是一段
@transactional(或等价的 db.run /
runAsync)函数:里面像本地 map 一样
get /
set,框架负责提交失败时整段重跑。第一次对接的人容易低估两件事:读过的
key range 会进入冲突检测,以及
事务默认活不过约 5 秒。
本文是系列第 2 篇,只钉客户端可见的事务契约:API 形态、读写集与冲突范围、自动重试语义,以及 5 秒限制的应用侧含义。Resolver 如何用这些范围做 OCC、严格可串行化如何成立,分别留给第 8、9 篇;此处只预告,不证明。
本文是「FoundationDB 内核」系列第 2 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 1 篇 · Unbundled 全景 角色与事务路径 第 2 篇 · Client API 冲突范围、重试、5 秒预告 第 3 篇 · Coordinator 配置与招募
版本锚定:FoundationDB 7.x Developer Guide(Transactions)、Known Limitations、API Reference;Zhou et al., SIGMOD 2021 §2.2 System Interface、§2.4.1–2.4.2。示例使用官方 Python binding 形态;不伪造运行输出。
一、事务看起来像本地 Map,提交才接触写路径
Zhou et al.(SIGMOD 2021 §2.2)给出的接口核心是:单 key
的 get / set,以及 range 上的读与
clear。事务在某一版本上观察数据库快照;写缓冲在客户端,直到
commit
才进入集群写管线;读己之写由客户端把缓冲与远端读结果合并。
官方 Developer Guide 推荐的模式是把业务逻辑包进可重入函数,由 binding 在可重试错误上循环:
@fdb.transactional
def set_status(tr, user_id, status):
key = b"user:" + user_id
tr[key] = status等价地,许多 binding 提供
db.transactional(fn) 或显式:
def set_status(db, user_id, status):
tr = db.create_transaction()
while True:
try:
tr[b"user:" + user_id] = status
tr.commit().wait()
return
except fdb.FDBError as e:
tr.on_error(e).wait()on_error
会根据错误码决定是否重置事务并继续循环;不可重试错误则向上抛出(Developer
Guide: Error
handling)。关键不变量:重试时函数从头执行,不能假设「上一次循环里的副作用已经生效」。
flowchart TD
start["Begin transactional fn"] --> getRV["Client gets read version<br/>via Grv Proxy"]
getRV --> body["Execute gets / sets / clears<br/>writes buffered locally"]
body --> commit["commit()"]
commit --> ok{"Committed?"}
ok -->|"yes"| done["Return"]
ok -->|"retryable error"| reset["on_error: reset txn"]
reset --> getRV
ok -->|"non-retryable"| fail["Propagate error"]
1.1 大小与键值限制(设计边界)
Known Limitations(Design limitations)写明:
| 限制 | 量级(官方文档) | 备注 |
|---|---|---|
| 单事务 affected data | 不超过 10,000,000 字节 | 含读写的 key / range / conflict range;读到的 value 不计入 |
| 建议避免的大事务 | 超过约 1 MB affected data | 文档警告可能影响性能与短暂可用性 |
| Key 长度 | ≤ 10,000 字节 | 超限客户端报错 |
| Value 长度 | ≤ 100,000 字节 | 超限客户端报错 |
这些是应用建模约束,不是调参能消掉的软上限。大对象、长事务要用 Layer 或拆事务技巧(文档 Workarounds),本系列第 15 篇在 Record Layer 边界再谈。
1.2 只读事务与 snapshot read
论文 §2.4.1:只读事务在 read version 上可串行,且可在客户端本地完成「提交」而无须走 Resolver / TLog。这解释了为何读扩展主要跟 Storage Server 数量相关。
Snapshot read(官方事务选项 /
API)刻意不把某些读计入冲突集合,从而放松隔离、减少冲突;换来的是与默认严格可串行化路径不同的语义。默认路径下,普通
get / range
读会计入读冲突范围——下一节展开。
二、冲突范围:Resolver 看见的不是「你读过的 value」
提交时客户端送给 Commit Proxy
的,是读冲突范围与写集合(论文
§2.4.1)。Resolver 检查:在
[read_version, commit_version)
窗口内,是否有已提交事务写过与本事务读集相交的 key
range(论文 Algorithm 1)。
因此:
- 读过的 key / range 会阻止与并发写冲突的提交(防幻读一类问题靠 range 冲突,而非值内容)。
- 写过的 key 进入写历史,供后续事务的读集比对。
- 读到的 value 本身不进入 affected data 的「值」部分(Known Limitations),但对应的 key / range 仍影响冲突与大小统计。
flowchart LR
subgraph client ["Client transaction"]
reads["Read conflict ranges"]
writes["Write set"]
end
subgraph commitPath ["Commit path"]
cp["Commit Proxy"]
res["Resolver<br/>OCC on recent writes"]
end
reads --> cp
writes --> cp
cp --> res
2.1 显式加减冲突范围
高级 API 允许 add_read_conflict_range /
add_write_conflict_range,以及用 snapshot
读从读冲突中剔除范围(Developer Guide / API
Reference)。典型用途:
- 读了大量仅用于决策、却不需要与并发写冲突的键时,改用 snapshot 读降低 abort。
- 需要保护「逻辑上读过但 API 未自动覆盖」的不变式时,手动补读冲突范围。
误用模式:为「提高成功率」盲目 snapshot 掉所有读——等于放弃默认隔离保证,bug 会表现为偶发不一致而非立即报错。
2.2 一个具体故事:库存扣减
假设两个事务都读 stock:item42,看到值为
1,各自减一后写回。在默认 OCC 下:
- 两者取得相近的 read version,都读到
1。 - 先提交者写入
0,其写进入 Resolver 历史。 - 后提交者的读冲突范围仍覆盖该 key,且读版本早于前者的
commit version → Resolver 判冲突 →
not_committed。 @transactional重试后,第二次读到0,业务决定失败或走另一分支。
这与悲观锁「先占坑」不同:冲突在提交时发现。低冲突 OLTP 下论文报告生产冲突率很低(Zhou et al., §2.4.2);高冲突工作负载会把重试放大变成主成本——第 9、17 篇再谈。
三、自动重试:哪些错误会回头,哪些不会
官方错误处理模型把一部分错误标为可重试:客户端应重置事务状态并重跑用户函数。常见与机制相关的错误(名称以 7.x 文档为准):
| 错误(概念) | 含义(机制层) | 典型客户端动作 |
|---|---|---|
not_committed |
Resolver 检出冲突 | 重试整笔事务 |
transaction_too_old |
超出 MVCC / 5 秒窗口等 | 重试;若逻辑本身过长需改设计 |
commit_unknown_result / 提交结果不确定 |
提交过程中发生恢复等,客户端不知是否已提交 | 重试;事务必须幂等(官方 Architecture 恢复节) |
stateDiagram-v2
[*] --> Running: create / reset transaction
Running --> Committing: commit()
Committing --> Succeeded: durable on logs
Committing --> Retry: not_committed / too_old / unknown
Committing --> Failed: non-retryable
Retry --> Running: on_error reset
Succeeded --> [*]
Failed --> [*]
工程间隙:论文与文档都强调幂等——因为
commit_unknown_result
时,原提交可能已成功,重试再成功一次必须可接受(Architecture:
Transaction System Recovery)。把「发邮件 /
扣款通知」放进事务函数且无幂等键,是经典事故源。
四、5 秒上限:先当产品契约,机制留后文
Known Limitations — Long running transactions:
当前版本不支持运行超过五秒的事务。自事务中第一次读起约 5 秒后:后续需访问数据库的读通常会遇到
transaction_too_old;带写的提交会遇到transaction_too_old或not_committed。
官方 Architecture 从存储侧补充同一窗口:Storage
Server 在内存中保留约 5 秒
mutation,磁盘侧保留更旧快照;客户端必须在近 5
秒版本上读,否则 transaction_too_old。Resolver
也只保留约 5 秒已提交写历史供 OCC。
本篇只要求读者记住三点:
- 5 秒是设计限制,不是「调大配置即可」的默认超时(Anti-Features / Known Limitations)。
- 它同时约束客户端事务寿命与服务器 MVCC / 冲突历史窗口——第 9 篇把它接到严格可串行化与恢复有界性。
- 需要长快照或外部交互时,应在 Layer 做版本化或拆成短事务(文档 Workarounds),而不是假设核心会放宽。
flowchart LR
t0["First read<br/>version V"]
t5["About 5s later"]
t0 --> window["MVCC + conflict history window"]
window --> t5
t5 --> err["transaction_too_old / not_committed"]
常见误解
- 「
@transactional保证 exactly-once 业务效果」。它保证的是在可重试错误下重跑函数直到成功或不可重试失败;与外部副作用合在一起时,需要应用级幂等。 - 「读很多 key 只要最终少写几个,就不会冲突」。冲突看读/写范围是否与并发提交相交,不是看「净变更」大小。
- 「把事务拖长到几十秒,只要最终提交就行」。超过约 5 秒窗口后,读与提交会按文档失败;长工作流必须拆开。
工程间隙
SIGMOD 2021 的目标负载是读多、每笔触摸 key 少、争用低(§2.3.2)。Client API 把复杂数据模型推到 Layer(Record Layer 等)。这意味着:API 简洁换来的是建模责任上移——二级索引、外键式约束要在 Layer 用额外 key 与同一事务维护(第 15 篇),而不是在 SQL 引擎里隐式完成。
五、版本锚定下的 API 行为清单
写业务代码时,把下面几条当成「与 7.x 文档对齐的检查表」,避免把其他数据库的事务习惯平移过来。
| 行为 | FoundationDB 默认 | 含义 |
|---|---|---|
| 写何时可见给集群 | commit 成功之后 |
之前只在客户端缓冲 |
| 事务内读己之写 | 支持(客户端合并) | 不等于已持久化 |
| 隔离默认 | 严格可串行化路径(普通读计入冲突) | snapshot 读需显式选择 |
| 冲突发现时机 | 提交时 OCC | 不是加锁等待 |
| 长事务 | 约 5 秒上限 | 超限预期失败,而非降级 isolation |
| 不确定提交 | 可能发生 | 函数必须幂等 |
flowchart TD
design["Model keys and conflict ranges"] --> code["Write transactional function"]
code --> idem["Ensure idempotent side effects"]
idem --> limit["Keep under size and 5s limits"]
limit --> run["Rely on automatic retry"]
run --> observe["Watch not_committed rate"]
observe -->|"high"| redesign["Narrow reads or re-shard keys"]
5.1 与「先修」文章怎么接
若你从 distributed/39 过来,那里的选型结论「要强事务 KV」在本篇落地为:强事务的操作面是这段 Client API,代价是冲突范围与时间窗。若你从 tikv-htap 过来,注意 Percolator 在存储侧持锁、FDB 在提交时检测——重试风暴的调优旋钮不在同一层。
六、小结
三句话小结
- FDB 事务在客户端缓冲写、在固定 read version 上读;提交时上传的是冲突范围与写集,而不是「再执行一遍 SQL」。
@transactional/on_error把 OCC 冲突与部分系统错误变成整函数重试,要求事务函数可重入且对不确定提交结果幂等。- 约 5 秒的事务 / MVCC 窗口是核心产品契约;本篇只预告,机制与严格可串行化证明见第 8–9 篇。
下一篇离开客户端,进入集群如何记住「谁在当 Sequencer / Proxy」:Coordinator 与集群配置。
参考资料
核心论文
- Zhou et al., FoundationDB: A Distributed Unbundled Transactional Key Value Store, SIGMOD 2021, §2.2–2.4(接口、缓冲写、OCC、只读事务)。
规范 / 文档
- FoundationDB Developer Guide — Transactions、Error handling(7.x)。
- FoundationDB Known Limitations — Large transactions、Long running transactions(7.x)。
- FoundationDB Architecture — Transaction Processing、commit_unknown_result 与幂等(7.x)。
- FoundationDB language binding API Reference(Python /
Java / Go 等,
@transactional/onError形态)。
站内
上一篇:Unbundled 全景
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【FoundationDB 内核】Resolver 与 OCC:冲突范围、窗口与自动重试
拆解 FoundationDB Resolver 如何用 read/write conflict ranges 做乐观并发控制:冲突窗口落在 read version 与 commit version 之间,客户端自动重试;并与 TiKV Percolator 快照隔离对照机制差异,不写无实测性能结论。
【FoundationDB 内核】严格可串行化与 5 秒限制:实时顺序与恢复有界性
说明 FoundationDB 如何用 Sequencer 版本把 OCC+MVCC 提升到严格可串行化,区分 serialization order 与实时顺序;论证 5 秒事务上限如何约束冲突窗口与恢复日志量,并点明高冲突下的开放问题。
【FoundationDB 内核】Storage Server:Shard、版本读取与读路径
说明 FoundationDB Storage Server 如何托管 shard、按版本服务读、从 TLog 异步拉日志并参与迁移;强调读路径直达 Storage、不经过 Commit Proxy 提交流水线,以及与 Data Distributor 的边界。
【FoundationDB 内核】Unbundled · OCC · TLog · Redwood · Simulation
补齐 FoundationDB 选型叙事与生产内核之间的一层:拆解 Proxy、Sequencer、Resolver、TLog、Storage Server 的 Unbundled 写路径,严格可串行化、Redwood 与确定性模拟,并以 Record Layer 和 TiKV/etcd 对照收束。