事务必须显式开启且全程使用tx对象,db.Create()等操作因走全局db实例而不参与事务;GORM的db.Transaction()依据闭包返回值自动提交或回滚,多数据源需手动协调并加context超时。

事务必须显式开启,db.Transaction 不能省;所有操作必须用 tx 对象,混用原始 db 就等于绕过事务。
为什么 db.Create() 在 db.Transaction 里也不起作用
因为 db.Create() 默认走的是全局 db 实例,它背后是独立的数据库连接,不参与当前事务上下文。哪怕你在 db.Transaction() 函数体内调用,只要没用参数传入的 tx,就完全不在事务里。
- ✅ 正确写法:
tx.Create(&order)、tx.Model(&user).Update(...) - ❌ 错误写法:
db.Create(&order)、db.Save(&user)—— 这些会立刻提交,无法回滚 - 关联操作也要用
tx:比如tx.Preload("Items").First(&order, id),否则预加载可能走的是非事务连接 - 嵌套函数调用时,必须把
tx显式传进去,不能让 repository 层偷偷用了包级db
db.Transaction 函数返回值决定提交还是回滚
GORM 的 db.Transaction() 是“函数式事务”:只有你传入的闭包函数返回 nil,才会自动提交;只要返回任意非 nil error,GORM 就自动执行 Rollback(),无需手动干预。
- 函数内不要做可能 panic 的事(比如
c.JSON()、空指针解引用),否则 panic 会跳过 error 返回逻辑,导致事务既没提交也没回滚,连接卡住 - 错误处理要尽早 return:每个
tx.Xxx().Error都该检查,别攒到最后统一判 - 别在事务函数里调用耗时外部服务(如 HTTP 请求、Redis 写入)——超时或失败会导致事务长时间挂起
多数据源场景下 db.Transaction 完全失效
db.Transaction() 底层绑定单个 *sql.Tx,只对一个数据库实例有效。MySQL + PostgreSQL、或两个 MySQL 实例之间,不存在“跨库事务”。
- ❌ 错误模式:
db1.Transaction(func(tx1 *gorm.DB) error { tx1.Create(...); db2.Create(...) })——db2.Create()是另一个已提交的独立操作 - ✅ 可行路径:用
db1.BeginTx(ctx, &sql.TxOptions{})和db2.BeginTx(ctx, ...)分别启两个事务,手动协调;任一失败,对另一个已成功提交的事务无法 rollback,只能靠补偿(如定时扫描 + 幂等重试) - 必须加 context 超时:防止某个库卡死拖垮整个流程,
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - FOR UPDATE 锁只在本库生效,别指望它能同步多源状态
最容易被忽略的一点:事务不是“开了就安全”,而是“用了才生效”。从 handler 到 service 再到 repository,每一层都得确认自己操作的是 tx,而不是习惯性写的 db。变量命名(比如叫 tx 而不是 db)和代码审查比文档更重要。


















