HTTP客户端重试必须显式控制,优先封装RoundTripper而非业务层for循环,因其可复用、可组合、可统一埋点,且能基于具体错误类型(如net.OpError)和状态码(如5xx)精准判断是否重试。

http.Client 默认不重试,grpc.ClientConn 默认也不重试——这不是缺陷,是设计选择。重试必须显式控制,否则要么漏掉容错,要么制造重试风暴、重复扣款、goroutine 泄漏。
HTTP 客户端该用 RoundTripper 还是业务层 for 循环?
优先封装 RoundTripper,而不是在每个 Do() 外套 for 循环。
- 业务层写
for容易混入超时判断、body 重放、错误分类逻辑,后续加监控或换退避策略就得动多处业务代码 -
RoundTripper可复用、可组合、可统一埋点;比如只对POST /api/transfer启用重试,而GET /health绕过 - 别直接改
http.DefaultClient.Transport:健康检查、指标上报等请求也会被重试三次,放大下游压力 - 正确做法是新建
&http.Transport{RoundTripper: originalRT},只拦截RoundTrip()返回的错误和响应码 - 判断依据必须具体:
err.(net.OpError)或err.(url.Error).Timeout()才算可重试网络错误;resp.StatusCode >= 500 && resp.StatusCode 才考虑重试,但要排除 <code>501 Not Implemented
gRPC 客户端重试配置了为啥还不生效?
三个条件缺一不可,漏一个就等于没配。
- 必须传入拦截器:
grpc.WithChainUnaryInterceptor(grpc_retry.UnaryClientInterceptor())(注意不是WithUnaryInterceptor) - 必须显式指定可重试码:
grpc_retry.WithCodes(codes.Unavailable, codes.ResourceExhausted, codes.DeadlineExceeded);codes.NotFound和codes.InvalidArgument不会触发重试 - proto.Message 必须可重复序列化:字段不能含
sync.Mutex、unsafe.Pointer等不可序列化类型,否则重试时 panic - 别以为
grpc.WithTimeout自带重试——它只控制单次 RPC 超时,和重试无关 - 重试会重新编码请求体,所以
idempotency_key字段必须在每次重试时保持一致(不能靠随机生成)
backoff.Retry 和 backoff.WithContext 的核心区别
backoff.Retry 是“无上下文重试”,backoff.WithContext 才是生产可用的起点。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
-
backoff.Retry(fn, b)完全忽略ctx.Done():哪怕外层 context 已Cancel,它仍会跑完全部次数,sleep 也会卡死 goroutine - 必须用
backoff.WithContext(b, ctx)包装,让每次b.NextBackOff()都检查上下文状态 - 每次重试前必须新建
context.WithTimeout(ctx, timeout):复用已 cancel 的 ctx 会导致空转和泄漏 -
retry.Attempts(3)表示总共执行 3 次(首次 + 2 次重试),想最多重试 3 次得写retry.Attempts(4);backoff.WithMaxRetries(b, 3)更直白,明确表示“最多重试 3 次” - 别依赖默认值:
backoff.NewExponentialBackOff()的MaxInterval = 1 * time.Second太小,第 4 次重试就被截断;建议显式设b.MaxInterval = 30 * time.Second
抖动(jitter)不是锦上添花,是防止雪崩的刚需
纯指数退避在高并发下必然导致重试脉冲——所有客户端在同一毫秒发起重试,刚恢复的服务瞬间被打挂。
- 抖动范围推荐
[0.5, 1.5):延迟 =base * math.Pow(2, float64(attempt)) * (0.5 + rand.Float64()*0.5) - 别用全局
rand.Rand:多 goroutine 共享 seed 会导致抖动失效;每次重试应使用独立rand.New(rand.NewSource(time.Now().UnixNano())) - 抖动必须和 context 绑定:每次等待都用
select { case ,不能裸调 <code>time.Sleep - 服务端幂等性是底线:HTTP 带
X-Idempotency-Key,gRPC 在 proto 中加string idempotency_key = 1;;没这个,重试就是危险操作
真正难的不是写个重试循环,而是判断「此刻该不该重试」——这需要穿透错误类型、响应码、业务语义三层。多数故障不是卡在重试没跑起来,而是重试跑多了,把临时抖动变成了雪崩。

















