db.Transaction是微服务中唯一推荐的事务入口,因其自动管理panic捕获、连接归还和上下文绑定;手动Begin易漏Rollback或复用已提交tx,导致连接泄漏或数据不一致。

db.Transaction 是唯一推荐的事务入口
微服务中绝大多数本地事务必须用 db.Transaction,而不是手写 db.Begin()。原因很直接:微服务请求生命周期短、并发高、错误路径多,db.Transaction 自动管理 panic 捕获、连接归还和上下文绑定,而手动事务极易漏掉 Rollback() 或复用已提交的 tx 实例,导致连接泄漏或数据不一致。
常见错误现象包括:
- 事务函数内分支未统一 return error,成功路径只写
return(无值),导致事务静默丢弃 - 在
tx内误调用原始db.Create(),该操作绕过事务直连数据库,破坏原子性 - 事务结束后继续使用同一
tx实例,触发"invalid transaction"panic
事务必须包裹全部 DB 操作,I/O 必须剥离
HTTP 调用、Redis 缓存写入、日志上报、消息队列投递等任何外部 I/O,严禁放在 db.Transaction 函数体内。它们可能超时、失败或阻塞,直接拖垮数据库连接池,甚至让整个事务卡住数秒。
正确做法是把 I/O 移到事务外,用 tx.AfterCommit() 注册回调:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err
}
if err := tx.Model(&stock{}).Where("id = ?", skuID).Update("qty", gorm.Expr("qty - ? ", num)).Error; err != nil {
return err
}
<pre class="brush:php;toolbar:false;">// ✅ 注册异步钩子,仅在 commit 成功后触发
tx.AfterCommit(func(tx *gorm.DB) {
go cache.Invalidate("stock:" + strconv.Itoa(skuID))
go notifyOrderCreated(order.ID)
})
return nil})
注意:tx.AfterCommit() 只对当前 tx 实例生效;若传参错用 db 或新建 tx.WithContext(),钩子会失效。
嵌套逻辑 ≠ 嵌套事务,SavePoint 不等于子事务
微服务里常有“主流程+补偿动作”或“核心写+旁路记录”的需求,容易误以为要写两层 db.Transaction。实际上 GORM 的嵌套调用只是 SAVEPOINT,不是独立事务:
-
tx.Transaction(...)在已有事务内执行,底层是SAVEPOINT sp_xxx,回滚只影响该保存点之后的操作 -
outer.Rollback()会连带清除所有内部保存点,无法实现“外层提交、内层回滚” - MySQL/PostgreSQL 对 SavePoint 的支持一致,但跨引擎迁移时行为不可移植
真正需要局部回滚的场景(如记录审计日志失败不影响主业务),应拆成两个独立事务 + 幂等重试,而非依赖 SavePoint。
事务粒度要窄,别包整个 HTTP handler
一个 Gin handler 里混着参数校验、DB 写入、缓存更新、第三方调用,如果把整段逻辑塞进 db.Transaction,事务持有时间会被非 DB 操作拉长,加剧锁竞争和超时风险。实际项目中,事务边界应严格限定在“读-改-写”这三步数据库操作上。
例如下单接口:
- ✅ 允许:查库存 → 扣库存 → 创建订单 → 更新用户余额
- ❌ 禁止:解析 JSON body → 校验优惠券 →
db.Transaction{...}→ 发 Kafka → 返回响应
事务越短,锁等待越少,连接池复用率越高。线上压测发现,事务平均耗时从 120ms 降到 35ms 后,QPS 提升近 3 倍,且死锁率归零。


















