嵌套事务本质是 SAVEPOINT 而非真正事务,GORM 通过 tx.Transaction() 或 tx.Begin() 实现局部回滚;内层 Rollback 仅回退至 savepoint,外层操作不受影响;outer.Rollback() 则彻底回滚整个事务;内层 error 不自动触发外层 rollback,需显式处理;并发下 SAVEPOINT 不提升隔离性,仍存幻读与超扣风险;务必在 Transaction 闭包中使用 tx 而非 db,否则操作脱离事务上下文导致原子性崩塌;db.Transaction() 自动管理 rollback 和连接释放,更安全;db.Begin() 需手动配对 Commit/Rollback,漏写将卡死连接;禁用默认事务可提性能但仅适用于最终一致性场景,强一致性链路严禁关闭;真正易被忽略的是混淆 tx 与 db 的使用,引发隐蔽数据错误。

嵌套事务不是事务,是 SavePoint
GORM 的 tx.Transaction() 或 tx.Begin() 嵌套调用,底层不创建新事务,而是打 SAVEPOINT。MySQL 本身不支持真正嵌套事务,GORM 只是模拟局部回滚能力。
这意味着:
-
inner.Rollback()只回退到 savepoint,外层已执行的UPDATE、INSERT不受影响 -
outer.Rollback()会清除所有 savepoint,整个事务彻底失败 - 内层返回 error 并不会自动触发外层 rollback —— 外层仍需显式判断并处理
- 并发场景下,
SAVEPOINT不提供隔离性提升,幻读、超扣风险照旧
别在 Transaction 闭包里混用 db 和 tx
常见错误:在 db.Transaction(func(tx *gorm.DB) error {}) 里写 db.Create(&u),而不是 tx.Create(&u)。
后果很直接:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
db.Create()走的是新连接或空闲连接,完全脱离当前事务上下文 - 数据提前落库,后续
tx.Rollback()对它无效,原子性崩塌 - 尤其在扣库存+发券链路中,极易导致超发、负余额
- 日志可能显示“事务已回滚”,但部分操作早已生效
Transaction 是默认安全项,Begin 是危险手动模式
db.Transaction() 是推荐方式:panic 或返回 error 都会自动 rollback,连接也保证释放。
db.Begin() 必须手动配对 Commit()/Rollback(),漏掉任意一个就会卡死连接:
- 没
tx.Commit()→ 连接一直占用,后续请求卡在acquire connection timeout - 没
defer tx.Rollback()或没条件判断 → 成功时误 rollback,数据丢失 - 跨函数传递
tx时,调用链任意一环漏处理,整条链不可靠
除非你要实现 savepoint 分段回滚,或必须把事务对象传进多个独立函数,否则别碰 Begin()。
禁用默认事务能提性能,但得清楚代价在哪
写操作(Create/Update/Delete)默认走事务,关掉可提升 30%+ 吞吐量,但仅适用于最终一致性场景:
- 全局关闭:
gorm.Config{SkipDefaultTransaction: true}—— 日志、埋点、缓存更新可用 - 单次关闭:
db.Session(&gorm.Session{SkipDefaultTransaction: true}).Create(...)—— 更精准,推荐 - 严禁在扣库存+生成订单、优惠券发放等强一致性链路里关,否则超卖不可避免
- 关事务后,
LAST_INSERT_ID()在 MySQL 下可能拿不到值,影响主键依赖逻辑
真正容易被忽略的,不是要不要嵌套,而是连 db.Transaction 里该用 tx 还是 db 都没分清 —— 这种错误上线后不会报错,只会悄悄发错券、扣错钱、查不到数据。

















