嵌套事务本质是savepoint而非新事务:gorm的tx.begin()仅创建保存点并返回带名gorm.db,不生成新sql.tx;inner.rollback()回滚至保存点,inner.commit()无效;outer.rollback()清除所有保存点并终结整个事务。

嵌套事务本质是 SAVEPOINT,不是新事务
GORM 的 tx.Begin() 在已有事务中调用,不会开启新事务,而是执行 SAVEPOINT sp_xxx 并返回一个带保存点名的 *gorm.DB 实例。它不产生新的 *sql.Tx,也不改变隔离级别、超时或上下文绑定。
这意味着:
-
inner.Rollback()实际执行的是ROLLBACK TO SAVEPOINT sp_xxx,只回滚该保存点之后的操作,外层已提交/修改的数据不受影响 -
inner.Commit()是无效操作,GORM 会忽略——保存点没有“提交”语义 - 外层调用
outer.Rollback()会连带清除所有保存点,整个事务彻底失效 - 若内层出错但未手动
inner.Rollback(),保存点仍存在,可能干扰后续逻辑或触发重复回滚
database/sql 层禁止嵌套 Begin(),否则 panic
标准库 database/sql 和所有主流驱动(pgx、mysql、sqlite3)都不允许在已有事务连接上调用 db.Begin() 或 db.BeginTx()。这不是 GORM 的 bug,而是 SQL 标准本身不支持嵌套事务。
典型错误现象:
panic: sql: transaction has already been committed or rolled back-
pgx: pq: SAVEPOINT can only be used in transaction blocks(PostgreSQL 驱动报错) -
driver: bad connection(MySQL 驱动因连接状态异常返回)
唯一合法做法:复用当前 *sql.Tx,通过 SAVEPOINT 划分逻辑边界,而不是试图新建事务句柄。
GORM Transaction() 闭包不支持传播,也无法中途退出
db.Transaction(func(tx *gorm.DB) error { ... }) 是一次性闭环结构:进入即开启事务,返回 nil 即提交,返回非 nil 错误即回滚。它不提供 REQUIRES_NEW 或 NESTED 这类传播行为。
常见误用场景:
- 想在事务中“暂停外层、另起一个独立事务发消息”,结果只能靠手动
outer.Rollback()+db.BeginTx(),但这已脱离原 context 和连接池状态 - 封装了日志写入或埋点上报,内部用了独立
*sql.DB实例 → 这些操作完全游离于事务 context 之外,超时、取消、回滚均不生效 - 试图在闭包内调用
tx.Transaction(...)做嵌套 → 实际只是新增保存点,且容易因错误处理遗漏导致状态混乱
真正需要隔离的子操作,应该剥离出事务
如果业务要求“扣库存成功但发消息失败,仍算整体成功”,那就不能依赖嵌套事务。MySQL 不支持跨事务的局部提交,GORM 的保存点也做不到数据可见性隔离。
推荐做法是最终一致性方案:
- 事务内只做数据库变更(如更新库存、生成订单),确保 ACID
- 发消息、调第三方 API、写日志等副作用操作,全部移出事务,在提交成功后异步执行
- 配套幂等设计 + 补偿任务(如定时扫描待发消息表)
- 避免把 HTTP 调用、文件写入、Redis 操作混进事务函数体 —— 它们既不受事务控制,又拖慢事务持有时间
保存点适合的场景非常窄:单次事务内循环插入时跳过某条脏数据,或条件分支中临时回退部分变更。把它当成“轻量级事务”来用,很容易踩到数据不一致的坑。











