必须用db.PingContext而非db.Ping,因其受context超时控制,能及时中断阻塞的dial尝试;重试前需校验ctx.Err()并立即退出,错误须分类处理,仅幂等操作可重试,且不可重复调用sql.Open。

启动时重试必须用 db.PingContext 而非 db.Ping
因为 db.Ping() 完全忽略 context 超时,即使外层 context.WithTimeout 已过期,它仍会阻塞直到 TCP 连接尝试彻底失败(可能长达 30 秒)。这会让整个服务卡在启动阶段,无法响应 cancel 信号。只有 db.PingContext(ctx) 才真正受 context 控制,能及时中断 dial 尝试。
- 每次重试前必须调用
db.PingContext(ctx),不是db.Ping() - ctx 必须由
context.WithTimeout(context.Background(), 30*time.Second)创建,且 defer cancel() 不可少 - 若
ctx.Err() != nil(如context.DeadlineExceeded),应立即返回错误,不 sleep、不进下一次循环 - 错误需分类处理:
driver.ErrBadConn、net.OpError(含"connection refused")可重试;"password authentication failed"等认证错误必须立刻退出
别在重试循环里反复调用 sql.Open
sql.Open 只校验 DSN 格式,不建连,返回的是连接池句柄 *sql.DB。它本就是线程安全、可复用的长期对象。反复调用不仅浪费资源,还可能因并发初始化导致连接池参数错乱(比如 SetMaxOpenConns 被覆盖)。
- 只在 main 初始化阶段调用一次
sql.Open,得到db *sql.DB - 把
db传给重试函数,重试逻辑只做PingContext + sleep + 错误判断 - 连接池配置(如
db.SetMaxOpenConns(20)、db.SetConnMaxLifetime(10*time.Minute))也必须在sql.Open后立即设置,不能拖到重试函数里 - DSN 中建议加
connect_timeout=3(PostgreSQL)或timeout=3s(MySQL),防止单次拨号卡死拖垮退避节奏
运行时连接断开靠连接池自动恢复,无需手动重试
启动后业务查询中遇到 "connection refused" 或 driver.ErrBadConn,通常不是数据库挂了,而是连接池里的某个连接被中间件回收、DB 主动断开或网络闪断。此时 *sql.DB 会自动丢弃坏连接、新建一个,只要你的查询是幂等的(如 SELECT、UPDATE WHERE id=...),上层几乎感知不到。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 业务层所有
db.Query/Exec必须检查 error,但不要自己写 for 循环重试 —— 连接池已内置重试逻辑 - 仅当 error ==
driver.ErrBadConn且操作幂等时,才考虑重试一次(例如封装一个safeQuery函数) - 对非幂等操作(如 INSERT 不带唯一约束、DELETE 无 WHERE),重试会导致数据重复或丢失,绝对禁止
- 如果频繁出现
driver.ErrBadConn,优先检查db.SetConnMaxIdleTime和db.SetConnMaxLifetime是否设得太长
指数退避要带 jitter,避免重试风暴
纯 1s→2s→4s 的退避在多实例同时启动时,容易造成所有服务在同一时刻发起连接请求,形成脉冲式探测压力,可能触发数据库端限流或拒绝连接。
立即学习“go语言免费学习笔记(深入)”;
- 基础退避时间乘以 [0.5, 1.5) 区间的随机因子,例如
time.Sleep(time.Duration(float64(base)*rand.Float64()*1.0)) - 最大重试次数建议设为 5~8 次,总超时控制在 20~30 秒内(Kubernetes readiness probe 通常设为 10~15 秒,需留余量)
- 首次 sleep 不要太短(低于 100ms),否则密集探测易被 DB 端识别为扫描行为
- 日志中记录每次重试的耗时和错误类型,方便区分是网络层问题(
dial tcp)还是认证层问题(password authentication failed)

















