@transactional 自动重试,以及 5 秒上限如何约束应用设计——机制证明留给第 8–9 篇。" />

土法炼钢兴趣小组的算法知识备份

【FoundationDB 内核】Client API 与事务模型:冲突范围、自动重试与 5 秒窗口

文章导航

分类入口
databasestorage
标签入口
#foundationdb#client-api#transaction#conflict-range#occ#retry#mvcc

目录

业务代码里最常见的 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 LimitationsAPI 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)。

因此:

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 掉所有读——等于放弃默认隔离保证,bug 会表现为偶发不一致而非立即报错。

2.2 一个具体故事:库存扣减

假设两个事务都读 stock:item42,看到值为 1,各自减一后写回。在默认 OCC 下:

  1. 两者取得相近的 read version,都读到 1
  2. 先提交者写入 0,其写进入 Resolver 历史。
  3. 后提交者的读冲突范围仍覆盖该 key,且读版本早于前者的 commit version → Resolver 判冲突 → not_committed
  4. @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_oldnot_committed

官方 Architecture 从存储侧补充同一窗口:Storage Server 在内存中保留约 5 秒 mutation,磁盘侧保留更旧快照;客户端必须在近 5 秒版本上读,否则 transaction_too_old。Resolver 也只保留约 5 秒已提交写历史供 OCC。

本篇只要求读者记住三点:

  1. 5 秒是设计限制,不是「调大配置即可」的默认超时(Anti-Features / Known Limitations)。
  2. 它同时约束客户端事务寿命服务器 MVCC / 冲突历史窗口——第 9 篇把它接到严格可串行化与恢复有界性。
  3. 需要长快照或外部交互时,应在 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"]

常见误解

  1. @transactional 保证 exactly-once 业务效果」。它保证的是在可重试错误下重跑函数直到成功或不可重试失败;与外部副作用合在一起时,需要应用级幂等。
  2. 「读很多 key 只要最终少写几个,就不会冲突」。冲突看读/写范围是否与并发提交相交,不是看「净变更」大小。
  3. 「把事务拖长到几十秒,只要最终提交就行」。超过约 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 在提交时检测——重试风暴的调优旋钮不在同一层。


六、小结

三句话小结

  1. FDB 事务在客户端缓冲写、在固定 read version 上读;提交时上传的是冲突范围与写集,而不是「再执行一遍 SQL」。
  2. @transactional / on_error 把 OCC 冲突与部分系统错误变成整函数重试,要求事务函数可重入且对不确定提交结果幂等。
  3. 约 5 秒的事务 / MVCC 窗口是核心产品契约;本篇只预告,机制与严格可串行化证明见第 8–9 篇。

下一篇离开客户端,进入集群如何记住「谁在当 Sequencer / Proxy」:Coordinator 与集群配置


参考资料

核心论文

  1. Zhou et al., FoundationDB: A Distributed Unbundled Transactional Key Value Store, SIGMOD 2021, §2.2–2.4(接口、缓冲写、OCC、只读事务)。

规范 / 文档

  1. FoundationDB Developer Guide — Transactions、Error handling(7.x)。
  2. FoundationDB Known Limitations — Large transactions、Long running transactions(7.x)。
  3. FoundationDB Architecture — Transaction Processing、commit_unknown_result 与幂等(7.x)。
  4. FoundationDB language binding API Reference(Python / Java / Go 等,@transactional / onError 形态)。

站内

  1. 第 1 篇 Unbundled 全景
  2. 本系列 index

上一篇Unbundled 全景

下一篇Coordinator 与集群配置

返回 系列目录 · 数据库索引

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .