Polly重试策略生效需满足三要素:精准捕获异常(如503需HandleResult或HttpPolicyExtensions)、退避带jitter(用DecorrelatedJitterBackoffV2而非Math.Pow)、策略嵌套正确(熔断在外、重试在内、超时最外);且必须通过AddPolicyHandler注册并避免手动new HttpClient,否则策略不生效。

直接说结论:Polly 重试策略不是“加了就稳”,关键在异常捕获是否精准、退避是否带 jitter、策略是否嵌套得当——三者错一个,重试就可能变雪崩或静默失效。
Handle() 为什么经常漏掉 503?
HTTP 请求返回 HttpStatusCode.ServiceUnavailable(503)时,HttpClient 不抛异常,而是正常返回 HttpResponseMessage。只写 Handle<httprequestexception>()</httprequestexception> 完全捕获不到它。
- 必须显式用
HandleResult<httpresponsemessage>(r => r.StatusCode == HttpStatusCode.ServiceUnavailable)</httpresponsemessage>补上 - 更推荐一步到位:用
HttpPolicyExtensions.HandleTransientHttpError(),它已内置覆盖 5xx、408、429 和HttpRequestException - 若业务还定义了 JSON 错误码(如
{"code":500}),得自己解析响应体,再用OrResult扩展判断逻辑
WaitAndRetryAsync 的 sleepDurationProvider 别手写 Math.Pow
手动写 TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) 看似指数退避,但实际会触发“重试风暴”:所有客户端在第 1、2、3 秒整点同时重试,下游压力翻倍。
- 改用
Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(0.5), retryCount: 3) - 这个函数内部自动加随机抖动,避免同步重试
- 不要传
new Random()到 lambda 里——它在高并发下会生成重复种子,导致抖动失效
Policy.WrapAsync(retry, circuitBreaker) 的顺序不能反
把 circuitBreaker 放内层、retry 放外层,会导致熔断器统计不到重试过程中的失败次数——每次重试都算作一次独立调用,熔断永远不触发。
- 正确顺序是:
Policy.WrapAsync(circuitBreaker, retry)(熔断在外,重试在内) - 这样重试期间的所有失败都会被熔断器统一采样,failureThreshold 才有意义
- 如果还加了
TimeoutPolicy,它必须放在最外层:Policy.WrapAsync(timeout, circuitBreaker, retry)
HttpClientFactory 注册时没用 AddPolicyHandler 就白配了
直接 new HttpClient() 或从 DI 容器 resolve 出未注册策略的 IHttpClientFactory 实例,Polly 策略根本不会执行——策略只绑定在 AddPolicyHandler 链路上。
- 必须这样注册:
services.AddHttpClient("apiClient").AddPolicyHandler(GetResilientPolicy()) -
GetResilientPolicy()返回的是IAsyncPolicy<httpresponsemessage></httpresponsemessage>,不是Policy基类 - 别在
ExecuteAsync里手动 new HttpClient——它绕过了整个策略管道,等于没用 Polly
最易被忽略的一点:所有策略组合后,ExecuteAsync 抛出的异常类型变了。比如熔断触发时抛 BrokenCircuitException,而不是原始的 HttpRequestException;超时抛 TimeoutRejectedException。上层 try-catch 如果还只捕 HttpRequestException,就会漏掉这些关键失败信号。


















