事务必须显式提交或回滚,否则连接永久占用;推荐使用GORM的Transaction()闭包自动兜底回滚,避免手动Begin()导致泄露;嵌套事务基于SavePoint,需谨慎管理;连接池参数须匹配事务生命周期,并配合context超时控制。

事务必须显式提交或回滚,否则连接永远卡住
调用 db.Begin() 后,GORM 返回一个 *gorm.DB 事务对象,它底层绑定了一个未释放的 *sql.Tx。这个事务一旦开启,就独占一个数据库连接,直到你调用 Commit() 或 Rollback() —— 如果没调,连接就不会归还给连接池,也不会被复用或关闭。
常见错误现象包括:
- 服务运行几小时后,MySQL 的
Threads_connected持续缓慢上涨,最终打满max_connections - 日志里反复出现
SQLSTATE[HY000] [1040] Too many connections - 用
SHOW PROCESSLIST查看,大量连接状态为Sleep,Command 是Sleep,Time 很长(几十分钟甚至数小时)
根本原因不是“忘了写 Commit”,而是:在异常路径(如 panic、return、error early exit)下,Rollback() 根本没被执行。
用 Transaction() 闭包封装,自动兜底回滚
GORM 内置的 Transaction() 方法是唯一推荐的事务入口。它内部做了 defer + recover,确保无论函数体是否 panic、是否提前 return、是否 error,事务都会被安全回滚。
正确写法:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err // 自动 rollback
}
if err := tx.Create(&profile).Error; err != nil {
return err // 自动 rollback
}
return nil // 自动 commit
})
错误写法(极易泄露):
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
// 忘了 defer tx.Rollback(),也没写 tx.Commit()
tx.Create(&user)
tx.Create(&profile)
// 这里如果 panic 或 return,tx 就永远卡住
关键点:
-
Transaction()是 GORM v1.20+ 的标准方式,不要自己手写Begin()+defer Rollback() - 闭包内任何
return err都会触发回滚;只有返回nil才提交 - 即使闭包里发生 panic,recover 机制也会捕获并执行
Rollback()
嵌套事务不等于新开连接,但 SavePoint 要手动管理
GORM 的“嵌套事务”实际是基于 SavePoint 实现的,不会新建数据库连接。但如果外层事务已开始,内层又调用 Transaction(),GORM 会复用当前连接并创建 savepoint —— 这本身不导致泄露,但若你在 savepoint 分支里漏掉 RollbackTo() 或逻辑错乱,可能让外层事务无法正常提交/回滚,间接延长连接占用时间。
典型风险场景:
- 外层事务中调用了一个封装了
Transaction()的函数,该函数内部 panic 但没被外层捕获,导致外层事务状态混乱 - 手动使用
tx.SavePoint("sp1")后,忘记调用tx.RollbackTo("sp1")或tx.Commit(),使事务长期挂起 - 多个 goroutine 共享同一个
*gorm.DB事务对象并发操作,引发竞争和状态不一致
建议:
- 避免手动管理 savepoint,优先用
Transaction()闭包隔离边界 - 事务对象不跨 goroutine 传递;每个 goroutine 应有自己的事务上下文
- 如需复杂控制,务必在所有分支路径(包括 defer、recover、error 处理)中显式处理 savepoint 状态
连接池配置必须配合事务生命周期做约束
即使事务逻辑完全正确,如果连接池参数不合理,仍可能放大泄露影响。比如 SetMaxOpenConns(100) 却有 200 个并发请求都卡在未完成事务上,那最多只有 100 个连接可用,其余请求会阻塞在 acquireConn 阶段,表现为高延迟或超时。
生产环境关键配置(作用于底层 *sql.DB):
-
SetMaxIdleConns(10):空闲连接上限,防止连接堆积 -
SetMaxOpenConns(50):总连接上限,应略高于预期峰值并发事务数 -
SetConnMaxLifetime(1h):强制重建老连接,避免因网络闪断导致的僵死连接 -
SetConnMaxIdleTime(30m):空闲太久的连接主动关闭(GORM v1.21+ 支持)
特别注意:SetConnMaxLifetime 对事务中的连接无效 —— 它只影响空闲连接。正在执行事务的连接,哪怕持续 2 小时,也不会被主动 kill。所以事务超时必须由业务层控制(如用 context.WithTimeout 包裹整个 Transaction() 调用)。
最常被忽略的一点:事务泄露往往不是单次错误,而是某个边缘 case(比如某类异常数据触发 panic)在低频下长期存在,慢慢把连接池耗尽。上线前必须用压测工具模拟异常路径(如强制 panic、网络中断、DB kill connection),验证事务是否真能全部清理干净。











