实现重试机制
问题
如何用 Go 实现一个健壮的重试机制?
答案
基础重试
func Retry(attempts int, sleep time.Duration, fn func() error) error {
for i := 0; i < attempts; i++ {
if err := fn(); err == nil {
return nil
} else if i < attempts-1 {
log.Printf("第 %d 次失败,%v 后重试: %v", i+1, sleep, err)
time.Sleep(sleep)
} else {
return fmt.Errorf("重试 %d 次后失败: %w", attempts, err)
}
}
return nil
}
指数退避 + 抖动(推荐)
type RetryConfig struct {
MaxAttempts int
InitDelay time.Duration
MaxDelay time.Duration
Multiplier float64 // 退避倍数(通常 2)
}
func RetryWithBackoff(ctx context.Context, cfg RetryConfig, fn func() error) error {
delay := cfg.InitDelay
for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}
// 最后一次不再等待
if attempt == cfg.MaxAttempts-1 {
return fmt.Errorf("重试 %d 次后失败: %w", cfg.MaxAttempts, err)
}
// 加抖动:避免多个客户端同时重试(惊群效应)
jitter := time.Duration(rand.Int63n(int64(delay) / 2))
sleepDuration := delay + jitter
if sleepDuration > cfg.MaxDelay {
sleepDuration = cfg.MaxDelay
}
log.Printf("第 %d 次失败,%v 后重试: %v", attempt+1, sleepDuration, err)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(sleepDuration):
}
delay = time.Duration(float64(delay) * cfg.Multiplier)
}
return nil
}
// 使用
func main() {
cfg := RetryConfig{
MaxAttempts: 5,
InitDelay: 100 * time.Millisecond,
MaxDelay: 10 * time.Second,
Multiplier: 2.0,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := RetryWithBackoff(ctx, cfg, func() error {
return callExternalAPI()
})
}
可选重试(按错误类型判断)
// 只重试临时性错误
type RetryableError interface {
Retryable() bool
}
func RetryOnRetryable(ctx context.Context, attempts int, fn func() error) error {
for i := 0; i < attempts; i++ {
err := fn()
if err == nil {
return nil
}
// 检查是否可重试
var retryable RetryableError
if errors.As(err, &retryable) && !retryable.Retryable() {
return err // 不可重试,直接返回
}
// 网络超时也可重试
if errors.Is(err, context.DeadlineExceeded) {
continue
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Duration(i+1) * time.Second):
}
}
return fmt.Errorf("重试次数用尽")
}
HTTP 客户端重试
func HTTPGetWithRetry(ctx context.Context, url string, maxRetries int) (*http.Response, error) {
var lastErr error
delay := 500 * time.Millisecond
for i := 0; i <= maxRetries; i++ {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
} else {
lastErr = err
}
if i < maxRetries {
jitter := time.Duration(rand.Int63n(int64(delay)))
time.Sleep(delay + jitter)
delay *= 2
}
}
return nil, lastErr
}
退避策略对比
| 策略 | 公式 | 特点 |
|---|---|---|
| 固定间隔 | d | 简单,可能惊群 |
| 线性退避 | d * n | 逐步拉长 |
| 指数退避 | d * 2^n | 快速拉长 |
| 指数+抖动 | d * 2^n + random | 避免惊群(推荐) |
常见面试问题
Q1: 重试和幂等性的关系?
答案:重试必须配合幂等性设计。如果接口不幂等(如扣款),重试可能导致重复执行。确保被调用方支持幂等(唯一请求 ID、数据库唯一约束等)。
Q2: 什么情况不应该重试?
答案:
- 4xx 错误(客户端参数错误,重试也不会成功)
- 业务逻辑错误(余额不足、权限不够)
- 非幂等操作(无法保证幂等时不要重试)