绝大多数业务写操作必须用 db.Transaction 而非手写 Begin/Commit,因 db.Begin() 返回新事务实例 tx,混用 db 会绕过事务直连数据库;tx 提交或回滚后不可复用;db.Transaction 仅捕获 panic 并回滚,不校验逻辑错误、不自动提交,易静默失败;嵌套事务实为 SAVEPOINT;HTTP/缓存等 I/O 操作严禁入事务体,应移至 tx.AfterCommit() 或异步处理。

绝大多数业务写操作,必须用 db.Transaction,而不是手写 Begin/Commit;否则极大概率漏回滚、卡连接、数据不一致。
为什么 db.Begin() 后还用 db.Create() 就失效了
因为 db.Begin() 返回的是一个全新的 *gorm.DB 实例(带事务上下文),原始 db 对象仍是无事务的普通连接。所有 CRUD 操作若继续走 db,就等于绕过事务直连数据库。
- ✅ 正确:用返回的
tx调用tx.Create()、tx.Update()、tx.Preload().First() - ❌ 危险:在
tx事务中混用db.Create()—— 这条记录已不可逆落库,破坏原子性 - ⚠️ 注意:
tx提交或回滚后不可复用,再调tx.Create()会 panic 报"invalid transaction"
db.Transaction 静默失败的三种典型场景
它只捕获 panic 并回滚,但不校验逻辑错误、不强制 return err、也不自动提交——稍不注意就“啥也没干成”。
- 函数内分支遗漏
return err,比如条件判断后直接return(无值),事务静默丢弃 - 忘了在成功路径上显式
tx.Commit(),而仅靠return nil——db.Transaction不会自动提交,连接归还池时事务被丢弃 - 函数里发生 panic 被捕获并回滚,但若调用
os.Exit()或runtime.Goexit(),它拦不住,事务挂起
嵌套事务不是子事务,而是 SAVEPOINT 模拟
GORM 的 tx.Begin() 在已有事务内调用,底层是数据库 SAVEPOINT,MySQL 和 PostgreSQL 行为一致但非真正嵌套事务。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
-
inner.RollbackTo("sp")只回滚到保存点,外层已执行的操作不受影响 -
outer.Rollback()会连带清除所有 savepoint,inner 状态作废 - 别混用
tx.SavePoint("sp")和原生tx.Exec("SAVEPOINT sp"),容易报"no such savepoint" - Savepoint 不提供跨事务可见性,也做不到“部分提交”——需要局部回滚时,优先考虑拆成独立事务 + 幂等重试
HTTP 调用、缓存写入、日志上报严禁进事务体
任何可能阻塞或超时的 I/O 操作放进事务,轻则拖垮连接池,重则导致整个服务不可用。
- HTTP 请求必须移出
db.Transaction函数体,改用tx.AfterCommit()异步触发,或走本地消息表 + 定时补偿 -
tx.AfterCommit()必须在事务函数体内注册,且只对当前tx实例生效;新建tx.WithContext()或传参错用db,钩子就失效 - 禁用默认事务(
SkipDefaultTransaction: true)可提 30%+ 写入性能,但仅适用于日志、埋点、缓存刷新等“尽力而为”场景;关了之后,db.Create()就是裸 SQL,约束冲突不会自动回滚
真正难的不是写 tx.Commit(),而是判断哪一步该提前 Rollback、哪个 SavePoint 失效了还去 ROLLBACK TO、或者 HTTP 调用卡在事务里拖垮连接池——这些细节不踩一次坑很难记住。

















