db.Transaction()是最安全的事务入口,官方推荐且自动管理生命周期:成功返回nil则提交,返回非nil错误则回滚,所有操作必须使用参数tx而非原始db。

db.Transaction() 是最安全的事务入口
直接用 db.Transaction() 包裹操作,是 GORM 官方推荐且最不容易出错的方式。它自动管理事务生命周期:成功返回 nil 则提交,任意位置返回非 nil 错误则回滚,不用手动调 Commit() 或 Rollback()。
常见错误是还在用 db 执行操作,而不是传入的 tx 参数 —— 这会导致操作脱离事务上下文,看似成功实则没进事务。
- 必须全程使用函数参数
tx *gorm.DB,不能混用全局db - 所有数据库操作(
Create、Save、Updates等)都得在闭包内完成 - 闭包内不能 defer 任何依赖事务状态的操作(比如发 MQ 消息),否则可能在回滚后才触发
- 如果需要提前退出并回滚,直接
return err即可,别自己调tx.Rollback()
db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&User{}).Where("id = ?", fromID).Update("balance", gorm.Expr("balance - ?"), amount).Error; err != nil {
return err // 自动回滚
}
if err := tx.Model(&User{}).Where("id = ?", toID).Update("balance", gorm.Expr("balance + ?"), amount).Error; err != nil {
return err // 自动回滚
}
return nil // 自动提交
})手动 Begin/Commit/Rollback 容易漏掉错误分支
用 db.Begin() 启动事务后,必须确保每个可能出错的路径都显式处理回滚,否则事务会一直挂起或意外提交。这是线上事故高发点。
典型坑是只在某个 if err != nil 分支里调 Rollback(),但忘了在后续操作失败时也回滚;或者 Commit() 失败后没再补一次 Rollback()。
-
tx.Commit().Error可能非空(比如锁超时、连接断开),此时必须立刻tx.Rollback() - 所有
tx.Create()、tx.First()等调用后都要检查.Error,不能只看返回值是否为 nil - 不要在事务块外 defer
tx.Rollback()—— 成功提交后 rollback 会 panic - 事务对象
tx不支持并发复用,每个 goroutine 必须独立 begin
嵌套事务不等于“子事务”,本质是 SavePoint
GORM 的 tx.Begin() 在已有事务中调用,并不会开启真正隔离的新事务,而是创建一个保存点(SavePoint)。回滚只影响该保存点之后的操作,不影响外层已执行内容。
这意味着:外层事务失败时,所有保存点都会被一并回滚;但内层主动 RollbackTo() 不会影响外层状态。它适合局部补偿,不是真正的 ACID 嵌套事务。
- 内层
tx.Rollback()实际调的是RollbackTo("savepoint_xxx"),不是终止整个事务 - 外层
Commit()成功,不代表内层 SavePoint 已提交 —— 它只是被合并了 - MySQL 5.7+ 支持 SavePoint,但 SQLite 和某些云数据库可能限制数量或不支持
- 避免多层嵌套,三层以上极易逻辑混乱,优先拆成独立事务或业务校验
SkipDefaultTransaction 要按需关闭,别盲目追求性能
初始化 GORM 时设 SkipDefaultTransaction: true,会让单条 Create/Update 不再自动包裹事务 —— 性能确实提升约 30%,但代价是单操作也不再具备原子性保障。
这在日志写入、缓存更新等弱一致性场景可行,但在用户余额、订单状态等强一致场景下,关掉等于放弃最基本的数据保护。
- 关掉后,
db.Create()失败只会返回 error,不会自动回滚(本来也没事务) - 即使开了
SkipDefaultTransaction,db.Transaction()依然完全可用,两者不冲突 - 高频小写入(如埋点、统计计数)可关;涉及资金、库存、状态流转的,必须保留默认事务
- 别因为文档写了“性能提升”就全局关闭 —— 先压测,再根据实际慢 SQL 和错误率决策
事务不是银弹,但它是数据一致性的底线。GORM 提供了足够清晰的接口,关键在于每次写操作前,想清楚:这个动作能不能接受中间态?失败了有没有补偿手段?没有的话,老老实实用 db.Transaction() 就对了。


















