在 go 的 database/sql 中,直接对全局 db 连接调用 query 会复用独立连接池中的连接,与事务 tx 所绑定的底层数据库会话无关,导致读写冲突或死锁;正确做法是始终通过事务对象(tx)执行所有相关操作。
在 go 的 database/sql 中,直接对全局 db 连接调用 query 会复用独立连接池中的连接,与事务 tx 所绑定的底层数据库会话无关,导致读写冲突或死锁;正确做法是始终通过事务对象(tx)执行所有相关操作。
Go 的 database/sql 包采用连接池机制管理数据库连接,而事务(*sql.Tx)本质上是独占绑定某一个底层物理连接的会话上下文。关键点在于:tx.Exec()、tx.Query()、tx.QueryRow() 等方法均复用该事务专属连接;而对全局 *sql.DB 对象(如 connection.Query())发起的查询,则从连接池中另取一个连接——该连接与事务所在会话完全隔离,既看不到未提交的变更,也无法规避锁竞争。
你遇到的死锁正是典型场景:
- tx.Exec(insert_statement) 在事务连接中插入数据(可能持有行锁或事务锁);
- connection.Query(select_statement) 使用另一连接尝试读取同一数据(例如 SELECT ... FOR UPDATE 或因 MVCC 可见性规则等待快照建立);
- 若 select_statement 涉及被 insert_statement 修改的行(尤其在 READ COMMITTED 隔离级别下),新连接可能因锁等待或快照冲突而阻塞;
- 最终 tx.Exec(insert_statement_2) 和 tx.Commit() 被挂起,形成跨连接的循环等待——即死锁。
✅ 正确写法:所有关联操作必须统一通过事务对象执行:
tx, err := connection.Begin()
if err != nil {
log.Fatal(err)
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
_, err = tx.Exec("INSERT INTO users(name) VALUES($1)", "alice")
if err != nil {
return
}
rows, err := tx.Query("SELECT id FROM users WHERE name = $1", "alice") // ✅ 使用 tx.Query
if err != nil {
return
}
defer rows.Close()
// 处理 rows...
_, err = tx.Exec("INSERT INTO logs(msg) VALUES($1)", "user created")
if err != nil {
return
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
⚠️ 注意事项:
- 绝不混用 *sql.DB 与 *sql.Tx 对同一逻辑单元的操作——这是并发安全与事务一致性的基本前提;
- 事务对象不可复用:tx 提交或回滚后即失效,再次使用将 panic;
- 连接池大小(SetMaxOpenConns)需合理配置,避免高并发下事务连接被耗尽;
- PostgreSQL 中可配合 SHOW TRANSACTION ISOLATION LEVEL 验证会话隔离级别,推荐生产环境使用 READ COMMITTED(默认)或根据业务需要升级至 REPEATABLE READ。
总结:事务的本质是“连接+状态+隔离”,Go 的 database/sql 明确将事务与连接绑定。所谓“新建连接”并非解决之道——而是应严格遵循“事务内操作全部走 tx.* 方法”的原则。这既是避免死锁的必要条件,也是保障 ACID 特性的底层契约。











