事务未显式提交或回滚导致连接卡在“in transaction”状态是最常见原因,需用defer tx.rollback()开头、成功时显式commit,并确保全程使用同一事务实例、避免全局db变量、移出耗时操作、合理配置连接池。

事务未显式提交或回滚,连接卡在“in transaction”状态
这是最常见原因:事务开启后,代码路径存在 panic、return 早于 tx.Commit() 或 tx.Rollback(),导致底层连接一直被占用,无法归还池中。MySQL 的 SHOW PROCESSLIST 里能看到大量 Command=Sleep 但 State=in transaction 的连接。
实操建议:
- 所有事务必须用
defer tx.Rollback()开头,再在成功路径上显式tx.Commit()—— 不要依赖if err != nil { tx.Rollback() }这种写法,panic 会跳过它 - 用
db.Session(&gorm.Session{PrepareStmt: true})创建的 session 必须确保其ConnPool和事务 db 一致,否则事务上下文丢失,Rollback()实际作用于新连接 - 调试时加一句
log.Println("in tx?", tx.Statement.ConnPool == nil),输出false才说明事务真正生效;若为true,说明你操作的是非事务实例
事务内调用了非传参的全局 *gorm.DB 实例
你在事务函数里直接用了包级变量 db(比如 models.DB.First(&u)),而不是把 tx *gorm.DB 作为参数传入——这会导致查询走的是普通连接池,和事务完全无关,而事务连接却被晾在一边,长时间空闲后触发 MySQL 的 wait_timeout 断连,变成 invalid connection 错误。
实操建议:
- 禁止在事务函数内部使用任何全局
*gorm.DB变量,包括db、models.DB等 - 统一用参数传递:例如
func transfer(tx *gorm.DB, fromID, toID uint, amount int) error - 如果必须复用逻辑,把业务函数改成接收
*gorm.DB接口,调用方决定传db还是tx
大事务中混入了耗时操作(HTTP 调用、文件 IO、长循环)
事务持有时间越长,连接被独占越久。一旦某个事务执行超过 MySQL 的 wait_timeout(默认 28800 秒,但很多生产环境设为 120–300 秒),连接会被服务端主动 kill,GORM 下次尝试用该连接时就报 invalid connection,且连接池不会自动剔除已断开的连接。
实操建议:
- 把 HTTP 请求、日志写入、批量计算等移出事务块,只保留 DB 写操作
- 对超长事务(如导出百万数据),改用
db.FindInBatches()分批 + 每批独立短事务,避免单事务锁表+占连接 - 给事务加 context 超时:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),并在关键位置检查ctx.Err()主动中断
连接池配置与事务并发不匹配
当高并发请求同时开启事务,而 SetMaxOpenConns() 设置过小(如默认 10),就会出现“想开事务但没连接可用”,部分 goroutine 卡在 db.Begin() 阻塞等待,另一些则抢到连接但迟迟不释放——最终连接池耗尽,新请求全部 hang 住。
实操建议:
- 设置
db.SetMaxOpenConns(100)(根据 QPS 和平均事务耗时估算,一般不低于并发峰值的 2 倍) -
SetMaxIdleConns建议设为和MaxOpenConns相近(如 80–100),避免连接频繁创建销毁引发TIME_WAIT泛滥 - 上线前用
ab或hey压测事务接口,观察SHOW STATUS LIKE 'Threads_connected'是否持续攀升不回落











