跳到主要内容
版本:3.0.0

Polly

这页对应 3.x 里的容错与重试使用方式。Polly 在当前文档里按“开发指南”维护,而不是按核心组件维护,因为它更像调用侧按需引入的策略库,而不是通过 Component.deps.json 统一装配的框架组件。

先记住 Polly 的边界

Polly 主要解决的是“调用失败后怎么办”,最常见的是:

  • 远程 HTTP 请求重试
  • 第三方接口超时保护
  • 连续失败后的熔断
  • 调用失败后的降级结果

它不负责业务补偿本身,也不会自动替所有外部调用加策略。
策略要不要加、加在哪一层、失败后返回什么,仍然要由业务自己明确。

最小引入方式

Polly 这页对应的是代码级接入,不需要改 Component.deps.json
通常只需要两步:

  1. 给调用项目安装 Aegis.Extensions.Polly
  2. 在具体调用代码或 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 = "当前远程服务不可用,请稍后重试"
});

当前团队里最值得坚持的几条规则

规则一:不要给所有远程调用统一套一层重试

不同接口的幂等性、时效性和副作用不同。
越是“统一一刀切”,越容易把问题隐藏掉。

规则二:超时和重试通常要一起考虑

如果没有超时保护,重试前的单次等待就可能已经拖垮调用链。
如果只有超时没有重试,很多短暂抖动又会直接暴露给业务。

规则三:熔断阈值要偏保守

熔断不是越敏感越好。
阈值太小容易误伤正常流量,阈值太大又起不到保护作用。

常见问题

为什么加了重试,接口还是经常失败

优先检查:

  1. 失败是不是持续性故障,而不是偶发抖动
  2. 当前接口是否根本不适合重试
  3. 重试次数、间隔和超时时间是否过小

为什么加了重试后,问题反而更严重

最常见原因是:

  • 非幂等接口被重复调用
  • 没有限流和熔断,所有实例同时重试
  • 对端已经很慢,又被重试进一步放大压力

为什么明明 HTTP 是 200,策略还是应该触发

因为很多系统把业务失败包在正常 HTTP 响应里。
这种情况下就应该用 OrResult(...) 判断业务结果,而不是只看状态码。

推荐阅读顺序

  1. HTTP请求组件
  2. WebService调用
  3. 远程调用客户端(Aegis.Net.Broker)