最常见原因是未在事务上下文中使用tx对象操作数据库,gorm中db.create()等默认走全局db,必须全部改用函数参数传入的tx,否则操作绕过事务直接提交。

事务回滚不生效,数据还是写进去了
最常见原因是没在事务上下文中用 tx 对象操作数据库。GORM 的 db.Create()、db.Save() 这些方法默认走的是全局 db,哪怕你在 db.Transaction() 里调用,只要没显式传入 tx,就根本不在事务里。
必须确保所有增删改操作都使用函数参数传入的 tx,而不是包级变量 db。比如:
db.Transaction(func(tx *gorm.DB) error {
// ✅ 正确:用 tx
if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
return err
}
// ❌ 错误:用 db(会直接提交,绕过事务)
// db.Create(&User{Name: "Bob"})
return nil
})
- 事务函数内禁止出现任何
db.Xxx()调用 - 如果涉及关联表操作(如
tx.Preload().First()),也要确认 preload 是在tx上做的 - 嵌套调用时,要把
tx显式透传给下层函数,不能让 Repository 层偷偷用了全局db
手动 Begin/Commit/Rollback 容易漏掉错误分支
用 db.Begin() 启动事务后,必须保证每个可能出错的路径都调用 tx.Rollback(),否则连接会卡住、事务长期挂起,甚至导致数据库连接池耗尽。
推荐统一用 defer + recover 或者更稳妥的「两段式」写法:
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
if err := tx.Create(&Order{}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&OrderItem{}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
- 不要只在最后写一次
tx.Commit()就完事,中间任意一步失败都要主动Rollback() -
tx.Commit().Error本身也可能失败(比如锁超时),得检查它的返回值 - 避免在事务块里做 HTTP 请求、文件 IO 等不可控耗时操作,增加事务持有时间
跨 Repository 层事务失效
分层架构下,如果 logic 层调用多个 repository 方法,而每个 repo 内部又各自 new 了一个 db 或用了全局 db,那事务就完全断掉了。
正确做法是把 *gorm.DB 作为参数注入到各 Repository 方法中:
// repository/user.go
func (r *UserRepo) Create(tx *gorm.DB, u *User) error {
return tx.Create(u).Error
}
// repository/order.go
func (r *OrderRepo) Create(tx *gorm.DB, o *Order) error {
return tx.Create(o).Error
}
// logic/payment.go
func (l *PaymentLogic) Pay(userId uint, amount int64) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := l.userRepo.Create(tx, &User{...}); err != nil {
return err
}
if err := l.orderRepo.Create(tx, &Order{...}); err != nil {
return err
}
return nil
})
}
- Repository 方法签名必须接收
*gorm.DB,不能依赖内部持有的r.db - 事务生命周期由最上层(通常是 handler 或 logic)控制,下层只负责执行,不决定 commit/rollback
- 别在 Repository 里调
tx.Transaction()—— 嵌套事务在 MySQL 中实际是 savepoint,不是新事务,容易误判
自动迁移(AutoMigrate)和事务不兼容
db.AutoMigrate() 会隐式开启并提交自己的事务,无法被外层 Transaction() 包裹。如果你在事务里调了它,会导致「事务提前提交」,后续操作变成新事务,回滚失效。
迁移操作必须放在事务之外,且仅在服务启动时执行一次:
- 绝对不要在
db.Transaction()函数体内调AutoMigrate - 不要在 handler 或业务逻辑里动态调用
AutoMigrate(尤其是线上环境) - 开发期可放
main()或initDB()里,加个开关控制是否执行
事务真正的难点不在语法,而在数据流的可控性——你得清楚每一行 SQL 跑在哪条连接上、受不受当前事务约束。一旦有任意一环脱离 tx,回滚就只是幻觉。











