事务必须全程使用tx对象执行sql操作,混用db和tx会导致数据不一致;正确流程是db.begintx→defer tx.rollback()→执行→错误则return→成功则tx.commit();跨服务需saga或发件箱模式。

事务必须用 tx 对象执行所有 SQL 操作
混用 db 和 tx 是最常见、最隐蔽的数据不一致根源。比如在事务里调 db.Exec("UPDATE ..."),这条语句根本不在事务上下文中——它从连接池拿了新连接,独立提交,而 tx.Commit() 对它毫无影响。
- 所有操作必须显式走
tx.Query()、tx.Exec()、tx.Prepare() - GORM 用户尤其注意:钩子(
BeforeCreate等)里若调db.Create(),就掉出事务;必须传入当前tx实例:tx.Create(&u) - SQLite 无连接池概念,但依然要遵守该规则;PostgreSQL/MySQL 驱动下混用会直接导致锁残留或脏读
defer tx.Rollback() 是兜底,不是主逻辑
写 defer tx.Rollback() 是为了防 panic 或提前 return 漏掉回滚,但它不能替代错误分支里的显式 tx.Rollback()。更危险的是把 tx.Commit() 也塞进 defer ——一旦前面已 rollback,defer 还会再 commit,数据直接错乱。
- 正确顺序:
tx, err := db.BeginTx(ctx, opts)→defer tx.Rollback()→ 执行 SQL →if err != nil { return err }→return tx.Commit() - Go 1.21+ 可用
tx.RollbackUnlessCommitted()替代手动判空,老版本需自己设tx = nil后再 defer - 如果
tx.Rollback()返回 error(如连接已断),通常可忽略;重点是确保它被调用,而非它的返回值
微服务里 tx.Commit() 根本管不了其他服务
单体事务的原子性在跨服务时彻底失效。orderSvc 的 tx.Commit() 成功,不代表 inventorySvc 的扣库存也成功——它们之间没有共享事务上下文,HTTP/gRPC 调用天然脱离 ACID。
- 不要试图用 context 透传事务 ID 或“手动协调”远端行为,这既不原子也不隔离
- 可行路径只有两条:一是用 Saga(本地事务 + 补偿事件),二是发件箱模式(DB 写 + 消息表写 在同一
tx里) - 任何跨服务写操作,必须拆成「本地事务完成」→「发事件」两步;事件发送失败要重试,不能阻塞主流程
超时和并发是事务生命周期的实际杀手
默认 db.Begin() 没超时,一条卡住的 tx.Exec()(比如等行锁)会让整个事务挂起,连接池迅速耗尽。同时 *sql.Tx 非并发安全,跨 goroutine 复用会触发 sql: transaction has already been committed or rolled back。
- 务必用
db.BeginTx(ctx, &sql.TxOptions{}),配合context.WithTimeout(ctx, 5*time.Second) - MySQL 的
innodb_lock_wait_timeout(默认 50s)要和 Go 层超时协同,避免 Go 先放弃而 DB 还在等锁 - 每个请求独占一个
tx,别塞 channel、别传给 worker;异步动作(如发通知)必须在tx.Commit()之后启动
db 还是 tx 的那一行。跨服务时,这个边界直接消失——得靠状态机、日志表、幂等键这些“笨办法”把它重新焊回去。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











