c# 如何在c#中实现断路器(Circuit Breaker)模式

直接用 Polly 而非手写断路器,因其线程安全、状态严谨、已深度集成生态;手写易致并发探测异常或内存泄漏,Polly 支持 HttpClient 原生集成及自定义策略组合与状态监听。

为什么直接用 Polly 而不是手写断路器

手写一个线程安全、状态切换严谨、支持超时/重试/降级的断路器非常容易出错。比如 CircuitState.HalfOpen 下并发请求可能触发多次探测,或计时器未正确清理导致内存泄漏。生产环境建议直接用成熟库——Polly 是 .NET 生态事实标准,已深度适配 IHttpClientFactoryMicrosoft.Extensions.DependencyInjection

安装与基础用法:Polly + AddCircuitBreaker

从 .NET 6 开始,Microsoft.Extensions.Http.Resilience 提供了原生集成(基于 Polly),推荐优先使用:

dotnet add package Microsoft.Extensions.Http.Resilience

Program.cs 中注册:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient()
    .AddCircuitBreaker(policy => policy
        .HandledStatusCodes(500, 502, 503, 504)
        .FailureThreshold(0.3) // 连续失败率阈值
        .SamplingDuration(TimeSpan.FromSeconds(30))
        .MinimumThroughput(10) // 每 30 秒至少 10 次调用才触发统计
        .BreakDuration(TimeSpan.FromMinutes(1))); // 熔断时长

关键点:

  • FailureThreshold 不是固定次数,而是失败率;低流量下即使只失败 1 次也可能不触发熔断(因未达 MinimumThroughput
  • BreakDuration 是“硬暂停”,期间所有请求直接抛 BrokenCircuitException,不会转发
  • 该配置仅对 HttpClient 生效;若需保护普通方法(如数据库访问),仍需手动包装 Policy

保护非 HTTP 方法:用 Policy.WrapAsync 组合策略

比如封装一个可能抛异常的仓储方法:

private readonly AsyncCircuitBreakerPolicy _circuitBreaker = Policy
    .Handle(ex => ex.Number is 1205 or 1222) // 死锁/超时
    .Or()
    .CircuitBreakerAsync(
        exceptionsAllowedBeforeBreaking: 3,
        durationOfBreak: TimeSpan.FromMinutes(2));

调用时必须显式包裹:

try
{
    await _circuitBreaker.ExecuteAsync(async () => await _repository.FetchDataAsync());
}
catch (BrokenCircuitException)
{
    // 返回缓存或默认值
    return _cache.GetOrAdd("fallback", _ => new List());
}

注意:

  • 不要把 ExecuteAsync 写在循环里——每次调用都计入熔断统计,高频调用会快速触发熔断
  • exceptionsAllowedBeforeBreaking 是绝对次数,和时间窗口无关;适合低频、高价值操作(如支付确认)
  • 若需同时加重试,用 Policy.WrapAsync(retryPolicy, circuitBreaker),顺序很重要:外层是熔断器,内层是重试

自定义状态监听与诊断:别只靠日志

熔断器状态变化(如 Open → HalfOpen)是关键信号,但默认不输出。启用监听:

var breaker = Policy
    .Handle()
    .CircuitBreakerAsync(3, TimeSpan.FromMinutes(1),
        onBreak: (ex, ts) => {
            Console.WriteLine($"Circuit broken for {ts.TotalMinutes} min due to {ex.GetType().Name}");
        },
        onReset: () => Console.WriteLine("Circuit reset"),
        onHalfOpen: () => Console.WriteLine("Circuit half-open: allowing one probe")); 

生产环境应将这些回调对接到指标系统(如 Prometheus 的 counter),而不是仅打印日志。特别注意 onHalfOpen——它只在第一个试探请求前触发,不代表试探成功。

真正难的是状态同步:分布式场景下,单机熔断器无法共享状态。这时得上 Redis 或专用服务(如 Resilience4j 的集中式断路器),Polly 默认不解决这个问题。