transaction闭包内禁用全局db,因其脱离事务上下文导致原子性破坏;必须统一使用参数tx实例,且95%业务应优先选db.transaction而非begin。

Transaction 闭包里为什么不能用全局 db
事务原子性会被直接破坏,且问题极难排查。只要在 db.Transaction 闭包中混用 db.Create() 这类全局实例调用,那一行就脱离事务上下文,立刻写入数据库——哪怕后续 tx.Create() 全部失败并回滚,这行也已经落库。
必须统一使用闭包参数传入的 tx 实例,它是绑定当前事务连接的专属句柄。常见错误包括:
- 日志记录误用
db.Create(&Log{})而非tx.Create(&Log{}) - 调用其他 service 方法时,该方法内部又用了全局
db - 忘记把
tx作为参数逐层透传,导致下层函数“不知情”地走默认连接
Begin 和 Transaction 到底该选哪个
95% 的业务场景应该无条件选 db.Transaction(),而不是 db.Begin()。前者是安全封装,后者是裸操作接口。
db.Transaction() 自动处理:
– 正常返回 → Commit()
– 返回非 nil error → Rollback()
– 闭包内 panic → recover 后自动 Rollback()
db.Begin() 只适合两种明确需求:
- 需要跨 handler → service → repo 多层传递同一事务对象(此时你得自己确保每层都用
tx,且最终有且仅有一次Commit()或Rollback()) - 要手动管理 savepoint,比如 A 操作失败不影响 B,但 B 失败需回滚到 A 之后(这时要用
tx.SavePoint("a")+tx.RollbackTo("a"))
硬套 Begin() 写业务逻辑,漏 Commit() 会导致连接池耗尽;漏 Rollback() 会让部分数据提前落库。
嵌套事务是不是真能“局部回滚”
GORM 不支持语义上的嵌套事务。所谓 inner := outer.Begin() 实际只是创建 savepoint,不是独立事务。它不提供跨事务的数据可见性隔离,也不允许 inner commit 后 outer rollback 时忽略 inner 的效果。
如果你看到类似“扣库存成功、发消息失败,只回滚发消息”的需求,这不是嵌套事务能解决的——这是业务设计问题。正确做法是:
- 把发消息改为异步补偿:本地先插入
message_task表,再由定时任务重试 - 若强依赖外部服务响应,应将整个流程(含调用结果)视为一个原子单元,失败则全部回滚
- 真要用 savepoint,必须显式
tx.SavePoint("sp1"),出错时tx.RollbackTo("sp1"),且注意外层Rollback()会清除所有 savepoint
事务里执行 delete 和 truncate 的区别
DELETE 支持回滚,TRUNCATE 不支持——这是 MySQL 层面的限制,GORM 无法绕过。
在事务中误用 tx.Exec("TRUNCATE table_name"),一旦执行就不可逆,后续 Rollback() 完全无效。更隐蔽的风险是:
-
TRUNCATE是 DDL 操作,会隐式提交当前事务(MySQL 默认行为) - 如果之前已有修改未提交,
TRUNCATE会把它们一起刷出去 - 并发场景下,另一个连接往表里插数据,你的事务回滚也无法撤回那些新数据
需要清空表且保留事务控制能力,只能用 tx.Where("1 = 1").Delete(&Model{}),并确认 WHERE 条件足够安全。











