事务必须显式开启并全程使用同一tx对象,禁用全局db;关联查询须用tx.preload();中间件不可中断事务context;嵌套事务需改用savepoint。

事务必须显式开启,不能依赖自动提交
在 Gin + GORM 场景下,db.Create() 或 db.Save() 默认走的是自动提交模式。哪怕你写了多条写操作,只要没手动启事务,每条语句都是独立事务——一旦中间某步失败,前面已执行的不会回滚,数据就脏了。
常见错误是直接在 handler 里串行调用多个 Create,比如先建订单、再建订单商品、再扣库存,看起来逻辑连贯,但实际没有原子性保障。
- 必须用
db.Transaction()包裹整个业务块,函数参数接收一个*gorm.DB实例(已绑定事务上下文) - 传入的
tx对象要全程使用,不能混用全局db变量,否则会跳出事务作用域 - 事务内所有操作必须返回 error,且仅当 error 为 nil 时才提交;非 nil 则自动 rollback
嵌套事务不生效,要用 SavePoint 控制局部回滚
GORM 的 Transaction() 不支持真正的嵌套事务(MySQL 本身也不支持)。如果在事务内又调用另一个封装了 db.Transaction() 的函数,外层事务会被中断,变成两个独立事务。
典型场景:下单主流程里调用「扣库存」子服务,而该子服务自己也写了事务——结果就是扣库存成功了,但下单失败,库存却没恢复。
- 子逻辑改用
tx.Session(&gorm.Session{AllowGlobalUpdate: false}).Create()避免误操作全局 db - 需要局部回滚时,用
tx.SavePoint("sp1")和tx.RollbackTo("sp1")替代嵌套 - 注意 SavePoint 名称唯一性,重复名会导致 panic
事务内关联查询要加 tx.Preload(),别用全局 db
事务中查关联数据(比如查订单后预加载商品详情),如果仍用全局 db.Preload(),GORM 会新开连接执行查询,脱离当前事务隔离级别,可能读到未提交的中间状态或脏数据。
尤其在 RR(Repeatable Read)隔离级别下,这种跨事务读取会让一致性校验失效。
- 所有关联操作必须基于事务对象:
tx.Preload("OrderGoods.Goods").First(&order, orderId) -
Preload不能链在db.Transaction()外层调用,必须在事务函数体内 - 若需 Join 查询,同样用
tx.Joins("JOIN m_goods...").Find(),避免隐式连接
Context 传递要透传,别让中间件切断事务链
Gin 中间件(如日志、鉴权)默认不感知事务上下文。如果你在中间件里用了 db.WithContext(c.Request.Context()),这个 context 没绑事务,后续 handler 里再开事务,两者 context 就断开了。
后果是:事务提交/回滚时,context cancel 可能提前触发,或数据库连接被意外释放。
- 事务应从 handler 入口开始,在
c.Request.Context()基础上用context.WithValue()注入事务对象(不推荐)或更稳妥地——把tx作为参数显式传给 service 层 - 避免在中间件里做任何数据库操作;如需审计日志,等事务结束后再异步写
- 使用
tx.Session(&gorm.Session{Context: c.Request.Context()})确保底层连接复用且上下文一致
Transaction() 就万事大吉,关键在「全程使用同一事务对象」和「上下文不泄漏」。最容易被忽略的是 Preload 和中间件里的 context 误用——这两处出错,事务形同虚设。











