必须用Transaction包裹而非裸调Begin,因Transaction自动recover panic并回滚、确保事务与编排执行强绑定、避免混用db导致原子性失效,而Begin无兜底易致连接卡死或数据不一致。

业务流程编排中涉及数据库写操作时,必须用 Transaction 包裹,而不是裸调 Begin;事务内所有 DB 操作必须使用传入的 tx 实例,混用全局 db 会导致原子性失效。
为什么不能在编排函数里直接用 db.Begin()
手动管理 Begin + Commit + Rollback 在流程编排中极易出错:节点失败时可能漏回滚、panic 未被捕获导致连接卡死、跨 goroutine 传递 tx 易引发竞态。更关键的是,多数编排框架(如 Eino)本身不持有 DB 连接上下文,无法帮你自动清理事务状态。你写的每个步骤函数若自行 Begin,就等于把事务边界撕碎了——A 步骤提交了,B 步骤失败回滚,数据已不一致。
- 事务生命周期必须与单次编排执行强绑定,
Transaction的闭包机制天然满足这点 -
Transaction内部会 recover panic 并自动Rollback,而Begin不做任何兜底 - 连接池超时错误(
acquire connection timeout)往往就源于Begin后忘记Commit或Rollback
如何把 Transaction 嵌入业务编排链路
编排逻辑本身不处理事务,事务只作用于具体的数据写入步骤。你需要让「执行步骤」和「事务控制」解耦:编排层负责顺序/分支/重试,事务层负责原子性。典型做法是定义一个带事务参数的步骤类型:
type TxStep func(tx *gorm.DB) error
<p>func TransferStep(fromID, toID uint, amount int) TxStep {
return func(tx *gorm.DB) error {
if err := tx.Model(&user{}).Where("id = ?", fromID).Update("balance", gorm.Expr("balance - ? ", amount)).Error; err != nil {
return err
}
return tx.Model(&user{}).Where("id = ?", toID).Update("balance", gorm.Expr("balance + ? ", amount)).Error
}
}
- 所有步骤实现为
TxStep,不碰全局db,也不自己开事务 - 编排引擎执行时统一套一层
db.Transaction(...),把tx注入每个步骤 - 若某步骤返回非 nil 错误,整个闭包退出,
Transaction自动回滚
常见错误:在 Transaction 闭包里调用了 db.Create()
这是最隐蔽也最致命的问题。例如在 Transaction 回调里写了 db.Create(&log),这个 log 会立刻落库,不受事务控制——因为 db 是新连接或空闲连接,而 tx 才是当前事务专属句柄。现象是:转账成功但日志已写,或转账失败但日志残留。
立即学习“go语言免费学习笔记(深入)”;
- 务必检查每一行 DB 调用:开头是
db.还是tx. - GORM 的预加载(
Preload)、关联创建(Association)等方法,同样必须用tx实例调用 - 第三方库封装的 DAO 函数,如果内部硬编码用了全局
db,就必须重写为接受*gorm.DB参数
复杂编排下的事务边界注意事项
事务不能跨 HTTP 请求、不能跨 goroutine、不能嵌套——GORM 的 Transaction 不支持嵌套调用。如果你的编排包含异步通知(如发 MQ)、外部 API 调用(如调支付网关),这些操作必须放在事务外,并设计补偿逻辑。
- 事务只包裹「本地数据库变更」,像扣库存、改订单状态这类操作
- 发消息、写 ES、调第三方服务等,应在
Transaction成功提交后触发,否则需引入 Saga 模式 - 不要试图在
Transaction闭包里起 goroutine 并在里面操作tx,tx不是并发安全的
事务不是万能锁,它只解决单次请求内的数据库一致性。编排越长,越要警惕把不该放进去的操作塞进事务里——连接占用时间变长,死锁风险上升,超时错误更频繁。


















