gorm无真正嵌套事务,仅用savepoint模拟局部回滚;db.transaction()嵌套会创建独立事务破坏原子性,正确做法是外层begin()、内层savepoint(),并显式处理error和panic。

GORM 没有真正的嵌套事务,所谓“嵌套”只是用 SAVEPOINT 模拟的局部回滚机制;外层事务未提交前,所有子操作都处于同一数据库连接和事务上下文中,tx.Begin() 不会开启新事务,只设保存点。
为什么 db.Transaction() 里再套 db.Transaction() 会出错
第二次调用 Transaction() 会脱离外层事务上下文,创建一个全新、独立的事务,导致数据提前落库,原子性完全失效。GORM 的 Transaction() 是闭环设计:它内部调 Begin() → 执行闭包 → 根据 error 自动 Commit() 或 Rollback(),不支持中途插入另一个同级事务。
- 错误写法:
db.Transaction(func(tx1 *gorm.DB) error { return db.Transaction(func(tx2 *gorm.DB) error { ... }) }) - 正确替代:外层用
db.Begin(),子逻辑用tx1.Begin()(即保存点),所有操作必须显式使用对应层级的tx1或tx2 - 混用
db.Create()和tx1.Create()会导致部分操作不进事务,查不到数据或重复提交
tx.Begin() 实际做了什么
在已有事务中调 tx.Begin(),GORM 不会调底层 db.Begin()(那会 panic),而是执行 SAVEPOINT sp_xxx,并返回同一个 *gorm.DB 实例,仅内部记录保存点名。因此:
-
tx2.Commit()被忽略,不是真提交 -
tx2.Rollback()等价于ROLLBACK TO SAVEPOINT sp_xxx,只回滚该保存点之后的操作 -
tx1.Rollback()会连带清除所有保存点,整个事务作废 - 若已调
tx1.Commit(),再对tx2做任何操作都会报错(如 PostgreSQL 的 “current transaction is aborted”)
保存点命名与错误处理必须手动兜底
保存点名重复、panic 未捕获、error 未检查,是三个高频翻车点。GORM 不自动 recover panic,也不校验保存点名唯一性。
- 避免硬编码名:
tx1.SavePoint("sp_profile")在并发请求下极易重名,改用"sp_" + uuid.NewString()[:8] - 别依赖
defer tx2.Rollback():它会在成功时误回滚,必须根据 error 显式判断 - panic 必须主动 recover:
defer func() { if r := recover(); r != nil { tx1.RollbackTo(spName) } }() - 全局禁用嵌套事务时(
DisableNestedTransaction: true),SavePoint()静默失效且无 warning,需确认配置
真正需要局部回滚时,不要强求“嵌套事务”语义,优先考虑拆成独立幂等事务 + 重试;若必须保留在同一事务内,就老实用 Begin() 链 + RollbackTo(),并把 panic 和 error 当成一等公民来处理。











