绝大多数业务写操作必须用 db.transaction,因其自动捕获panic、根据error智能commit/rollback并确保连接安全释放;直接defer tx.rollback()配合begin不可靠,因commit后调用会报错、panic未recover则崩溃、且无法保证事务原子性。

绝大多数业务写操作必须用 db.Transaction,而不是 db.Begin();手动管理事务只在 SavePoint 或跨函数传递时才需要。
为什么不能直接用 defer tx.Rollback() 配合 Begin
这是新手最常踩的坑:写成 tx := db.Begin(); defer tx.Rollback(),看起来“自动回滚”了,但实际完全不可靠。因为 defer 在函数退出时才执行,而事务可能早已 Commit(),也可能在中间 panic 了却没被捕获——结果就是该回滚的没回滚,连接还卡在池里不释放。
-
tx.Commit()成功后,再执行tx.Rollback()会报错sql: transaction has already been committed or rolled back - panic 发生在
Commit()前,defer确实会触发,但你没做recover,程序直接崩溃,日志都来不及打 - GORM 的事务句柄不是线程安全的,多个 goroutine 并发用同一个
tx实例会导致状态混乱
db.Transaction 是默认安全方案
db.Transaction 内部封装了 Begin → 执行闭包 → 捕获 panic → 根据返回 error 自动 Commit 或 Rollback。你只要保证闭包内所有 DB 操作都用传入的 tx,而不是全局 db。
- 错误返回即回滚:
return errors.New("库存不足")会立刻触发回滚,不用写tx.Rollback() - panic 也被捕获:哪怕闭包里有空指针或除零,事务也会安全回滚,不会泄露连接
- 所有操作必须用
tx.Create()、tx.Model().Update()等,混用db.Create()会导致部分数据提前落库,破坏原子性
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&Product{}).Where("id = ? AND stock >= ?", pid, num).
Update("stock", gorm.Expr("stock - ?", num)).Error; err != nil {
return err // 自动回滚
}
if err := tx.Create(&Order{ProductID: pid, Num: num}).Error; err != nil {
return err // 自动回滚
}
return nil // 自动提交
})
什么时候必须用 Begin + SavePoint
只有两类场景绕不开 Begin():需要跨多个函数传递事务句柄(比如 service 层拆成几个函数),或要实现“局部回滚”——也就是保存点(SavePoint)。
-
tx.SavePoint("sp1")后的操作失败,可tx.RollbackTo("sp1"),外层事务仍可继续提交 - 保存点名不能重复,否则后一次
SavePoint会覆盖前一次,导致RollbackTo跳过中间状态 - MySQL 中在保存点内执行 DDL(如
CREATE TABLE)会隐式提交,整个事务原子性就破了,这类操作必须移出事务体
事务里最容易被忽略的并发陷阱
死锁不是小概率事件,尤其在订单+库存这种高频更新组合里。GORM 本身不解决加锁顺序问题,得靠你设计。
- 多表更新必须约定固定顺序:比如总是先
SELECT ... FOR UPDATE锁orders表,再锁inventory表,避免 A 锁 order 后等 inventory,B 反过来 -
FOR UPDATE的 WHERE 条件必须命中索引,否则 MySQL 会升级为表锁,放大冲突面 - 事务粒度要小:日志记录、消息通知、HTTP 调用这些非数据库操作,一律移出
Transaction闭包,否则锁持有时间拉长,死锁概率指数上升
真正难的从来不是写对 Transaction 这一行代码,而是想清楚哪些操作必须捆在一起、哪些可以松耦合、哪些根本不该进事务——这决定了你的系统是稳定还是半夜被报警叫醒。











