事务必须用 tx 实例,不能混用 db;GORM 事务靠实例隔离,需全程使用 Begin 返回的 tx 句柄,闭包模式适合简单原子操作,复杂场景须手动控制 SavePoint、超时等。

事务必须用 tx 实例,不能混用 db
常见错误是开启事务后,还在里面调用原始 db 实例(比如 db.Create()),这会导致操作脱离事务控制,回滚时完全无效。GORM 的事务不是“全局开关”,而是靠实例隔离:一旦调用 db.Begin(),后续所有数据库操作必须使用返回的 tx *gorm.DB 句柄。
- 正确写法:
tx.Create(&order)、tx.Model(&user).Update("balance", 100) - 错误写法:
db.Create(&order)或在事务回调里又 new 一个db - 嵌套调用时也要透传
tx,不能让子函数偷偷用回全局db - HTTP handler 中尤其容易踩坑:别在中间件里开事务,然后业务函数里直接用
db—— 必须把tx通过参数或 context 传下去
db.Transaction() 闭包模式更适合简单场景
这个 API 是 GORM 提供的“自动管理”封装,内部帮你处理了 Begin/Commit/Rollback 和 panic 捕获,适合单入口、逻辑清晰的业务,比如下单、转账这类原子性明确的操作。
- 它会在回调返回
nil时自动Commit,返回非 nil error 时自动Rollback - 1.20+ 版本会捕获回调里的 panic 并转为 error,避免事务悬挂
- 但不支持手动控制回滚点(比如部分失败还想继续),也不方便做超时或重试——这些得自己用
Begin管理 - 闭包内无法提前退出(如
return后还想做清理),因为控制权交给了 GORM
复杂业务必须手动控制 Begin/Commit/Rollback
当需要条件判断、多次回滚尝试、保存点(SavePoint)、或与其他资源(如消息队列、缓存)联动时,闭包模式不够用,必须显式调用 Begin。
- 记得用
defer+recover()处理 panic,否则可能漏掉Rollback - 错误分支必须显式
tx.Rollback()并return,不能只写tx.Rollback()就往下走 - 长事务要设超时:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),再用db.Session(&gorm.Session{Context: ctx})包装tx - 保存点用
tx.SavePoint("sp1")和tx.RollbackTo("sp1"),适用于“主流程成功,子流程可选失败”的场景(如创建订单成功,发通知失败但不阻断)
事务粒度别过大,也别过小
事务不是越长越好,也不是每个 Create 都要套一层。粒度设计直接影响锁竞争、超时率和并发吞吐。
- 避免跨 HTTP 请求、跨 goroutine 共享一个
tx——tx不是线程安全的 - 不要把日志写入、HTTP 调用、文件操作塞进事务里,它们不参与 ACID,只会拖慢事务
- 读多写少场景下,考虑用
Session(&gorm.Session{SkipDefaultTransaction: true})关掉默认事务,查列表这种操作没必要上锁 - 高并发扣库存这类操作,事务内只做必要 DB 更新,别带复杂计算或循环;否则锁持有时间不可控
事务真正难的不是语法,是判断哪几行 DB 操作必须“捆在一起”,以及什么时候该让它们各自独立。很多线上问题,根源不在 tx.Commit() 写没写对,而在业务逻辑拆分时,把不该放一起的事硬塞进了同一个事务。











