服务启动时需阻塞重试等待数据库就绪,使用带超时和指数退避的db.PingContext(),仅对可恢复错误重试;运行时依赖连接池参数(如SetConnMaxLifetime、SetMaxIdleConns)而非手动Ping来应对连接失效。

启动阶段必须阻塞重试,不能靠运行时自动恢复
服务刚起来时数据库还没 ready(比如 Docker Compose 启动顺序错乱、K8s InitContainer 延迟),sql.Open 无感,但第一次 db.PingContext() 必然失败。此时若直接 panic 或返回 error,整个服务就起不来。必须主动等,但不能裸写 for { db.Ping(); time.Sleep() }。
- 用
context.WithTimeout(context.Background(), 30*time.Second)控制总等待窗口 - 每次重试前加指数退避:1s → 2s → 4s → 8s,避免打爆 DB 健康检查端点
- 只对可恢复错误重试:
timeout、dial tcp、connection refused;遇到password authentication failed这类认证错误应立刻退出 - 重试函数接收已初始化的
*sql.DB,绝不重复调用sql.Open
运行时连接中断靠连接池参数,不是靠手动 Ping
db.Ping() 成功不代表后续 db.Query() 就安全——它只探测池中一个空闲连接,而池里其他连接可能已被 MySQL 主动断开(如 wait_timeout=28800 后失效)。靠定时 ping 不仅无效,还会制造额外负载。
- 设
db.SetConnMaxLifetime(4 * time.Minute):强制连接在创建后 4 分钟内被回收,比 MySQL 默认wait_timeout短,避免复用陈旧连接 - 配
db.SetMaxIdleConns(10)和db.SetMaxOpenConns(25):前者防堆积将死连接,后者防连接数爆炸;生产建议MaxOpen = 2–3 × QPS峰值 - DSN 中
timeout、readTimeout、writeTimeout有用,但failover=true或retry=true是无效字段,驱动根本不识别
错误分类比重试更重要
不是所有 error 都该重试。盲目 retry 会掩盖真实问题,甚至把临时网络抖动放大成雪崩。
- 该重试:
driver.ErrBadConn、io: read/write timeout、use of closed network connection - 不该重试:
ERROR 1045 (28000): Access denied for user、ERROR 1049 (42000): Unknown database、SQL 语法错误等业务/配置类错误 - 执行查询时要用
db.QueryContext(ctx, ...)而非db.Query(),确保超时和 cancel 可穿透到底层连接 - 幂等性必须由业务层保障:重试可能造成同一条 INSERT 执行多次,DB 层不负责去重
别封装 SafeQuery 包住所有 SQL 调用
有人喜欢写个 SafeQuery(db, ctx, sql, args...) 函数,在里面自动 retry + ping。这看似省事,实则埋雷:
立即学习“go语言免费学习笔记(深入)”;
- 无法区分是连接问题还是 SQL 逻辑错误,把
WHERE id = ?写成WHERE id == ?也会被无限重试 - 掩盖了错误发生的真实上下文,日志里只剩 “query failed”,查不到是哪条语句、哪个参数触发的
- 重试粒度太粗:一次 HTTP 请求含多个 DB 操作,某个 SELECT 失败不该让整个请求重来
- 真正该做的是:在 handler 层捕获 error,按类型决定是否重试、降级或返回 503;连接池参数+健康配置已覆盖 90% 的运行时抖动
连接池里的“坏连接”不会自己消失,也不会被 ping 清掉——它们安静地躺在那里,直到某次 Query 把它捞出来,才爆出 invalid connection。所以配置 SetConnMaxLifetime 比写一百行重试逻辑更治本。


















