GORM事务不是“写出来就行”,而是“用对模式才不丢数据”;新手应优先使用db.Transaction()闭包模式,它自动处理错误回滚、panic捕获与连接释放,避免手动Begin/Commit/Rollback导致的连接泄漏或脏数据。

直接说结论:GORM 事务不是“写出来就行”,而是“用对模式才不丢数据”。新手最容易栽在 db.Create 和 db.Model().Update 连着写却没包事务——看着都成功了,其实库存扣少了、订单多生成了,线上查账时才发现。
用 db.Transaction() 闭包是最安全的起步方式
这是 GORM 官方推荐、也是新手最不容易出错的方式。它自动处理 panic 回滚、错误传播、连接释放,不用手动管 Commit 或 Rollback。
常见错误现象:手写两个 Create 操作,第二个失败了,第一个却已落库;或者中间 panic 导致事务悬挂,连接卡住。
- 必须把所有数据库操作写在闭包函数里,不能在外部调用
tx后再传出去 - 只要任意一步返回非
nil错误,整个事务自动回滚 - 闭包内发生的 panic 会被捕获并转为错误,不会导致连接泄漏(GORM 1.20+ 特性)
示例:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&Order{UserID: 1, Amount: 99}).Error; err != nil {
return err // 立即回滚
}
if err := tx.Model(&Product{}).Where("id = ?", 101).Update("stock", gorm.Expr("stock - 1")).Error; err != nil {
return err // 立即回滚
}
return nil // 显式返回 nil 才会提交
})
别在 Gin 中间件或 Handler 里直接用 db.Begin()
手动事务(Begin/Commit/Rollback)看起来更“可控”,但对新手极其危险。尤其在 Gin 的 HTTP 请求上下文中,容易漏掉 Rollback、忘记 defer、panic 没 recover,最终导致连接池耗尽或脏数据残留。
使用场景:仅当你需要跨多个函数传递事务对象、或要嵌套保存点(SavePoint)时才考虑手动控制。
- 必须配
defer func() { if r := recover(); r != nil { tx.Rollback() } }() - 每个
if err != nil分支后都要显式tx.Rollback()并 return - 最后必须调用
tx.Commit(),且要检查其返回的error
一句话:除非你清楚自己在绕过什么,否则别碰 Begin。
嵌套事务不是“事务套事务”,而是靠 SavePoint 实现局部回滚
新手常误以为 db.Transaction() 套一层就是嵌套事务——其实 GORM 默认不支持真正嵌套(即外层事务不感知内层 rollback),所谓“嵌套”是通过保存点模拟的。
典型用途:主流程不能失败,但某个子操作(如发通知、记日志)失败时只回滚它自己,不影响主事务。
- 要用
tx.SavePoint("sp1")创建保存点,再用tx.RollbackTo("sp1") - 保存点名必须唯一,且不能在不同事务间复用
- 外层事务 commit 后,所有保存点自动失效;rollback 则连带所有保存点一起回滚
示例片段:
err := db.Transaction(func(tx *gorm.DB) error {
tx.Create(&Order{...}) // 主操作
<pre class="brush:php;toolbar:false;">if err := tx.SavePoint("notify"); err != nil {
return err
}
if err := sendNotification(); err != nil {
tx.RollbackTo("notify") // 只回滚通知部分
}
return nil})
事务粒度要匹配业务边界,不是越小越好也不是越大越好
一个 HTTP 接口里开一个事务,看似“请求即事务”,但实际可能埋雷:比如事务里混入了 HTTP 调用、文件 IO、或长时间等待,会导致数据库连接长时间占用、锁等待加剧、超时风险上升。
容易被忽略的关键点:
- 事务里只放真正需要原子性的数据库操作,其他逻辑(如参数校验、缓存更新、第三方调用)移到事务外
- 如果事务内要更新多张表,确认它们是否真的需要强一致性;有时用最终一致性 + 补偿任务更稳
- GORM 默认隔离级别是数据库默认值(MySQL 是
REPEATABLE READ),高并发扣库存等场景建议显式设为READ COMMITTED
真正难的从来不是“怎么写事务”,而是“哪几行代码必须捆在一起不可分割”——这得从领域模型和业务语义里抠,不是看 ORM 文档能解决的。


















