结论:事务必须全程用tx对象执行所有sql操作,混用db会导致部分语句脱离事务、数据不一致;db.transaction闭包是首选方案,它自动处理panic回滚、成功提交和连接归还。

直接说结论:事务必须全程用 tx 对象执行所有 SQL 操作,混用 db 会导致部分语句脱离事务、数据不一致;db.Transaction 闭包是首选方案,它自动处理 panic 回滚、成功提交和连接归还。
为什么 db.Exec 不进事务?
因为 database/sql 的 db 是连接池抽象,每次 db.Exec 可能从池里拿到全新连接,而事务只绑定在单个底层连接上。一旦你在事务里调 db.Query 或 db.Exec,那条语句就跑在另一个连接上,独立提交——tx.Commit() 对它完全无效。
- 常见错误现象:
tx.Commit()返回 nil,但查库发现只有部分写入生效(比如订单写了,库存没扣) - 所有操作必须显式走
tx.Query、tx.Exec、tx.Prepare - GORM 用户尤其注意:模型钩子(如
BeforeCreate)里若调db.Create(),就掉出事务;必须传tx.Create()
怎么写才不会漏 rollback?
defer tx.Rollback() 是兜底手段,不是主逻辑。它防的是 panic、提前 return 或嵌套 error 分支遗漏,但不能替代显式错误处理。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确顺序:
tx, err := db.BeginTx(ctx, opts)→defer tx.Rollback()→ 执行 SQL →if err != nil { return err }→return tx.Commit() - 别把
tx.Commit()塞进 defer:万一前面已 rollback,defer 还会再 commit,数据直接错乱 - Go 1.21+ 推荐用
tx.RollbackUnlessCommitted(),老版本需手动设tx = nil后再 defer -
tx.Rollback()返回 error(如连接断开)通常可忽略,重点是它被调用了
高并发下事务容易卡死?
默认 db.Begin() 没超时,一条等锁的 tx.Exec() 会让整个事务挂起,连接池迅速耗尽。MySQL 的 innodb_lock_wait_timeout(默认 50s)和 Go 层超时必须协同,否则 Go 先放弃而 DB 还在等锁。
- 务必用
db.BeginTx(ctx, &sql.TxOptions{}),配合context.WithTimeout(ctx, 5*time.Second) - 驱动超时参数要显式配置:
timeout=3s&readTimeout=2s,避免依赖 context 包裹(老驱动可能忽略) -
*sql.Tx非并发安全,禁止跨 goroutine 复用,否则触发sql: transaction has already been committed or rolled back - 对库存、余额等场景,加
SELECT ... FOR UPDATE显式行锁,但要注意死锁风险
微服务或双写场景怎么办?
tx.Commit() 在跨服务或跨库时彻底失效——它只管得住本机一个 *sql.DB。你没法靠它让订单服务和库存服务“一起成功或一起失败”。
- 跨服务必须拆成「本地事务完成」→「发事件」两步,推荐发件箱模式(DB 写 + 消息表写 在同一
tx里) - 双写失败时,
database/sql无法跨库回滚,必须设计幂等、重试、对账机制,而不是靠内存标记或 sync.Once - HTTP、Redis、文件等外部调用绝不能放事务体里;GORM 的
tx.AfterCommit()是唯一安全出口 - Repository 层必须支持
WithTx()参数传递,不能依赖全局db实例“猜”事务上下文
最隐蔽的坑不是语法写错,而是某处悄悄混用了 db 和 tx;最危险的不是没写 rollback,而是写了却没覆盖所有分支。事务一致性不靠“差不多”,而靠每一步都显式契约化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










