mysql不支持真正嵌套事务,所谓“嵌套”实为savepoint模拟;外层begin()启动事务,内层begin()仅设保存点,子事务失败需rollbackto()局部回滚,最终由外层commit()或rollback()统一控制。

嵌套事务不能用 Transaction() 套娃
很多人看到“嵌套事务”第一反应是在 Transaction() 闭包里再调一次 Transaction(),这是错的——Transaction() 不支持嵌套,第二次调用会脱离外层事务上下文,变成独立新事务,数据提前落库,原子性彻底失效。
真正能嵌套的只有 Begin() 链式调用,且必须手动管理每层状态。比如支付系统里「主订单 + 子退款单」这种场景,外层管订单创建,内层管退款单生成和冻结资金,两层失败要联动回滚。
- 外层
tx1 := db.Begin()启动主事务 - 内层
tx2 := tx1.Begin()复用连接,获得带 savepoint 的子事务句柄 - 所有操作(
Create、Update)必须只用对应层级的tx1或tx2,绝不能混用db -
tx2.Commit()实际只是打一个 savepoint 标记,不是真提交;tx1.Commit()才真正落库
RollbackTo() 是嵌套回滚的核心机制
嵌套事务里,子事务失败不等于整个流程失败。比如库存扣减失败,但日志记录成功了,你可能只想回滚到子事务起点,保留日志——这时候靠的是 SavePoint 和 RollbackTo(),不是直接 tx2.Rollback()。
GORM 没有显式 SavePoint API,但 tx.Begin() 内部就等价于设了一个命名 savepoint。所以正确回滚姿势是:
- 子事务出错时,调
tx1.RollbackTo("sp_name")(需先tx1.SavePoint("sp_name")) - 或者更常用:用
tx2 := tx1.Begin()后,直接tx2.Rollback()—— 这会回滚到tx2开始前的状态,不影响tx1已做的操作 - 最终外层仍需判断:若任意子事务失败,
tx1.Rollback();否则tx1.Commit()
panic 和 error 都得兜住,但方式不同
Transaction() 闭包能自动 recover panic 并回滚,但 Begin() 链式事务不会。一旦在 tx2 操作中 panic,tx1 连接卡死,后续请求可能 hang 在 acquire connection timeout。
所以手写嵌套事务时,必须主动加 defer + recover,或用 error 驱动流程:
- 别依赖 defer tx.Rollback() —— 它会在成功时误回滚
- 每个子事务块结尾统一检查 error,按需调
tx2.Rollback()或tx1.Rollback() - 外层 commit 前务必确认所有子事务返回 nil error,否则直接
tx1.Rollback() - 示例关键片段:
if err := tx2.Create(&refund).Error; err != nil { tx2.Rollback() return err // 让外层知道该终止 }
MySQL 和 PostgreSQL 对嵌套事务的支持差异
MySQL 本身不支持真正的嵌套事务,GORM 的 Begin() 嵌套实际是靠 savepoint 模拟的;PostgreSQL 原生支持 SAVEPOINT,行为更一致。但无论哪种数据库,GORM 层的语义是一样的:内层 Commit() 只是标记,外层不提交,什么都不会写入磁盘。
容易被忽略的一点是隔离级别——嵌套事务共享外层事务的 isolation level,无法单独设置。如果业务需要子事务用 ReadCommitted 而外层用 RepeatableRead,GORM 做不到,得拆成两个独立 Transaction() 调用,用应用层逻辑保证顺序和一致性。











