MySQL服务端断开连接时,database/sql常报invalid connection或driver: bad connection错误;应设SetConnMaxLifetime小于wait_timeout并加幂等重试。

MySQL连接被服务端断开时,database/sql 会报什么错
Go 的 database/sql 包本身不维护长连接,它用连接池复用底层连接。当 MySQL 服务端因 wait_timeout 或 interactive_timeout 主动关闭空闲连接后,下一次从连接池取出该连接执行查询,大概率触发 driver: bad connection 或 invalid connection 错误(具体文本取决于驱动,如 github.com/go-sql-driver/mysql 常返回 invalid connection)。
这不是网络层超时,也不是 DNS 解析失败,而是连接还“活着”但已被服务端标记为失效 —— 所以 ping 可能成功(TCP 连接未断),但 query 会失败。
- 别依赖
db.Ping()做前置检查:它只验证连接池里至少有一个可用连接,不保证你即将用的那个是有效的 - 错误不是每次必现:连接池可能刚巧分配了一个新连上的连接,掩盖问题
-
sql.ErrConnDone在较新驱动中可用于识别已关闭连接,但不能替代重试逻辑
用 db.SetMaxOpenConns 和 db.SetConnMaxLifetime 控制连接老化
靠“出错再重试”被动兜底不够稳。主动让连接在服务端超时前就退役,是最轻量的预防手段。
SetConnMaxLifetime 是关键:它强制连接在被创建后最多存活指定时间,到期后下次归还连接池时会被直接关闭。只要设得比 MySQL 的 wait_timeout 小(比如 MySQL 设了 300 秒,这里设 240 秒),就能避免拿到一个“服务端已关、客户端还不知道”的连接。
立即学习“go语言免费学习笔记(深入)”;
-
db.SetConnMaxLifetime(240 * time.Second):推荐值,留 60 秒缓冲 -
db.SetMaxOpenConns(20):防止突发请求打爆 MySQL 连接数,也间接减少空闲连接堆积 -
db.SetMaxIdleConns(10):配合MaxOpenConns控制空闲连接上限,避免资源浪费 - 注意:
ConnMaxLifetime是连接“出生后”的绝对存活时间,和是否空闲无关
写查询时加一层简单重试,别等框架帮你兜底
即使做了连接老化,网络抖动、主从切换、MySQL 重启仍可能导致单次操作失败。Go 标准库不自动重试,必须自己处理。
重试不是无脑循环。重点是区分可重试和不可重试错误:网络类、连接类错误(如 invalid connection、i/o timeout、connection refused)可以重试;主键冲突、唯一索引违例、语法错误这类业务/逻辑错误重试没意义。
- 用
errors.Is(err, sql.ErrConnDone)或字符串匹配"invalid connection"判断是否值得重试 - 最多重试 2 次,间隔用指数退避(如 10ms → 30ms),避免雪崩
- 写法示例:
for i := 0; i < 3; i++ {
err := db.QueryRow("SELECT id FROM users WHERE name = ?", name).Scan(&id)
if err == nil {
return id, nil
}
if !isRetryableError(err) {
return 0, err
}
if i == 2 {
return 0, err
}
time.Sleep(time.Duration(math.Pow(2, float64(i))) * 10 * time.Millisecond)
}事务里遇到断连,不能只靠重试
事务内连接中断更危险:可能部分语句已提交,重试整个事务会导致重复执行(比如扣款两次)。这时候光靠重试逻辑会放大问题。
正确做法是把事务控制权收回来:用 db.BeginTx 显式开启,并在 defer 中判断 tx.Rollback() 是否成功。如果事务中途断连,tx.Commit() 必然失败,此时应记录日志并人工介入,而不是自动重试。
- 事务中禁止使用
db.Query等全局方法,所有操作必须走tx.Query - 一旦
tx.Commit()返回非nil错误,不要假设数据一致,也不要静默重试 - 对强一致性要求高的场景(如支付),建议结合幂等 key + 状态表,而非依赖连接层重试
连接断开本身不难处理,难的是判断哪次失败能安全重试、哪次必须放弃。很多人卡在“重试逻辑写了但线上还是丢数据”,问题往往出在事务边界没理清,或者把连接错误和业务错误混在一起判定了。










