事务内必须统一使用同一tx实例,混用db和tx会导致事件与数据脱节;正确做法是闭包式事务、AfterCommit触发事件、显式透传*gorm.DB参数。

事务内不能混用 db 和 tx,否则事件和数据必然脱节
最常见也最危险的错误:在 db.Transaction() 回调里,一边用 tx 更新库存,一边又用原始 db 发送消息或写日志。比如:
-
tx.Model(&stock).Update("num", stock.Num-1)—— 这步受事务保护 -
db.Create(&OrderLog{Action: "order_created"})—— 这步立刻落库,哪怕后续tx回滚,日志已不可逆
结果就是:事务回滚了,库存没扣,但日志写了、消息发了、缓存删了——事件和数据库状态彻底不一致。根本原因在于 db 和 tx 是两个完全独立的连接上下文,tx 的隔离性对 db 零影响。
db.Transaction() 是默认安全选择,别手动 Begin() 除非真需要 SavePoint
90% 的业务场景(如创建订单+扣库存+写流水)应该直接用闭包式事务。它自动处理 panic 捕获、error 判定、commit/rollback 分支,不会漏掉任何一环。
- ✅ 正确:
db.Transaction(func(tx *gorm.DB) error { ... return tx.Create(&order).Error }) - ❌ 危险:
tx := db.Begin()后自己手写defer tx.Rollback()+tx.Commit(),稍有疏忽就卡死连接池 - ⚠️ 仅当需要局部回滚(比如校验失败只撤回某一步,不影响外层)才用
tx.SavePoint("sp1")+tx.RollbackTo("sp1")
手动事务容易在 error 处理分支遗漏 tx.Rollback(),或在成功路径忘记 tx.Commit(),导致连接长期占用,后续请求直接超时。
Hooks 必须绑定到 tx 实例,且只在 AfterCommit 中触发外部事件
GORM Hooks 本身不感知事务,AfterCreate 或 AfterUpdate 在 tx 上执行时,只是“事务内的一次回调”,不代表事务已提交。真正可靠的时机只有 AfterCommit。
- ✅ 安全:
func (u *Order) AfterCommit(tx *gorm.DB) { go publishOrderEvent(u.ID) }—— 此时tx已 commit,数据库状态确定 - ❌ 危险:
func (u *Order) AfterCreate(tx *gorm.DB) { redis.Del("stock:" + u.GoodsID) }—— 若事务后续回滚,缓存已删,数据库却没变,出现脏读 - ⚠️ 注意:
AfterCommit里不能再执行数据库写操作(如更新日志表),否则可能抛出transaction has been committed or rolled back错误
如果必须在提交后做数据库操作(比如写本地消息表),应新开一个 db 连接,而非复用 tx。
跨函数传递事务时,所有层都必须接收 *gorm.DB 参数
当业务逻辑分层(如 handler → service → repo),事务对象必须显式透传,不能在任意一层偷偷拿全局 db。
- ✅ 正确:
service.CreateOrder(tx, req)→repo.CreateOrder(tx, order),全程用同一个tx - ❌ 危险:
service.CreateOrder(db, req)里又调repo.CreateOrder(db, order),实际用的是无事务的原始连接 - ⚠️ 结构体字段若存 DB 实例,必须是
*gorm.DB类型,值类型会导致编译失败或行为异常
事务一致性不是靠“开了事务”就自动生效,而是靠整个调用链路严格使用同一个 tx 实例。一旦某层绕过参数传入、直接取全局变量,一致性即刻崩塌。
事务和事件一致性的关键不在“有没有开事务”,而在于所有数据库操作和事件触发是否共享同一事务上下文。最容易被忽略的,是 Hooks 的执行时机错配和跨层调用时的连接混用——这两点几乎覆盖了线上 80% 的数据不一致故障。











