事务操作必须使用 tx 对象,db.begin() 返回新 *gorm.db 实例,原始 db 无事务;db.transaction 易静默失败,需确保函数各分支显式 return err;嵌套事务实为 savepoint,非真正子事务;默认事务可关闭以提升性能,但需自行保障一致性。

事务必须用 tx 对象操作,不能继续用原始 db;嵌套事务本质是 SAVEPOINT,不是真正的子事务;db.Transaction 是最安全的默认选择,但复杂流程仍得手动管理。
为什么 db.Begin() 后还用 db.Create() 就失效了?
因为 db.Begin() 返回的是一个全新的 *gorm.DB 实例(带事务上下文),原始 db 仍是无事务的普通连接。所有 CRUD 必须链在返回的 tx 上,否则压根不进事务。
- 错误写法:
tx := db.Begin(); db.Create(&u)→ 这条插入走的是默认连接,完全绕过事务 - 正确写法:
tx.Create(&u),且后续所有操作都必须用tx -
tx提交或回滚后不可复用,再调tx.Create()会 panic 报"invalid transaction"
db.Transaction 看似简单,但容易静默失败
它只执行函数、捕获 panic 并回滚,但不校验函数是否忘了 return err 或漏掉 tx.Commit()。如果函数里没出 panic、也没显式 return 错误,事务就静默丢弃——既不提交也不回滚。
- 必须确保函数内每个分支都明确
return err,别用裸return - 函数内若调用
os.Exit或runtime.Goexit,Transaction拦不住,事务状态残留 - 适合场景:单次原子操作,如“创建用户 + 插入默认地址”,逻辑短、出口少
嵌套事务 ≠ 子事务,而是 SAVEPOINT 模拟
GORM 的 tx.Begin() 在已有事务内调用,底层不是数据库原生嵌套事务(MySQL 本身不支持),而是建 SAVEPOINT。这意味着:
- 内层
tx.Rollback()只回滚到 savepoint,外层已执行的操作不受影响 - 外层
tx.Rollback()会连带清除所有 savepoint,整个事务彻底失效 - 别指望靠它实现“部分提交”——GORM 不提供跨事务的数据可见性控制
- 统一用
tx.SavePoint("sp_name")和tx.RollbackTo("sp_name"),别混用原生 SQL 的SAVEPOINT
要不要关掉默认事务?看场景
GORM 默认对每个 Create/Update/Delete 都包一层事务,确保单条语句原子性。关掉能提 30%+ 写入性能,但代价是你得自己兜底。
- 开启方式:
gorm.Config{SkipDefaultTransaction: true} - 适用场景:日志写入、埋点上报、缓存刷新等“尽力而为”型操作
- 风险点:单条失败不再自动回滚(本来也没啥可回滚),但你也失去了隐式一致性保障
- 注意:
db.CreateInBatches默认也不开事务,某一批失败,前面批次已提交,数据可能不一致
真正难的不是写 tx.Commit(),而是判断哪一步该提前 Rollback、哪个 SavePoint 失效了还去 ROLLBACK TO、或者 HTTP 调用卡在事务里拖垮连接池——这些细节不踩一次坑很难记住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











