结论:应使用 waitandretryasync 配合带 jitter 的退避函数,而非 retryasync 或手写 math.pow(2, n),因后者在并发下易引发雪崩式重试且无法应对下游瞬时过载。

直接说结论:用 WaitAndRetryAsync 配合带 jitter 的退避函数,而不是 RetryAsync 或手写 Math.Pow(2, n) —— 后两者在并发下极易引发雪崩式重试,且无法应对下游瞬时过载。
为什么 RetryAsync(3) 不能用于生产 HTTP 请求
RetryAsync(3) 表示失败后立即重试 3 次,中间无任何延迟。它不接受时间参数,也不做退避计算。
- 网络抖动或服务短时不可用时,这种“秒级三连击”会把压力原样放大三倍,下游可能还没恢复就被打挂
- 所有客户端在同一毫秒触发重试,形成请求峰谷同步(即“重试风暴”)
- 它只适用于极轻量、纯内存逻辑(如读取本地配置文件失败),且必须确保异常是真正瞬时的
WaitAndRetryAsync 怎么写才防雪崩
关键不是“指数”,而是“带随机扰动的指数”。别自己算 TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) —— 这仍是确定性间隔,高并发下照样撞车。
- 生产环境首选
Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(0.5), retryCount: 3),它来自Polly.Extensions.Http,需单独安装包 - 手动实现 jitter 也行,但必须每次生成独立随机值:
retryAttempt => TimeSpan.FromMilliseconds(100 * Math.Pow(2, retryAttempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(50, 150)) - 数组形式(如
new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(4) })适合调试,但无法适应动态负载,上线前务必换成 jitter 版
文件 IO 重试为什么总失败
文件操作(如 File.ReadAllText)被其他进程锁定时,IOException 会在毫秒级复现。固定间隔重试只是反复撞墙。
- 必须显式捕获
IOException和UnauthorizedAccessException,仅Handle<exception>()</exception>不够精准 - 每次重试必须新建
FileStream,且带FileOptions.Asynchronous和合理缓冲区(如 4096),否则句柄残留会导致后续重试持续失败 - 退避不能太激进:首延迟建议从
TimeSpan.FromMilliseconds(50)起步,而非秒级——文件锁释放通常很快,等太久反而拖慢整体流程
HttpClient 重试策略不生效的三个硬条件
Polly 策略不是“加了就跑”,HTTP 场景下必须同时满足:
- 异常捕获要覆盖真实失败路径:仅
Handle<httprequestexception>()</httprequestexception>抓不到HttpStatusCode.ServiceUnavailable (503),得补HandleResult<httpresponsemessage>(r => !r.IsSuccessStatusCode)</httpresponsemessage>或直接用HttpPolicyExtensions.HandleTransientHttpError() - 策略必须通过
AddPolicyHandler注册到 DI 管理的IHttpClientFactory命名客户端上;手写new HttpClient()或直接 resolve 未注册策略的 client,策略完全不介入 - 退避必须带 jitter,否则即使策略链正确,也会因同步重试压垮下游——这点最容易被忽略,因为日志里看不出错,只有监控里 CPU/错误率突增
最常被跳过的其实是 jitter 和异常捕获的组合:503 返回不抛异常,但没加 HandleResult 就等于对它视而不见;而加了 HandleResult 却用固定间隔,又把瞬时过载变成持续碾压。这两处一漏,重试就从保护机制变成攻击脚本。











