事务回滚失败主因是事务对象被提前释放或重复调用;必须用db.Transaction()而非手动Begin/Commit;所有操作须使用同一tx对象,避免混用db与tx、多次提交/回滚、协程共享及链式调用生成新实例。

事务回滚失败的典型表现
执行 tx.Rollback() 后数据仍写入数据库,或抛出 sql: transaction has already been committed or rolled back 错误——这说明事务对象被提前释放、重复调用,或没在正确作用域内捕获异常。
必须用 db.Transaction() 启动事务
GORM 不支持手动 Begin() + Commit() 的裸 SQL 风格事务管理(除非你显式调用 db.Session(&session).Begin())。推荐方式是直接使用 db.Transaction() 函数,它自动处理提交/回滚,并保证 panic 或 error 时回滚。
常见错误:自己调用 db.Begin(),再传给后续操作,但忘记在 defer 中判断是否已提交,导致回滚失效。
正确做法:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err // 自动触发 rollback
}
if err := tx.Create(&profile).Error; err != nil {
return err // 自动触发 rollback
}
return nil // 自动 commit
})
if err != nil {
// 这里拿到的是事务内任一操作的 error
}
手动控制事务时,tx 必须贯穿所有操作
如果你确实需要手动控制(比如想在回滚前记录日志、或做条件性提交),必须确保所有数据库操作都使用同一个 tx 对象,且只调用一次 Commit() 或 Rollback()。
- 不要混用
db和tx:例如db.Create()是全局连接,不会参与事务;必须用tx.Create() - 不要多次调用
tx.Commit()或tx.Rollback():第二次会 panic - 避免在 goroutine 中使用
tx:GORM 的*gorm.DB不是并发安全的,跨协程操作事务会导致未定义行为
嵌套事务不真正存在,需靠 SavePoint
GORM 没有真正的嵌套事务支持。所谓“嵌套”,其实是通过 SavePoint 实现局部回滚。如果外层事务已 rollback,内层 savepoint 也无效。
用法示例:
tx := db.Begin()
tx.SavePoint("sp1")
tx.Create(&order)
tx.RollbackTo("sp1") // 回滚到 sp1,order 被撤销
tx.Commit()
注意:SavePoint 名称要唯一,且不能在已 rollback 的事务上调用 RollbackTo,否则报错 invalid transaction state。
*gorm.DB 是只读副本,每次链式调用(如 Where()、Session())都会生成新实例。你在事务中用了 db.Where().Create(),其实根本没走事务——务必确认每一步操作的对象是同一个 tx。


















