在 C# 开发中,调用外部服务或数据库时经常会遇到临时故障,比如网络闪断、服务过载等,这类故障通常不需要立即报错,通过重试或者暂时切断调用链路就能恢复。Polly 是一个轻量级的 .NET 弹性瞬态故障处理库,提供了丰富的策略来应对这类问题,其中重试和断路器是最常用的两种策略。

Polly 基础准备
首先需要在项目中引入 Polly 包,如果是 .NET Core 或 .NET 5+ 项目,可以通过 NuGet 安装 Polly 最新稳定版,安装完成后即可在代码中引用相关命名空间。
// 安装命令:Install-Package Polly 或 dotnet add package Polly using Polly; using Polly.CircuitBreaker;
实现重试机制
重试机制适用于 transient 故障,也就是临时出现的、大概率会自动恢复的错误,比如网络偶发超时。Polly 提供了多种重试策略,包括固定间隔重试、指数退避重试等。
简单重试策略
下面的示例实现了最多重试 3 次,每次重试间隔 1 秒的策略,当调用外部接口抛出 HttpRequestException 时触发重试。
// 定义重试策略,最多重试3次,每次间隔1秒,仅在抛出HttpRequestException时重试
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(1));
// 使用策略执行接口调用逻辑
var result = retryPolicy.Execute(() =>
{
// 模拟调用外部HTTP接口
var client = new HttpClient();
return client.GetAsync("http://ipipp.com/api/test").Result;
});
指数退避重试
如果服务故障是短暂的,指数退避重试可以避免短时间内大量重试加重服务负担,每次重试间隔按指数增长。
// 指数退避重试,最多重试5次,首次间隔1秒,之后每次间隔翻倍
var exponentialRetryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1)));
// 执行调用
exponentialRetryPolicy.Execute(() =>
{
var client = new HttpClient();
return client.GetAsync("http://ipipp.com/api/test").Result;
});
实现断路器策略
断路器的作用是当服务出现连续故障时,暂时切断调用链路,避免无意义的重试浪费资源,同时给故障服务恢复的时间。断路器有三个状态:关闭、打开、半打开。
- 关闭状态:正常执行调用,当连续失败次数达到阈值时切换到打开状态
- 打开状态:直接拒绝调用,经过设定的熔断时间后切换到半打开状态
- 半打开状态:允许少量调用尝试,如果成功则切换到关闭状态,失败则回到打开状态
基础断路器实现
下面的示例定义了连续失败 5 次后熔断,熔断时间为 30 秒的断路器策略。
// 定义断路器策略:连续5次失败后熔断,熔断时长30秒
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.CircuitBreaker(5, TimeSpan.FromSeconds(30));
// 执行调用
try
{
circuitBreakerPolicy.Execute(() =>
{
var client = new HttpClient();
return client.GetAsync("http://ipipp.com/api/test").Result;
});
}
catch (BrokenCircuitException)
{
// 捕获断路器打开时的异常,执行降级逻辑
Console.WriteLine("服务当前不可用,已触发熔断,请稍后再试");
}
组合重试与断路器策略
实际业务中通常会将重试和断路器组合使用,先执行重试策略,重试多次失败后触发断路器,避免对故障服务持续发起请求。
// 先定义重试策略
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(1));
// 再定义断路器策略
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.CircuitBreaker(5, TimeSpan.FromSeconds(30));
// 组合策略:先执行重试,重试失败后触发断路器
var combinedPolicy = Policy.Wrap(retryPolicy, circuitBreakerPolicy);
// 使用组合策略执行调用
try
{
combinedPolicy.Execute(() =>
{
var client = new HttpClient();
return client.GetAsync("http://ipipp.com/api/test").Result;
});
}
catch (BrokenCircuitException)
{
Console.WriteLine("服务不可用,已熔断");
}
catch (HttpRequestException)
{
Console.WriteLine("调用失败,重试后仍未成功");
}
策略的复用与依赖注入
在实际项目中,策略通常定义为单例,通过依赖注入的方式提供给各个业务模块使用,避免重复创建策略实例。
// 在Startup或Program中注册策略
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(1));
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.CircuitBreaker(5, TimeSpan.FromSeconds(30));
var combinedPolicy = Policy.Wrap(retryPolicy, circuitBreakerPolicy);
// 注册为单例
builder.Services.AddSingleton(combinedPolicy);
// 在业务类中注入使用
public class TestService
{
private readonly IAsyncPolicy _policy;
public TestService(IAsyncPolicy policy)
{
_policy = policy;
}
public async Task CallApi()
{
await _policy.ExecuteAsync(async () =>
{
var client = new HttpClient();
return await client.GetAsync("http://ipipp.com/api/test");
});
}
}
注意事项
- 重试策略不要用于非 transient 故障,比如参数错误、权限不足等,这类错误重试没有意义
- 断路器的阈值和熔断时间需要根据实际业务场景调整,避免阈值过低频繁熔断,或者阈值过高起不到保护作用
- 组合策略时,顺序很重要,通常是重试在前,断路器在后,避免重试触发断路器
- 如果调用的是同步方法,使用 Execute 方法,异步方法使用 ExecuteAsync 方法,不要混用