Go 事务开始后 Prepare 卡住:连接池只剩事务占用的连接
Go 事务占用唯一连接后,通过 DB.Prepare 再取连接会一直等待。使用事务上的 PrepareContext,并核对懒缓存、取消和提交结果。
Go 应用把 db.SetMaxOpenConns(1) 设为 1,db.Begin 成功后紧接着调用 db.Prepare,程序却一直卡住,也没有数据库返回的死锁错误。先检查事务是否已经占用了唯一连接,而 Prepare 又通过 sql.DB 请求另一条连接。
sql.DB 管理连接池,sql.Tx 使用事务的专用连接。对 DB 发起操作,不会因为同一函数里已有 tx 就自动加入该事务。后续提交本来需要等 Prepare 返回,Prepare 又需要等事务释放连接,这种应用内等待可以一直持续。
从调用顺序确认等待位置
核对连接上限、事务开始点、准备语句的接收者,以及所有错误和返回路径。以下是有问题的结构片段,省略部分错误处理只为展示调用顺序:
db.SetMaxOpenConns(1)
tx, err := db.Begin()
// 这里已经检查 err
stmt, err := db.Prepare(insertSQL)
// 后续才准备 tx.Stmt(stmt) 和 tx.Commit()
sql.DB 的 Prepare 需要池中可用连接。tx 占着唯一连接时,这次请求只能等待;尚未走到后面的 tx.Stmt,不会反过来改变前面 Prepare 获取连接的方式。
可以在受控诊断中结合 goroutine 堆栈和 db.Stats() 观察连接使用与等待趋势。InUse 接近上限、Idle 为零,只证明没有空闲容量;还需对照调用顺序,确认持有者就是等待操作本身的事务。其他长查询或连接未归还也可能产生类似等待。
服务器没有锁等待,不排除应用正在池中排队。此时增加数据库锁等待超时,未必约束客户端取得连接的等待时间。
在事务连接上准备并执行语句
属于该事务的操作应使用 tx.PrepareContext、tx.ExecContext 等方法。以下局部示例假设 db 是已初始化的 MySQL 连接池,SQL 与参数已按实际表结构确认:
func writeItem(ctx context.Context, db *sql.DB, name string) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.PrepareContext(ctx, "INSERT INTO items(name) VALUES (?)")
if err != nil {
return err
}
defer stmt.Close()
if _, err := stmt.ExecContext(ctx, name); err != nil {
return err
}
return tx.Commit()
}
context、database/sql 的导入及项目现有错误处理应保留。问号占位符适用于这里的 MySQL 示例,其他驱动应使用自己的占位语法。这个函数会写入并提交数据,仅在项目正常的验证环境使用;不要在生产表中随意试插入。
事务准备出的语句在提交或回滚后会关闭。只有一次执行时,也可按项目需要直接使用 tx.ExecContext,不必为了修复等待额外增加全局语句缓存。
如果确需复用 DB 上已存在的预备语句,应先完成其准备,再开始事务,并通过 tx.StmtContext 得到事务对应的语句。不能让缓存的懒初始化在事务已占满连接后又调用 db.Prepare;缓存命中和未命中两条路径都需要验证。
有限等待与连接容量分别处理
为操作传入有合理期限、可以取消的 context,让池中等待具备退出条件。PrepareContext 的 context 控制准备阶段,执行仍应使用相应的 ExecContext。BeginTx 的 context 则贯穿事务生命周期,取消时会影响事务及提交。
数据库驱动是否支持正在执行的查询取消,需要按驱动核对;不能承诺任意阻塞都会立即停止。取消和超时是失败处理边界,不会把这条 DB/Tx 混用路径变成正确事务。
把上限从 1 改为 2,可能暂时让这段串行代码通过,但并发请求各自持有事务后,仍可能再次耗尽连接。更重要的是,DB 上执行的其他 SQL 可能在事务之外产生读写结果。应先修复事务接收者与释放路径,再按全应用和服务器容量安排连接预算。
用同样的受限池验收
修改前保存连接池和语句缓存配置。在隔离数据中,保留低连接上限,覆盖首次缓存未命中、重复执行、并发、SQL 失败和 context 取消。确认原 Prepare 不再因事务占用而等待,写入只在成功提交后生效,错误路径能结束事务并归还资源。
不要只根据函数终于返回就认定写入成功,Commit 的错误也必须处理;提交状态不明确时,不应盲目重复非幂等操作。回退代码不会撤销已提交的数据,也不需要通过重启数据库或删除数据目录处理这类客户端调用顺序问题。


