95%业务场景应优先使用db.Transaction()闭包而非db.Begin(),因其自动处理panic回滚、成功提交及连接归还;手动Begin易漏写Commit/Rollback导致连接卡死或数据部分落库。

db.Transaction 闭包是默认安全选择
95% 的业务场景该用 db.Transaction(),而不是裸调 db.Begin()。前者自动处理 panic 回滚、成功提交、连接归还;后者是底层原语,漏写 tx.Commit() 或 tx.Rollback() 就会导致连接卡死、数据部分落库。
常见错误现象:acquire connection timeout、事务里某条 db.Create() 提前写入数据库但后续失败、查库发现“订单建了库存没扣”或“库存扣了订单没建”——基本都是混用了 db 和 tx,或者手动 Begin 后忘了收尾。
-
db.Transaction(func(tx *gorm.DB) error { ... })内部会捕获 panic,并在闭包返回非nilerror 时自动Rollback,成功则自动Commit - 闭包参数传入的
tx是唯一合法操作句柄,所有 DB 调用(tx.Create()、tx.Where().Update()、tx.Exec())必须走它 - 不要在闭包里调
tx.Commit()或tx.Rollback()—— GORM 已接管生命周期
所有操作必须用 tx 实例,禁止混用全局 db
混用 db 和 tx 是静默破坏原子性的最常见原因。比如在 db.Transaction 闭包里写 db.Model(&Product{}).Update(...),这条语句会从连接池取新连接、立刻提交,完全脱离当前事务上下文。
现象是:事务最终回滚了,但那条 db.Update() 已不可逆写入;或者事务提交了,但部分数据却没进库——状态撕裂,对账时根本无法还原。
- DAO 层函数若被复用,必须把
*gorm.DB作为参数传入,而不是闭包内直接引用包级变量db - 链式调用中容易忽略:比如
tx.Preload("Items").Create(&order)是 OK 的,但db.Preload("Items").Create(&order)就危险 - 原生 SQL 也得走
tx.Raw().Scan()或tx.Exec(),不能用db.Raw()
嵌套调用本质是 savepoint,不是新事务
在 db.Transaction 闭包里再调一次 db.Transaction,GORM 不会开启新事务,而是创建 SAVEPOINT(MySQL/PostgreSQL 均支持)。标准库禁止在已有 *sql.Tx 上重复 Begin(),否则直接 panic:"sql: transaction already in progress"。
所以所谓“嵌套”,只是局部回滚点管理,不是独立生命周期。想实现部分回滚(比如扣库存失败但订单日志仍要记),应该用 tx.SavePoint("sp_stock") + tx.RollbackTo("sp_stock"),而不是套娃闭包。
- savepoint 名称建议固定可读,如
"sp_charge",别用随机字符串,否则调试时无法定位 - 注册
tx.AfterCommit()钩子必须在闭包内完成,且只对当前tx生效;若事务回滚,钩子不会执行 - HTTP 请求、Redis 写缓存、发 MQ 消息等外部调用,必须挪到
tx.AfterCommit(func(){})中,不能放在事务体里
事务体里不能放阻塞或超时风险操作
任何可能超时、阻塞或失败的外部调用放进事务体,轻则拖慢事务,重则耗尽连接池、引发服务雪崩。GORM 的 tx.AfterCommit() 是唯一安全出口,但它只对当前 tx 生效,且必须在闭包内注册。
注意:tx.AfterCommit() 注册后若事务最终回滚,钩子不会执行;但若事务提交后钩子 panic,不会影响数据库状态,需自行兜底(比如加日志、重试队列)。
- 错误示例:
http.Post(...)、rdb.Set(...)、os.WriteFile(...)直接写在闭包里 - 正确做法:统一移到
tx.AfterCommit(func(){ notifyOrderCreated(order.ID); cacheOrder(order.ID) }) - 如果回调逻辑复杂或需跨函数复用,可用
defer+ 状态标记模拟,但必须显式判断committed标志位,不能无条件执行
tx、什么时候该切出去、哪里必须加 context.WithTimeout、savepoint 名称要不要带业务语义——这些细节不写进日志、不暴露在监控里,但线上出事时,全靠它们决定能不能快速定位。


















