gorm 的 tx.begin() 实际执行 savepoint 而非新事务,不创建新 *sql.tx、不改变隔离级别或 ctx;inner.rollback() 回滚到保存点,inner.commit() 被忽略;所有操作共享同一底层事务连接。

Go 没有事务传播机制,GORM 的 Transaction() 和 Begin() 都不支持类似 Spring 的 REQUIRES_NEW 或 NESTED 语义——这不是 bug,是 SQL 标准和 database/sql 底层约束共同决定的。
为什么 GORM 里调 tx.Begin() 不等于开启子事务
GORM 的 tx.Begin() 实际只执行一条 SAVEPOINT sp_xxx,返回一个带保存点名的 *gorm.DB 实例。它不创建新 *sql.Tx,也不改变隔离级别、超时或 context 绑定。
-
inner.Rollback()→ 翻译为ROLLBACK TO SAVEPOINT sp_xxx,外层事务仍可继续 -
inner.Commit()→ GORM 忽略该调用,保存点本身不提供“提交局部变更”的能力 - 所有操作共享同一个底层
*sql.Tx连接,因此共用初始BeginTx(ctx, opts)中传入的ctx - 若在事务中混用
db.QueryRow()(非tx.QueryRow()),那些查询脱离事务上下文,不受超时/取消控制
如何手动模拟 REQUIRES_NEW 效果
真要实现“挂起当前事务、开新事务再回来”,必须放弃自动管理,手动拆解生命周期:
- 先调
tx.Rollback()主动终止当前事务(注意:这会丢弃所有未提交变更) - 再用原始
*gorm.DB调db.BeginTx(ctx, &sql.TxOptions{...})开新事务 - 业务逻辑执行完后,显式
newTx.Commit()或newTx.Rollback() - 若需恢复原事务状态,得靠应用层缓存数据、重试逻辑、或领域事件补偿——GORM 不提供挂起/恢复能力
这种做法已脱离“事务嵌套”范畴,属于跨事务协调,务必配套幂等设计与错误回溯机制。
容易被忽略的 context 陷阱
事务的 ctx 只在 BeginTx() 时绑定一次,后续所有 tx.Query()、tx.Exec() 都复用它——但 *sql.Tx 类型没有 QueryContext() 方法,你无法中途替换。
- 日志写入、埋点上报等辅助操作若用了独立
*sql.DB实例,它们完全游离于事务ctx外,可能超时未触发、panic 未捕获 - 异步 goroutine 中调用
tx方法,若原ctx已 cancel,tx操作会立即失败,但不会自动 rollback - 测试时用
context.Background()启动事务,上线后切到带 timeout 的ctx,可能暴露之前没发现的阻塞点
真正棘手的不是怎么写 tx.Begin(),而是当多个业务函数都声称“我需要事务”时,你得亲手厘清它们之间是共享、隔离还是互斥——GORM 不替你做这个决策,它只提供 SAVEPOINT 这把小刀,而手术方案得你自己画。











