在 Go 的 database/sql 中,若在同一个数据库连接上混合使用事务操作(如 tx.Exec)和非事务查询(如 db.Query),可能导致跨会话资源竞争,引发 PostgreSQL 死锁;正确做法是所有相关操作均通过事务对象(*sql.Tx)执行。
在 go 的 `database/sql` 中,若在同一个数据库连接上混合使用事务操作(如 `tx.exec`)和非事务查询(如 `db.query`),可能导致跨会话资源竞争,引发 postgresql 死锁;正确做法是所有相关操作均通过事务对象(`*sql.tx`)执行。
Go 的 database/sql 包采用连接池机制,*sql.DB 本身不表示单个物理连接,而是连接池的抽象;而 *sql.Tx 则绑定到池中某个独占的底层连接,并在整个事务生命周期内持有该连接。关键在于:db.Query()、db.Exec() 等方法从连接池获取任意空闲连接执行,与 tx 所持有的连接无关。
在你提供的代码中:
tx, _ := connection.Begin() // tx 绑定到某条专属连接 A tx.Exec(insert_statement) // 在连接 A 上执行 INSERT(未提交,行锁持续存在) rows, _ := connection.Query(select_statement) // 从池中另取连接 B 执行 SELECT // 若 select_statement 查询刚被 tx 插入但尚未提交的数据(如 SELECT ... FOR UPDATE 或涉及相同行的 MVCC 可见性判断), // 连接 B 可能因等待连接 A 释放锁而阻塞;而连接 A 又因 tx.Commit() 未执行无法释放锁 —— 形成典型跨连接死锁。 tx.Exec(insert_statement_2) // 永远不会执行 tx.Commit()
✅ 正确做法:*所有需与事务一致的读写操作,必须统一通过 `sql.Tx` 对象调用**:
tx, err := db.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()
_, err = tx.Exec("INSERT INTO logs(user_id) VALUES($1)", 123)
if err != nil {
return
}
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
⚠️ 注意事项:
- 绝不混用 db.XXX 与 tx.XXX 访问同一逻辑数据集,尤其当存在写后读、锁竞争(如 SELECT ... FOR UPDATE)、或依赖事务隔离级别的场景;
- tx.Query / tx.QueryRow / tx.Exec 均复用事务专属连接,保证原子性与一致性;
- 若需在事务外执行独立查询(如审计日志、监控指标),应确保其不依赖事务内未提交状态;
- 使用 defer tx.Rollback() 配合错误检查,避免事务泄露;
- PostgreSQL 的 idle_in_transaction_session_timeout 可辅助发现长期挂起的事务,但不能替代编码规范。
总结:这不是 database/sql 的 Bug,而是对连接抽象与事务语义理解偏差所致。牢记——*事务即连接上下文,所有关联操作必须路由至 `sql.Tx`**。遵循这一原则,即可彻底规避此类跨连接死锁。











