GORM 1.20不支持Spring风格事务传播,所谓“嵌套事务”实为SavePoint机制:外层Transaction开启真实事务,内层Transaction仅设回滚点,Rollback()只回滚至保存点而非终止外层;必须显式传递错误、避免同名SavePoint、状态变更与DB操作须严格绑定同一tx实例。

多级事务不能靠嵌套函数自动传播
GORM 1.20 的 Transaction 函数不支持 Spring 风格的事务传播(如 REQUIRED、REQUIRES_NEW),所谓“嵌套事务”本质是 SavePoint 机制,不是真正独立的事务上下文。外层 db.Transaction 启动一个事务,内层 tx.Transaction 只是在该事务里设一个回滚点,Rollback() 不会终止外层,只会回滚到 SavePoint。
常见错误现象:调用两层 Transaction 后,内层出错 Rollback(),外层仍继续执行并 Commit(),结果部分数据写入成功——这不是预期的“全失败”,而是状态撕裂。
- 想实现“内层失败则整个外层也失败”,必须手动传递错误,不能依赖内层自动中断外层
-
tx.Transaction返回的 error 必须显式检查并返回,否则外层会当作成功继续 - SavePoint 名称不可重复,多次调用同名 SavePoint 会覆盖前一个,GORM 默认用随机名,一般不用管
状态机流转必须靠显式状态字段 + 事务边界对齐
真实业务里的状态机(比如订单从 created → paid → shipped → delivered)不能只靠数据库字段更新,必须把状态变更和事务操作严格绑定在同一事务中,否则会出现“状态已改但业务数据未落库”或反之。
典型踩坑场景:先 db.Model(&order).Update("status", "paid"),再 tx.Create(&payment),两个操作不在同一事务里,中间若崩溃,订单状态卡在 paid 但支付记录没生成,下游无法对账。
- 所有状态变更和关联操作必须塞进同一个
tx实例里,不要混用db和tx - 状态校验逻辑(如“只能从 created 切到 paid”)必须放在事务内做,用
SELECT ... FOR UPDATE加行锁防并发冲突 - 避免在事务里调用外部 HTTP 或消息队列,一旦失败无法回滚,应改为事务提交后异步触发
手动 Begin/Commit/SavePoint 是唯一可控方式
当状态机分支多、条件复杂(比如“超时自动取消”“库存不足降级为预售”),用 db.Transaction 闭包封装反而难调试。直接用 Begin() + 显式 SavePoint() 更清晰。
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
<p>// 主流程
if err := tx.Create(&order).Error; err != nil {
tx.Rollback()
return err
}</p><p>// 设置保存点,用于后续分支回滚
sp, err := tx.SavePoint("after_order")
if err != nil {
tx.Rollback()
return err
}</p><p>// 分支一:尝试扣减库存
if err := tx.Model(&Stock{}).Where("product_id = ? AND amount >= ?", order.ProductID, order.Quantity).Updates(map[string]interface{}{"amount": gorm.Expr("amount - ?")}).Error; err != nil {
tx.RollbackTo(sp) // 回滚到保存点,不影响 order 记录
// 启动降级逻辑...
}</p><p>return tx.Commit().Error
-
SavePoint不是事务,只是回滚锚点,RollbackTo后仍可继续在该事务中执行其他操作 - 多个
SavePoint可以嵌套命名,但必须按创建顺序倒序回滚,否则报错 - 注意
RollbackTo失败时不会自动触发Rollback,需额外判断
事务内 panic 被捕获不等于安全
GORM 1.20 确实会捕获事务回调里的 panic 并转成 error,但这只解决“不悬挂”的问题,不解决“语义错误”。比如在回调里忘了 return error,panic 被转成 error 后被忽略,事务照常 Commit()。
最易被忽略的点:事务函数签名是 func(*gorm.DB) error,但很多人写成 func(*gorm.DB)(无返回值),此时 GORM 根本不会执行该函数,也不会报错,静默跳过。
- 务必检查函数签名是否带
error返回值 - 所有关键路径上的 error 都要显式返回,不要只 log
- 测试时故意在事务内 panic,确认最终行为是 rollback 而非 commit


















