← [ BACK_TO_NOTES ]

BodySense 数据库第 11 课:Transaction Boundary 与 ACID——从 Go Service 到 Commit

以 BodySense TransactionManager 和跨 Repository 业务动作学习 ACID、autocommit、commit/rollback、事务边界与 side effect 边界。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

BodySense 数据库第 11 课:Transaction Boundary 与 ACID——从 Go Service 到 Commit

[!abstract] 本课目标 会说 ACID 不够;必须能在真实业务代码里指出事务从哪里开始、哪些 SQL 共享它、什么时候 commit,以及哪些副作用不在数据库事务里。

从零建立概念:Transaction 不是“多条 SQL 放一起”这么简单

Transaction 可以理解成:

数据库对一组读写提供的一个一致状态转换边界。

假设 BodySense 做一个操作:

create new TreatmentRevision
update Treatment.current_revision_id
write audit/decision trace

如果只完成第一步:

revision exists
but current pointer still old

数据库可能进入业务上不允许的中间状态。

所以我们希望:

before state
↓
all required DB changes
↓
commit
↓
after state

要么:

after state exists completely

要么:

none of these changes become committed

Autocommit:即使你没写 BEGIN,也不是“没有 Transaction”

PostgreSQL 中,显式 transaction:

BEGIN;
UPDATE ...;
INSERT ...;
COMMIT;

如果不显式 BEGIN:

UPDATE users SET ...;

这条 statement 仍然运行在自己的单语句 transaction 中。

所以:

no explicit transaction block
≠
database has no transaction semantics

区别是:

多条 statement 是否共享同一个 transaction boundary。

BEGIN / COMMIT / ROLLBACK

BEGIN

开始显式 transaction。

COMMIT

告诉数据库:

我希望这个 transaction 的修改正式成为可见的 committed state。

Commit 也可能失败。

所以 application 不应在 Commit() 返回 error 后仍假设结果成功。

ROLLBACK

放弃这个 transaction 尚未 commit 的修改。

Rollback 不是“撤销任何外部世界副作用”。

它只作用在这个数据库 transaction 范围内。

ACID 从零讲

A — Atomicity

transaction changes
→ commit as a unit
or
→ rollback / abort

对外不应该只提交其中一半。

注意:

Atomicity 是 transaction 的提交边界,不代表“代码永远不会抛错”。

C — Consistency

最容易被讲错的一项。

Consistency 不是:

PostgreSQL 自动知道所有业务规则。

更准确:

Transaction 从一个满足数据库/业务 invariant 的状态出发,在正确的 application logic + constraints 下,应该到达另一个满足 invariant 的状态。

数据库能直接帮助的:

PK
FK
UNIQUE
CHECK
NOT NULL
transaction

业务没有表达给数据库的规则,DB 不会读心。

I — Isolation

多个 transaction 同时执行时:

彼此能看见什么?
冲突时等待还是失败?
结果能否等价成某种串行顺序?

下一课专门展开。

D — Durability

成功 Commit 后:

即使进程/机器随后 crash,数据库也应能根据持久化机制恢复已提交状态。

PostgreSQL 主要依赖 WAL 等机制,第 16 课展开。

Transaction Scope 应该由业务 Invariant 决定

错误思路:

一个函数
→ 一个 Transaction

或:

每个 Repository method
→ 各开自己的 Transaction

正确问法:

哪些数据库变化必须一起成立,业务 invariant 才不被破坏?

例如:

Create User
单表 insert
→ 单 statement transaction 也许足够

另一个:

Accept Treatment
├─ mark proposal accepted
├─ create immutable revision
├─ move current pointer
└─ write transition record

可能需要更大的 Application transaction。

Why Service Owns Transaction Boundary

Repository 通常只知道:

怎样读/写自己的数据

Service / Use Case 才知道:

这次业务动作包含哪些 Repository
哪些变化必须一起 commit

因此 BodySense:

TransactionManager.WithinTransaction
→ Service establishes tx

Repository
→ FromContext joins current tx

是一种很清晰的 ownership。

一个非常危险的错误:Transaction 内外混用 DB Handle

如果:

tx 已开始
Repository A uses tx
Repository B accidentally uses root db

Repository B 的 SQL 可能在另一个 connection / transaction执行。

结果:

A rollback
B already committed

这就是 FromContext(ctx, fallback) 设计要系统性避免的问题。

Go 官方 database/sql 也明确提醒:transaction 中不要混用普通 sql.DB 方法来完成本应在 Tx 内的操作。

External Side Effect 与 Transaction Boundary

最常见陷阱:

BEGIN
INSERT order
call payment API
COMMIT

情况:

payment API success
COMMIT fails

数据库可以 rollback INSERT,

但:

credit card charge

不会因为 PostgreSQL rollback 自动消失。

同理:

send email
publish Kafka
write Redis
upload object
call LLM/tool

都不是本地 PostgreSQL ACID transaction 的一部分。

跨系统一致性的几个方向

Outbox

DB business write
+ outbox event
→ same DB transaction
→ relay later publishes

Idempotency

重复 side effect 请求使用同一 business identity,避免真正重复执行。

Saga / Compensation

多个独立系统的步骤分别 commit,失败后执行补偿动作,而不是假装有一个巨大的跨系统 rollback。

这些不是本课全部展开,但必须先知道:

本地 ACID 的边界在哪里,才知道什么时候需要分布式一致性协议。

Transaction 越大越安全吗?

不是。

Transaction 过长会:

hold connection longer
hold locks longer
keep snapshot alive
increase contention
hurt VACUUM
increase deadlock window

所以目标是:

足够大以保护 invariant,又尽量小以降低并发和资源成本。

1. Transaction 是状态转换边界

例如一个业务动作:

创建 Treatment revision
→ 更新 current pointer
→ 写 DecisionTrace

如果这些事实必须同时成立或同时不成立,它们需要共享一个 transaction boundary。

2. ACID 重新放回工程语境

Atomicity   → 一组 DB change commit or rollback
Consistency → transaction 前后数据库 invariant 成立
Isolation   → 并发事务的可见性/冲突语义
Durability  → commit 后 crash 仍可恢复

Consistency 不是数据库自动理解业务;它依赖 constraint、transaction logic 和并发策略。

3. BodySense TransactionManager

Service
→ WithinTransaction(ctx, fn)
→ tx 放入 context
→ Repository FromContext(ctx, fallback)
→ 多个 repository 共享同一个 *gorm.DB tx

这个设计把 application-level transaction ownership 与 repository SQL ownership 分开。

4. External Side Effect 不会自动加入 ACID

事务里:

INSERT DB
→ call external HTTP
→ COMMIT

如果外部调用成功但 COMMIT 失败,数据库 rollback 并不会让外部世界自动 rollback。跨系统一致性需要 Outbox/Saga/idempotency 等另外的协议。

Prediction

[!question]

  1. 两个 repository 方法分别成功 commit,但业务要求要么都成功要么都失败,问题在哪?
  2. 事务里先写 DB,再发邮件,邮件成功后 DB commit 失败,ACID 能回滚邮件吗?
  3. 所有 service 方法都套 transaction 是否更安全?
  4. Transaction 很长但几乎不写数据,为什么仍可能有代价?
  5. BodySense 的 WithinTransaction + FromContext 主要解决什么?

[!success]- Answer / 解析 1. Transaction boundary 放错了;它应该包住业务原子动作,而不是各 repository 自行 commit。

2. 不能。 ACID 只覆盖该数据库事务,外部 side effect 需要独立一致性协议。

3. 不一定。 不需要原子组合的单查询套长事务只会增加连接占用、snapshot/lock 时间和复杂度。

4. 它持续占用连接和 snapshot,可能延迟 vacuum 清理旧 tuple,也可能持有锁。

5. 让 Application Service 能建立一个事务并让多个 repository 自动参与同一个 tx,而不是各自提交。

Lab

Trace 一个真实 BodySense Service:标出 transaction begin、每个 repository call、SQL mutation、commit/rollback。再列出函数里任何不受 DB transaction 保护的 side effect。

L4 Gate

给定一个新业务流程,能先写 invariant,再据此决定 transaction boundary,而不是按函数层级机械加 .Transaction()。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本