savepoint 是回滚锚点而非嵌套事务,仅支持 rollbackto 且依赖外层事务存活;命名需唯一、带业务上下文;局部容错必须用 begintx 而非 transaction。

SavePoint 不是嵌套事务,调用 tx.SavePoint("sp_name") 只是建一个回滚锚点,tx.Commit() 时它自动失效;想局部回滚必须用 tx.RollbackTo("sp_name"),且不能在事务已终止后调用。
为什么 tx.Begin() 在事务里不 panic 却也不提交子事务
GORM 的 tx.Begin() 在已有事务中不会真正开启新事务,而是执行 SAVEPOINT sp_xxx 并把名字记在返回的 *gorm.DB 实例上。这个实例和外层 tx 共享同一底层连接。
inner.Commit() 被 GORM 忽略——保存点不支持“提交”,只支持回滚到它;只有 inner.RollbackTo("sp_name") 才触发 ROLLBACK TO SAVEPOINT。
若外层已 Commit() 或 Rollback(),再操作该保存点会报错(如 PostgreSQL 的 current transaction is aborted)。
- 全局配置了
DisableNestedTransaction: true时,SavePoint()会静默失效,且无 warning - 别在
db.Transaction()回调里依赖保存点做“中途恢复”:回调一返回 error,整个事务就终止了,后续无法再用那个保存点
怎么安全命名和复用 SavePoint
保存点名必须唯一,否则后一次 tx.SavePoint("sp1") 会覆盖前一个,导致 RollbackTo("sp1") 跳过中间状态。
避免在循环里用 uuid.NewString()[:8] 动态生成名字——并发下仍有碰撞风险;也不要每个 Create() 前都设一个点,语义不清、难管理、易冲突。
- 推荐按业务子单元命名:
tx.SavePoint("sp_profile_create")、tx.SavePoint("sp_address_upsert") - 命名建议带上下文前缀,比如
"sp_user_order_123",便于排查和日志追踪 - 别混用
tx.SavePoint("sp")和原生tx.Exec("SAVEPOINT sp"),容易报no such savepoint
什么时候必须手动用 db.BeginTx() 而不是 db.Transaction()
db.Transaction() 是一次性闭环,适合「全有或全无」场景;一旦回调返回 error,整个事务立刻回滚,你没机会做局部恢复。
需要局部容错(比如地址创建失败但用户和 profile 仍要落库),就得自己用 db.BeginTx(ctx, nil) 启动事务,之后全程用该 tx 实例操作,并显式调 tx.SavePoint() 和 tx.RollbackTo()。
- 所有数据库操作必须复用同一个
*gorm.DB实例(即那个tx),不能穿插用db.Create() - DDL 操作(如
CREATE TABLE)在 MySQL 中可能隐式提交,破坏事务原子性,慎在保存点内执行 - 最后别忘了显式
tx.Commit()或tx.Rollback(),否则连接一直被占着
最常被忽略的一点:保存点不是为“每个写操作”准备的,而是为“可局部失败的业务边界”设计的——它解决的从来不是“怎么加”,而是“加在哪才真正有意义”。











