结论:事务必须用 tx 对象执行所有操作,混用 db 会导致部分语句脱离事务;需用 context 控制超时,隔离级别按需设置;Rollback 应 defer 调用以防遗漏。

直接说结论:用 db.Begin() 开启事务,tx.Commit() 或 tx.Rollback() 显式结束,中间所有操作必须用 tx 对象(不能用原始 db)。
为什么 db.Query 不会自动进事务?
因为 database/sql 的 db 是连接池抽象,每次 db.Query 可能拿到不同底层连接,事务只在单个连接内有效。一旦你混用 db 和 tx,比如在事务里调 db.Exec,那条语句根本不在事务上下文中,提交时完全不感知——数据就“漏”出去了。
常见错误现象:tx.Commit() 成功返回,但部分写入已生效,部分没写,查数据库发现状态不一致。
- 所有 SQL 操作(
Query、Exec、Prepare等)必须调tx.Query、tx.Exec -
tx本身不是线程安全的,别跨 goroutine 复用 - 事务超时或连接中断时,
tx.Commit()会返回 error,但此时可能已部分提交(取决于驱动和数据库),所以Rollback()要在 defer 里兜底
tx.Rollback() 必须在 defer 里调吗?
不是“必须”,但几乎总是该这么做。因为事务结束只有两个合法出口:成功 Commit,或出错 Rollback。手动写 if err != nil { tx.Rollback() } 很容易漏掉 panic、提前 return、或者嵌套 error 处理里的分支。
立即学习“go语言免费学习笔记(深入)”;
使用场景:任何涉及多步 DB 操作且要求原子性的逻辑,比如“扣库存 + 写订单 + 记日志”。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 推荐模式:
defer tx.Rollback()放在Begin()后立刻执行,等Commit()成功后再用tx = nil阻止 rollback - 如果
tx.Rollback()返回 error,通常说明连接已断,此时再记日志也没用,忽略即可(除非你要做连接恢复) - 注意:Go 1.21+ 支持
tx.RollbackUnlessCommitted(),更安全,但老版本得自己判tx是否为 nil
事务里能用 context.Context 控制超时吗?
可以,而且强烈建议。默认事务没有超时,如果某条语句卡住(比如锁等待、网络抖动),整个事务会一直挂起,拖垮连接池。
参数差异:db.BeginTx(ctx, opts) 才支持传 context.Context;db.Begin() 是无 context 版本,等价于 db.BeginTx(context.Background(), nil)。
- 传入带 timeout 的 context(如
context.WithTimeout(ctx, 5*time.Second)),一旦超时,后续tx.Exec会立即返回context.DeadlineExceeded - 超时后
tx.Commit()必然失败,但tx.Rollback()仍可尝试执行(不过连接可能已失效) - MySQL 默认
innodb_lock_wait_timeout=50秒,和 Go 层 context 超时要配合设置,避免 Go 先放弃而 DB 还在等锁
并发读写时,tx 的隔离级别怎么设?
不显式设置就是数据库默认隔离级别(MySQL 是 REPEATABLE READ,PostgreSQL 是 READ COMMITTED)。需要更高一致性时,得在 BeginTx 时传 &sql.TxOptions{Isolation: sql.LevelSerializable}。
性能影响明显:SERIALIZABLE 会加范围锁或升级为锁表,QPS 掉得厉害;READ UNCOMMITTED 在 Go 里基本不用,因为 database/sql 不支持(驱动层会拒绝)。
- 绝大多数业务用默认级别足够,比如“查余额 → 扣款 → 更新余额”,REPEATABLE READ 能防止中间被篡改
- 如果遇到幻读(比如分页查数据时另一事务插入新行),才考虑升到
LevelRepeatableRead或LevelSerializable - 注意:SQLite 不支持自定义隔离级别,
Isolation参数会被忽略
真正难的不是写对 tx.Commit(),而是判断哪些操作**必须塞进同一个事务**、哪些可以拆开异步做。比如发通知、更新缓存这类副作用,通常不该堵在事务里——它们失败不该导致主流程回滚,但又得保证最终一致。这个边界,比语法细节更常出问题。

















