backoff/v4 初始化必须每个请求独立新建实例,禁用共享;需启用抖动防重试风暴;只对临时性错误(如5xx、连接超时)重试,永久性错误(如4xx、context.canceled)立即退出;全程贯穿context超时控制。

backoff/v4 初始化必须每个请求独立
共享 backoff.BackOff 实例是 Go 重试中最隐蔽的坑。它内部维护 attempt 计数器和上一次返回的延迟,多个 goroutine 并发调用 NextBackOff() 会导致计数错乱、退避时间崩坏——比如本该第 2 次重试却返回了第 5 次的间隔。
- 每次重试前都应新建实例:
bo := backoff.NewExponentialBackOff() - 若需复用配置(如
InitialInterval、MaxElapsedTime),用backoff.WithXXX函数包装基础策略,而非复用实例 - 别在全局变量或 struct 字段里存
backoff.BackOff,除非你明确控制单例生命周期且无并发
抖动(jitter)不是可选项,而是防重试风暴的刚需
纯指数退避(100ms → 200ms → 400ms)在分布式场景下极易引发“重试风暴”:成百上千个客户端在同一毫秒发起重试,瞬间压垮刚恢复的服务节点。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须启用抖动:
bo := backoff.WithJitter(backoff.NewExponentialBackOff()) - 底层原理是每次计算出基准延迟后,叠加 ±25% 随机偏移,例如基准 400ms → 实际等待 300–500ms
- 用
rand.New(rand.NewSource(time.Now().UnixNano()))创建局部*rand.Rand,避免全局rand包被并发写入 panic
只对临时性错误重试,4xx 和 context 错误要立刻退出
重试不是万能胶布,对永久性错误重试只会浪费资源、拖慢响应、污染日志。HTTP 状态码、网络错误类型、context 状态必须分类处理。
- 允许重试的典型错误:
net.OpError(连接拒绝、超时)、context.DeadlineExceeded(但注意:这是本次请求超时,重试需新ctx)、HTTP 5xx 或 429 - 禁止重试的典型错误:
http.ErrHandlerTimeout(服务端已超时)、sql.ErrNoRows、HTTP 400/401/403/404、context.Canceled(上游已放弃) - 封装统一判断函数:
shouldRetry(err error) bool,把规则收口,别散落在各处
context 超时必须贯穿整个重试链路,不能只靠重试次数
仅设 maxRetries=3 不够——若每次失败都等 30 秒,总耗时可能达 90 秒,远超 SLA。真实系统中,必须用 context.WithTimeout 控制整体生命周期,并在每次循环中 select 监听取消信号。
- 初始化时就创建带超时的
ctx:ctx, cancel := context.WithTimeout(parentCtx, 30*time.Second),并在 defer 中调用cancel() - 每次循环开头用
select { case ,确保上游取消能立即中断重试 -
backoff.MaxElapsedTime是辅助项,不能替代 context;它只约束退避逻辑本身,不阻断外部取消
backoff.BackOff ——这三者漏掉任一环,线上高并发下就会出现重试时间漂移、goroutine 堆积、下游雪崩。










