示例说明
固定间隔重试在下游过载时火上浇油;纯指数退避会让全部客户端在同一时刻齐射。抖动是解药——每次退避随机减去零到一半的时长,重试时间点在时间轴上散开。分类同样关键:参数错误重试一万次还是失败,只有瞬态错误值得等待重试。
技术要点
- 位移运算实现指数增长并封顶
- 全抖动打散多客户端重试相位
- select 同时响应超时与取消
代码解读
backoff 用左移计算二的次幂倍数,溢出或超限都封顶到上限值;抖动区间是延迟的一半。withRetry 每轮先查 context,执行后分类——永久错误直接返回带上下文的包装错误。等待用 select 双路:到时或被取消 whichever first。主函数模拟前两次失败第三次成功。
使用建议
抖动模式有全抖动与等抖动两种,后者对下游更温和。重试要配配额防雪崩。错误分类与错误码体系对齐而非匹配文案。
代码示例
package main
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
)
var (
errTransient = errors.New("transient upstream failure")
errPermanent = errors.New("invalid argument")
)
func classify(err error) bool {
if errors.Is(err, errPermanent) {
return false // 永久错误不值得重试
}
return errors.Is(err, errTransient) || isNetworkErr(err)
}
func isNetworkErr(err error) bool {
var netErr interface{ Timeout() bool }
return errors.As(err, &netErr)
}
func backoff(attempt int, base, cap time.Duration) time.Duration {
exp := base << attempt // base * 2^attempt
if exp > cap || exp <= 0 {
exp = cap
}
jitter := time.Duration(rand.Int63n(int64(exp) / 2))
return exp - jitter // 全抖动:避免多客户端同步重试
}
func withRetry(ctx context.Context, fn func() error, maxAttempts int) error {
var lastErr error
for attempt := 0; attempt < maxAttempts; attempt++ {
if err := ctx.Err(); err != nil {
return fmt.Errorf("context done: %w", err)
}
if err := fn(); err == nil {
return nil
} else {
lastErr = err
if !classify(err) {
return fmt.Errorf("permanent failure: %w", err)
}
}
if attempt < maxAttempts-1 {
wait := backoff(attempt, 100*time.Millisecond, 5*time.Second)
select {
case <-time.After(wait):
case <-ctx.Done():
return ctx.Err()
}
}
}
return fmt.Errorf("gave up after %d attempts: %w", maxAttempts, lastErr)
}
func main() {
calls := 0
err := withRetry(context.Background(), func() error {
calls++
if calls < 3 {
return errTransient
}
return nil
}, 5)
fmt.Println(err, "calls:", calls)
}