事务失效最常见原因是函数内误用全局db或gorm.open()创建新实例,导致tx未被传递;preload默认走主db而非tx;commit/rollback遗漏或失败未二次处理;多数据源下跨库操作不支持单事务。

事务里调用的 db 不是同一个实例
最常见原因:你在函数内部又用了包级全局 *gorm.DB 变量,或者重新调用了 gorm.Open()。事务对象不会自动穿透到子函数——tx 是一个独立实例,它和你平时用的 db 毫无关系。
实操建议:
- 所有事务内操作必须显式传入
tx *gorm.DB参数,禁止在函数里直接引用外部db - 检查是否误用了
db.Session(&gorm.Session{PrepareStmt: true}):它返回新实例,若没基于tx创建,就脱离事务上下文 - 调试时加一句
fmt.Println(tx.Statement.ConnPool == nil),输出false才说明处于有效事务中
Preload 或关联操作绕过了事务
Preload 默认走的是主 db 实例,不是事务 tx。哪怕你在 tx.Preload("User").First(&order) 里写了 tx,GORM v1.20 仍可能忽略它,尤其在嵌套预加载或关联字段未导出时。
实操建议:
- 确保被预加载的结构体字段首字母大写(已导出),否则反射赋值失败,
Preload静默跳过 - 用
tx.Debug().Preload("User").First(&order)看生成 SQL 是否真带SELECT ... FROM users;若没出现,说明预加载根本没生效 - 避免在事务中混用
db.Preload和tx.First,全部统一用tx实例
事务未 Commit 或 Rollback 被吞掉
GORM v1.20 对 panic 的处理已改进,但手动事务仍极易漏掉 Commit() 或错误路径下的 Rollback()。更隐蔽的是:如果 Commit() 自身报错,你没再调一次 Rollback(),事务就“悬挂”了。
实操建议:
- 必须用
defer+recover包一层,防止 panic 导致 rollback 被跳过 - 不要只写
if err := tx.Commit().Error; err != nil { tx.Rollback() }——Commit()失败后,tx.Rollback()可能也失败,得再判一次 error - 检查
tx.Create()、tx.Save()等方法是否都取了.Error,漏判一个就会让后续逻辑继续执行,掩盖事务中断
多数据源下事务跨库失效
如果你连了多个库(比如 userDB、logDB),却在一个事务里混着操作两个 *gorm.DB 实例,那它根本不是分布式事务——只是单库事务,另一库的操作完全不受控。
实操建议:
- GORM 原生不支持跨库事务。要么改用单库分表,要么上两阶段提交(如 Saga),别指望
tx能管住两个独立db实例 - 确认每个库都有独立变量:
var userDB, logDB *gorm.DB,且事务只在其中一个上开启 - 检查驱动导入是否漏了下划线:比如
import _ "gorm.io/driver/mysql",否则gorm.Open()静默失败,tx根本没建起来
Begin() 到 Commit() 全部跑在同一个 *gorm.DB 实例上,且中间没被任何隐式创建的新实例截断。最容易被忽略的,是 Preload 和 Session 这类看似“只是加个功能”的调用,实际会悄悄换掉底层连接池。











