Polly
这页对应 3.x 里的容错与重试使用方式。Polly 在当前文档里按“开发指南”维护,而不是按核心组件维护,因为它更像调用侧按需引入的策略库,而不是通过 Component.deps.json 统一装配的框架组件。
先记住 Polly 的边界
Polly 主要解决的是“调用失败后怎么办”,最常见的是:
- 远程 HTTP 请求重试
- 第三方接口超时保护
- 连续失败后的熔断
- 调用失败后的降级结果
它不负责业务补偿本身,也不会自动替所有外部调用加策略。
策略要不要加、加在哪一层、失败后返回什么,仍然要由业务自己明确。
最小引入方式
Polly 这页对应的是代码级接入,不需要改 Component.deps.json。
通常只需要两步:
- 给调用项目安装
Aegis.Extensions.Polly - 在具体调用代码或
HttpClient注册处挂策略
最常见会用到的命名空间如下:
using System.Net;
using Polly;
using Polly.CircuitBreaker;
using Polly.Timeout;
什么时候最适合用 Polly
适合场景:
- 调 Broker、第三方平台、网关接口时,希望遇到短暂故障可自动重试
- 接口响应偶发超时,但业务仍希望给外部系统一次恢复机会
- 某个依赖长期不可用时,希望尽快熔断,避免把自身线程和连接耗死
- 调用失败后,需要返回可识别的降级结果,再由业务决定是否补偿
先做一个判断:不是所有接口都应该重试
在加策略前,先判断调用是否具备幂等性。
适合重试:
- 查询类接口
- 幂等更新
- 带唯一业务单号的提交
不适合盲目重试:
- 重复提交会产生副作用的创建接口
- 第三方没有幂等保障的扣费、发药、库存扣减接口
常用策略怎么选
| 策略 | 解决什么问题 | 最常见用法 |
|---|---|---|
| 重试 | 偶发异常、短暂网络抖动 | 查询、幂等写入 |
| 超时 | 对端太慢、连接迟迟不返回 | 外部 HTTP 或第三方接口 |
| 熔断 | 连续失败后快速失败,避免雪崩 | 不稳定依赖服务 |
| 降级 | 全部策略都失败后返回替代结果 | 非核心结果、兜底提示 |
最常见的代码起步方式
只做重试
var retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
适合场景:
- 目标接口偶发失败
- 可以接受最多再试 3 次
先超时,再重试
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5);
var retryPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.OrResult(response => !response.IsSuccessStatusCode)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
var policyWrap = Policy.WrapAsync(retryPolicy, timeoutPolicy);
适合场景:
- 对端偶尔很慢
- 不希望单次调用无限等待
超时 + 重试 + 熔断 + 降级
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(5);
var retryPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.OrResult(response => !response.IsSuccessStatusCode)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
var circuitBreakerPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.OrResult(response => !response.IsSuccessStatusCode)
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(10));
var fallbackPolicy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.Or<BrokenCircuitException>()
.OrResult(response => !response.IsSuccessStatusCode)
.FallbackAsync(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)
{
Content = new StringContent("{\"message\":\"fallback\"}")
});
var policyWrap = Policy.WrapAsync(
fallbackPolicy,
retryPolicy,
timeoutPolicy,
circuitBreakerPolicy);
适合场景:
- 外部依赖不稳定
- 既要保护调用方,也要保护被调用方
和 HttpClient 一起用的常见方式
如果你们已经通过 HttpClientFactory 管理调用,可以把策略直接挂到对应客户端上。
services.AddHttpClient("RemoteApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(response => !response.IsSuccessStatusCode)
.WaitAndRetryAsync(
3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));
这种方式最适合:
- 一个下游系统对应一个固定客户端
- 希望策略跟客户端配置放在一起管理
返回结果触发策略,通常怎么写
很多时候不是抛异常才算失败,接口即便返回了 200,业务结果也可能失败。
这时候可以用 OrResult(...) 把“业务失败结果”也纳入策略触发条件。
var retryPolicy = Policy<Result>
.Handle<HttpRequestException>()
.OrResult(result => result.Status != 200)
.WaitAndRetryAsync(
3,
retryAttempt => TimeSpan.FromSeconds(retryAttempt * 2));
这种写法很适合:
- 下游系统统一返回
Result/Response<T> - 业务码不成功时也希望重试或降级
使用降级时要想清楚什么
降级不是“吞掉错误”,而是“明确给上游一个兜底结果”。
常见方式有三类:
- 返回一个可识别的失败对象
- 返回缓存或默认数据
- 记录告警后转人工补偿
var fallbackPolicy = Policy<Result>
.Handle<Exception>()
.OrResult(result => result.Status != 200)
.FallbackAsync(new Result
{
Status = 503,
Message = "当前远程服务不可用,请稍后重试"
});
当前团队里最值得坚持的几条规则
规则一:不要给所有远程调用统一套一层重试
不同接口的幂等性、时效性和副作用不同。
越是“统一一刀切”,越容易把问题隐藏掉。
规则二:超时和重试通常要一起考虑
如果没有超时保护,重试前的单次等待就可能已经拖垮调用链。
如果只有超时没有重试,很多短暂抖动又会直接暴露给业务。
规则三:熔断阈值要偏保守
熔断不是越敏感越好。
阈值太小容易误伤正常流量,阈值太大又起不到保护作用。
常见问题
为什么加了重试,接口还是经常失败
优先检查:
- 失败是不是持续性故障,而不是偶发抖动
- 当前接口是否根本不适合重试
- 重试次数、间隔和超时时间是否过小
为什么加了重试后,问题反而更严重
最常见原因是:
- 非幂等接口被重复调用
- 没有限流和熔断,所有实例同时重试
- 对端已经很慢,又被重试进一步放大压力
为什么明明 HTTP 是 200,策略还是应该触发
因为很多系统把业务失败包在正常 HTTP 响应里。
这种情况下就应该用 OrResult(...) 判断业务结果,而不是只看状态码。