接口限流+接口节流+请求熔断+取消令牌,提高API韧性功能

0 阅读33分钟

目标

在高并发与下游故障时保护服务与数据库,防刷、防雪崩,维持核心路径可用。

为什么需要这些机制

  • 保护可用性与 SLA:异常/高峰时优雅退让,“服务存活”优先于“请求全部成功”
  • 防止雪崩与级联故障:隔离慢/坏依赖,避免线程/连接/队列被拖死
  • 抗滥用与公平配额:防刷、防恶意流量、租户隔离、公平分配资源
  • 资源治理:有限 CPU/DB 连接/线程用在最重要的请求上

简介

  • 限流(Rate Limiting)

    • 是什么:在单位时间只允许通过有限请求数,相当于“门口按秒/分限人流”。

    • 解决什么:防刷、防突发流量把服务压垮。

    • 什么时候用:对外接口、租户/客户端配额管理。

    • .NET/ABP怎么做:网关/应用层用限流中间件(AspNetCoreRateLimit 或 .NET 7 Rate Limiting),计数用 Redis。

    • 控制单位时间内的请求总数

      • 例如:1分钟内最多100个请求
      • 关注:频率/速率(requests per time unit)
  • 并发限制/节流(Concurrency Limit / Throttling)

    • 是什么:限制“同时在处理的请求数”,队列也有上限和最长等待时间,相当于“收银台最多同时服务 N 人,队列不超过 M 人、等不超过 T 秒”。

    • 解决什么:线程/数据库连接被占满、排队过长导致整体变慢。

    • 什么时候用:慢接口、数据库敏感路径、CPU/连接资源有限的服务。

    • .NET/ABP怎么做:.NET 7 Rate Limiting 的并发/队列策略;关键接口设并发上限+有界队列。

    • 控制同时处理的请求数量

      • 例如:同时最多处理10个请求
      • 关注:并发数量(concurrent requests)
  • 熔断(Circuit Breaking)

    • 是什么:下游持续失败时“临时拉闸”,在冷却期直接失败不再调用;一段时间后少量试探恢复。
    • 解决什么:持续失败引发的重试风暴与排队雪崩。
    • 什么时候用:调用第三方/微服务依赖。
    • .NET/ABP怎么做:Polly 的 CircuitBreaker 与 HttpClientFactory 结合。
  • 降级/回退(Fallback)

    • 是什么:下游不可用/很慢时,返回“退而求其次”的结果,比如缓存/默认值/空列表/暂时关闭非核心功能。
    • 解决什么:保证核心路径“有响应”,牺牲部分准确性换稳定性。
    • 什么时候用:推荐/画像/列表等可容忍旧数据或空数据的场景。
    • .NET/ABP怎么做:Polly Fallback;配合本地/Redis 缓存或简化逻辑。
  • 重试(Retry,配退避)

    • 是什么:遇到临时故障时“稍后再试”,按 200ms/500ms/1s 退避递增。
    • 解决什么:网络抖动、偶发错误。
    • 什么时候用:仅幂等操作(重复不会造成副作用)。
    • .NET/ABP怎么做:Polly Retry 与 Timeout/熔断配合;非幂等需幂等键或去重。
  • 超时(Timeout)

    • 是什么:给每次调用设“最长等待时间”,到点就返回,避免线程/连接占着不放。
    • 解决什么:卡死、队列积压。
    • 什么时候用:所有外呼都应设超时;内部也要尊重取消。
    • .NET/ABP怎么做:HttpClient 超时 + Polly Timeout;数据库/缓存也设查询超时。
  • 舱壁隔离(Bulkhead)

    • 是什么:为不同依赖分配独立的并发/连接池,互不拖累;像“包间”隔离。
    • 解决什么:一个依赖崩了拖垮全服务。
    • 什么时候用:服务依赖多、风险不一时。
    • .NET/ABP怎么做:Polly Bulkhead 或分客户端池化;每依赖设独立并发/队列/策略。

组合的直觉规则

  • 入口限速(防刷)+ 服务层并发/队列(容量保护)是“保命线”。
  • 对下游依赖:超时是底座;持续失败时用熔断快速止损;能重试的只重试幂等操作;能降级的返回替代结果;不同依赖分池(舱壁)。
  • 加上监控:看 429 命中、队列长度、延迟尾部(p95/p99)、熔断状态、重试/降级次数,边看边调参数。

限流与节流

维度限流 (Rate Limiting)并发限制 (Throttling)
控制目标单位时间内的请求总数同时处理的请求数量
计数方式累计计数(会重置)当前活跃数(动态变化)
典型场景防刷、API配额保护慢接口、数据库连接池
拒绝时机达到时间窗口上限达到并发上限
HTTP状态码429 Too Many Requests503 Service Unavailable
. NET实现FixedWindow/SlidingWindow/TokenBucketConcurrencyLimiter
是否排队通常不排队,直接拒绝可以排队等待

主流方案

方案 1:Microsoft.AspNetCore.RateLimiting(. NET 7+ 内置)

优点:

  • ✅ 官方原生支持,无需额外依赖
  • ✅ 性能好,内存实现
  • ✅ 与 ASP.NET Core 深度集成
  • ✅ 支持多种算法(固定窗口、滑动窗口、令牌桶、并发限制)

缺点:

  • 单机限流,不支持分布式(多实例场景会失效)
  • ❌ 缺少持久化,重启后计数器清零
  • ❌ 功能相对简单,没有高级特性

适用场景: 实例部署、内部服务


方案 2:AspNetCoreRateLimit(第三方库,老牌方案)

优点:

  • 支持分布式(基于 Redis/MemoryCache)
  • ✅ 功能丰富(IP限流、客户端限流、端点限流)
  • ✅ 配置灵活(appsettings.json 配置)
  • ✅ 支持白名单/黑名单

缺点:

  • ❌ 第三方库,维护不如官方积极
  • ❌ 配置复杂
  • ❌ 性能比原生方案稍差

适用场景: 多实例部署、需要复杂规则


方案 3:RedLock + Redis(自己实现)

优点:

  • 完全分布式
  • ✅ 灵活性最高
  • ✅ 可以自定义任意逻辑

缺点:

  • ❌ 需要自己实现
  • ❌ 复杂度高
  • ❌ 需要维护 Redis

适用场景: 复杂业务场景、多租户系统

方案一实现

Microsoft.AspNetCore.RateLimiting速率限制中间件

learn.microsoft.com/en-us/aspne…

依赖包

dotnet add package Microsoft.AspNetCore.RateLimiting

配置限流策略

在你的 HttpApiHostModuleWebModule 中配置:

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;

namespace Abp.RateLimiting;

public class RateLimitingModule : AbpModule
{
     public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services. GetConfiguration();
        
        context.Services.AddRateLimiter(options =>
        {
            // ========== 限流策略 (Rate Limiting) ==========
            
            // 1. 固定窗口:每分钟最多 100 个请求
            options.AddFixedWindowLimiter("rate-fixed", opt =>
            {
                opt. PermitLimit = 1;
                opt.Window = TimeSpan.FromMinutes(10);
                opt.QueueLimit = 0;
            });

            // 2. 滑动窗口:每分钟最多 100 个请求(更平滑)
            options.AddSlidingWindowLimiter("rate-sliding", opt =>
            {
                opt. PermitLimit = 100;
                opt.Window = TimeSpan.FromMinutes(1);
                opt.SegmentsPerWindow = 6;
                opt.QueueLimit = 0;
            });

            // 3. 令牌桶:平均每分钟50个,最多突发100个
            options.AddTokenBucketLimiter("rate-token", opt =>
            {
                opt.TokenLimit = 100;
                opt.TokensPerPeriod = 50;
                opt.ReplenishmentPeriod = TimeSpan.FromMinutes(1);
                opt.QueueLimit = 0;
            });
            
            
            // ========== 基于 IP 的固定窗口限流 ==========
            //每个IP独立计数,1个小时内最多100次请求
            options.AddPolicy("ip-rate-limit", httpContext =>
            {
                // 获取当前请求的 IP地址,也可以替换成固定IP,也可以再进入之前对IP进行处理,比如黑白名单等
                var ipAddress = httpContext.Connection.RemoteIpAddress?. ToString() 
                                ?? "unknown";

                return RateLimitPartition.GetFixedWindowLimiter(
                    partitionKey: ipAddress,  // 每个 IP 独立计数
                    factory: _ => new FixedWindowRateLimiterOptions
                    {
                        PermitLimit = 100,  // 每个 IP 每分钟 100Window = TimeSpan.FromMinutes(1),
                        QueueLimit = 0
                    });
            });

            // ========== 并发限制策略 (Concurrency Limiting) ==========
            
            // 4. 并发限制:最多同时处理 50 个请求
            options.AddConcurrencyLimiter("concurrency", opt =>
            {
                opt.PermitLimit = 5;
                opt. QueueLimit = 0;
                opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
            });
            
            
           
            
            // ========== 基于 IP 的并发限制(节流) ==========
            options. AddPolicy("ip-concurrency-limit", httpContext =>
            {
                var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString() 
                                ??  "unknown";

                return RateLimitPartition.GetConcurrencyLimiter(
                    partitionKey: ipAddress,
                    factory: _ => new ConcurrencyLimiterOptions
                    {
                        PermitLimit = 10,  // 每个 IP 最多 10 个并发
                        QueueLimit = 5,
                        QueueProcessingOrder = QueueProcessingOrder.OldestFirst
                    });
            });
            

            // ========== 统一的拒绝处理 ==========
            //根据实际,替换抛错为自己的全局抛错或者其它的处理
            options.OnRejected = async (context, cancellationToken) =>
            {
                var response = context.HttpContext.Response;
                
                // 根据不同的限制类型返回不同的状态码和消息
                if (IsRateLimitRejection(context))
                {
                    
                    // 限流拒绝 - 429
                    response.StatusCode = StatusCodes.Status429TooManyRequests;
                    
                    var retryAfter = GetRetryAfterSeconds(context);
                    if (retryAfter > 0)
                    {
                        response.Headers["Retry-After"] = retryAfter.ToString();
                    }

                    await response.WriteAsJsonAsync(new
                    {
                        error = "请求过于频繁,请稍后再试",
                        type = "rate_limit_exceeded",
                        retryAfter = retryAfter
                    }, cancellationToken);
                }
                else
                {
                    // 并发限制拒绝 - 503
                    response.StatusCode = StatusCodes.Status503ServiceUnavailable;
                    
                    await response.WriteAsJsonAsync(new
                    {
                        error = "服务繁忙,请稍后再试",
                        type = "concurrency_limit_exceeded",
                        message = "当前处理请求过多,请稍后重试"
                    }, cancellationToken);
                }
            };
        });
    }
     
    /// <summary>
    /// 判断是限流拒绝还是并发限制拒绝
    /// </summary>
    private static bool IsRateLimitRejection(OnRejectedContext context)
    {
        // 通过策略名称判断
        var policyName = context.HttpContext.GetEndpoint()?.Metadata
            . GetMetadata<EnableRateLimitingAttribute>()?. PolicyName;

        return policyName != null && 
               (policyName.StartsWith("rate-") || policyName == "user-rate-limit");
    }

    /// <summary>
    /// 获取重试等待时间(秒)
    /// </summary>
    private static int GetRetryAfterSeconds(OnRejectedContext context)
    {
        if (context. Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            return (int)retryAfter.TotalSeconds;
        }

        return 60; // 默认60秒
    }

}
// options.OnRejected 是限流中间件的配置选项

options.OnRejected = async (context, cancellationToken) =>
{
    // 当请求被限流拒绝时,这个委托会被执行
};

/*
【翻译】:
- OnRejected = 当被拒绝时
- context = 被拒绝的请求的上下文信息
- cancellationToken = 取消令牌(用于异步操作)

【执行时机】:
当用户的请求超过限流配额时,这个委托就会被执行
*/

注册中间件

    app.UseRouting();        // ✅ 必须先有路由
    app.UseRateLimiter();    // ✅ 然后是限流
    app.UseEndpoints(... );   // ✅ 最后是端点

在 Controller 或 AppService 中使用

  	
	//一分钟内只允许请求一个,其余的抛错429
	[EnableRateLimiting("rate-fixed")] 
    [HttpGet]
    [Route("folder-structure")]
    public async Task<FileFolderInfo> GetFolderStructure([FromQuery] string? path = null)
    {
        await Task. Delay(5000);
        return await _service.GetRecursionFileInfo(path);
    }
    
    //同时有且只有最多5个请求在处理,其余的报错503
    [EnableRateLimiting("concurrency")] 
    [HttpGet]
    [Route("folder-structure")]
    public async Task<FileFolderInfo> GetFolderStructure([FromQuery] string? path = null)
    {
        await Task. Delay(5000);
        return await _service.GetRecursionFileInfo(path);
    }

自定义策略

//黑白名单

options.AddPolicy("ip-rate-limit", httpContext =>
{
    var ipAddress = httpContext.Connection.RemoteIpAddress?. ToString() ?? "unknown";
    
    // 定义允许的 IP 列表
    var allowedIps = new[] { "192.168.41.8", "127.0.0.1", ":: 1" };
    
    if (allowedIps.Contains(ipAddress))
    {
        // 允许的 IP:每分钟 100 次
        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey:  ipAddress,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0
            });
    }
    
    // 不允许的 IP:直接拒绝(PermitLimit = 0)
    return RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: "blocked",
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 0,  // 完全阻止
            Window = TimeSpan.FromMinutes(1),
            QueueLimit = 0
        });
});

知识点

特性
namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// Metadata that provides endpoint-specific request rate limiting.
/// </summary>
/// <remarks>
/// Replaces any policies currently applied to the endpoint.
/// The global limiter will still run on endpoints with this attribute applied.
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class EnableRateLimitingAttribute : AttributeABPEnableRateLimitingAttribute特性在只在控制器被捕获,方法级是不生效的,Core web api是都可以的.
具体原因是因为ABP的服务层设计没有走传统的方式,中间件拿不到特性标记,可以自定义拦截器实现,目前在控制层实现也就足够了.
[DisableRateLimiting]
//禁止限流
RetryAfter

RetryAfter 是一个 HTTP 响应头,用于告诉客户端多久后可以重新尝试请求

作用

当服务器因为某些原因(如限流、服务维护、服务暂时不可用等)无法处理请求时,通过 RetryAfter 告诉客户端:

"请不要立即重试,请在 X 秒/分钟后再试"

响应示例

HTTP

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "error": "Too many requests"
}

这表示客户端应该在 60 秒后才重新尝试请求。

RetryAfter 的两种格式
1. 秒数格式(推荐)

HTTP

Retry-After: 60

表示 60 秒后可以重试

2. HTTP-Date 格式

HTTP

Retry-After: Wed, 14 Jan 2026 10:30:00 GMT

表示在这个具体时间点之后可以重试

固定限流参数
public sealed class FixedWindowRateLimiterOptions
{
  /// <summary>
  /// Specifies the time window that takes in the requests.
  /// Must be set to a value greater than <see cref="F:System.TimeSpan.Zero" /> by the time these options are passed to the constructor of <see cref="T:System.Threading.RateLimiting.FixedWindowRateLimiter" />.
  /// </summary>
  public TimeSpan Window { get; set; } = TimeSpan.Zero;

  /// <summary>
  /// Specified whether the <see cref="T:System.Threading.RateLimiting.FixedWindowRateLimiter" /> is automatically refresh counters or if someone else
  /// will be calling <see cref="M:System.Threading.RateLimiting.FixedWindowRateLimiter.TryReplenish" /> to refresh counters.
  /// </summary>
  /// <value>
  /// <see langword="true" /> by default.
  /// </value>
  public bool AutoReplenishment { get; set; } = true;

  /// <summary>
  /// Maximum number of permit counters that can be allowed in a window.
  /// Must be set to a value &gt; 0 by the time these options are passed to the constructor of <see cref="T:System.Threading.RateLimiting.FixedWindowRateLimiter" />.
  /// </summary>
  public int PermitLimit { get; set; }

  /// <summary>
  /// Determines the behaviour of <see cref="M:System.Threading.RateLimiting.RateLimiter.AcquireAsync(System.Int32,System.Threading.CancellationToken)" /> when not enough resources can be leased.
  /// </summary>
  /// <value>
  /// <see cref="F:System.Threading.RateLimiting.QueueProcessingOrder.OldestFirst" /> by default.
  /// </value>
  public QueueProcessingOrder QueueProcessingOrder { get; set; }

  /// <summary>
  /// Maximum cumulative permit count of queued acquisition requests.
  /// Must be set to a value &gt;= 0 by the time these options are passed to the constructor of <see cref="T:System.Threading.RateLimiting.FixedWindowRateLimiter" />.
  /// </summary>
  public int QueueLimit { get; set; }
}
并发限制的核心机制
1. 占用与释放

C#

[EnableRateLimiting("concurrency")] // PermitLimit = 5
public async Task<IActionResult> SlowEndpointAsync()
{
    // ⬇️ 请求进入时:占用 1 个槽位(5 → 4)
    
    await Task.Delay(5000); // 模拟慢操作
    
    return Ok("完成");
    
    // ⬆️ 请求返回时:释放 1 个槽位(4 → 5)
}
2. 完整生命周期

Code

时间线:
┌─────────────────────────────────────────────────────────┐
│ 请求1 进入                                               │
│   ↓ 占用槽位(5 → 4)                                    │
│   ├─ 执行业务逻辑 ─────────────┐                         │
│                                │                         │
│ 请求2 进入                      │                         │
│   ↓ 占用槽位(4 → 3)           │                         │
│   ├─ 执行业务逻辑 ──────┐       │                         │
│                         │       │                         │
│ 请求3 进入               │       │                         │
│   ↓ 占用槽位(3 → 2)    │       │                         │
│   ├─ 执行业务逻辑 ─┐     │       │                         │
│                    │     │       │                         │
│ 请求4 进入          │     │       │                         │
│   ↓ 占用槽位(2 → 1)│     │       │                         │
│   ├─ 执行业务逻辑  │     │       │                         │
│                    │     │       │                         │
│ 请求5 进入          │     │       │                         │
│   ↓ 占用槽位(1 → 0)│     │       │                         │
│   ├─ 执行业务逻辑  │     │       │                         │
│                    │     │       │                         │
│ 请求6 进入          │     │       │                         │
│   ↓ ❌ 无可用槽位   │     │       │                         │
│   ↓ 503 立即拒绝    │     │       │                         │
│                    │     │       │                         │
│                    ↓     │       │                         │
│             请求3 完成    │       │                         │
│             释放槽位(0 → 1)     │                         │
│                          ↓       │                         │
│                   请求2 完成      │                         │
│                   释放槽位(1 → 2)│                         │
│                                  ↓                         │
│                           请求1 完成                        │
│                           释放槽位(2 → 3)                 │
└─────────────────────────────────────────────────────────┘

详细示例:5个槽位的并发限制

配置

C#

options.AddConcurrencyLimiter("concurrency", opt =>
{
    opt.PermitLimit = 5;      // 最多 5 个请求同时处理
    opt.QueueLimit = 0;       // 不排队,直接拒绝
});

测试场景

Controllers/TestController.csv4

[ApiController]
[Route("api/test")]
public class TestController : AbpController
{
    private static int _currentProcessing = 0;

并发发送 10 个请求

bash

# 同时发送 10 个请求
for i in {1.. 10}; do
  curl http://localhost:5000/api/test/slow &
done
wait

日志输出(预期)

log

[12:00:00.100] 🔵 请求 a1b2c3d4 开始处理 | 当前处理中:  1/5 | 时间: 12:00:00.100
[12:00:00.102] 🔵 请求 e5f6g7h8 开始处理 | 当前处理中: 2/5 | 时间: 12:00:00.102
[12:00:00.105] 🔵 请求 i9j0k1l2 开始处理 | 当前处理中: 3/5 | 时间: 12:00:00.105
[12:00:00.108] 🔵 请求 m3n4o5p6 开始处理 | 当前处理中: 4/5 | 时间: 12:00:00.108
[12:00:00.110] 🔵 请求 q7r8s9t0 开始处理 | 当前处理中: 5/5 | 时间: 12:00:00.110
                ⬆️ 槽位已满!

[12:00:00.112] ❌ 请求 6 被拒绝 (503 Service Unavailable)
[12:00:00.113] ❌ 请求 7 被拒绝 (503 Service Unavailable)
[12:00:00.114] ❌ 请求 8 被拒绝 (503 Service Unavailable)
[12:00:00.115] ❌ 请求 9 被拒绝 (503 Service Unavailable)
[12:00:00.116] ❌ 请求 10 被拒绝 (503 Service Unavailable)

...  5 秒后 ... 

[12:00:05.101] 🟢 请求 a1b2c3d4 处理完成 | 剩余处理中: 4/5 | 时间: 12:00:05.101
[12:00:05.103] 🟢 请求 e5f6g7h8 处理完成 | 剩余处理中: 3/5 | 时间: 12:00:05.103
[12:00:05.106] 🟢 请求 i9j0k1l2 处理完成 | 剩余处理中: 2/5 | 时间: 12:00:05.106
[12:00:05.109] 🟢 请求 m3n4o5p6 处理完成 | 剩余处理中: 1/5 | 时间: 12:00:05.109
[12:00:05.111] 🟢 请求 q7r8s9t0 处理完成 | 剩余处理中:  0/5 | 时间: 12:00:05.111
                ⬆️ 全部释放,槽位恢复为 5

超时-重试-熔断

www.pollydocs.org/getting-sta…

通过 Polly 实现使用指数退避算法的 HTTP 调用重试 - .NET | Microsoft Learn

  • 重试 (Retry) : 当操作失败时,自动重试一定次数,使用指数退避(Exponential Backoff)来避免立即重试。
  • 熔断 (Circuit Breaker) : 在连续失败一定次数后,停止发送请求一段时间,让下游服务有时间恢复。状态包括:Closed(正常)、Open(拒绝请求)、Half-Open(测试请求)。
  • 超时 (Timeout) : 设置请求的最大等待时间,防止线程挂起过久。

推荐的策略顺序:Timeout → Retry → Circuit Breaker

使用

Polly  _V8.6.5:核心库,提供所有弹性策略(如 Retry, Circuit Breaker, Timeout)。

Microsoft.Extensions.Http.Polly  _V9.0.0:ASP.NET Core 扩展,允许通过 IHttpClientFactory 轻松集成 Polly 策略到 HttpClient。

Microsoft.Extensions.Resilience:可选的高级扩展,提供更现代的弹性 API(如 ResiliencePipeline),但我们的代码直接用 Polly 的经典 API,需要时再用。
using Polly;
using Polly.Extensions.Http;
using Polly.Timeout;
using System;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// 添加 Swagger
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new()
    {
        Title = "My API",
        Version = "v1",
        Description = "这是我的 API 文档"
    });
});


// 重试策略
var retryPolicy = HttpPolicyExtensions.HandleTransientHttpError()
       .Or<TimeoutRejectedException>()//不捕获超时抛错,无法触发
    .WaitAndRetryAsync(
        retryCount: 3, // 重试三次
        sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), // 指数退避:248秒
        onRetry: (outcome, timespan, retryAttempt, context) =>
        {
            // 每次重试时的处理逻辑,例如日志记录
            System.Diagnostics.Debug.WriteLine($"重试原因:{outcome.Exception?.Message}。等待时间:{timespan}。重试次数:{retryAttempt}。");
        });


// 超时策略
var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(3);

    // 熔断策略:添加处理 TimeoutRejectedException
    var circuitBreakerPolicy = HttpPolicyExtensions
        .HandleTransientHttpError()
       .Or<TimeoutRejectedException>()//不捕获超时抛错,无法触发
        .CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 2,
            durationOfBreak: TimeSpan.FromSeconds(15),
             onBreak: (exception, breakDelay) =>
  {
      System.Diagnostics.Debug.WriteLine($"[熔断器] 断开{breakDelay.TotalSeconds}秒,开始时间{DateTime.Now} 原因: {exception.ToString}");
  },
  onReset: () =>
  {
      System.Diagnostics.Debug.WriteLine("[熔断器] 进入恢复状态,开始时间{DateTime.Now},重新计数,恢复打开请求");
  },
  onHalfOpen: () =>
  {
      System.Diagnostics.Debug.WriteLine("[熔断器] 进入半开中,开始时间{DateTime.Now},进入测试阶段");
  }
        );

    // 使用 Policy.WrapAsync 明确顺序:Timeout -> Retry -> Circuit Breaker
    var combinedPolicy = Policy.WrapAsync(circuitBreakerPolicy,retryPolicy, timeoutPolicy);

    // 注册 HTTP 客户端,并添加组合策略
    builder.Services.AddHttpClient("ResilientClient", client =>
    {
        client.BaseAddress = new Uri("https://localhost:55319/");  // 第三方 URL
    })
    .AddPolicyHandler(combinedPolicy);

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.MapOpenApi();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

测试

本地接口

[HttpGet]
[Route("GetPollyAsync")]
public async Task<IActionResult> GetAsync()
{
    var client =  _httpClient.CreateClient("ResilientClient");
    try
    {
        // 调用下游 API(模拟失效)
        var response = await client.GetAsync("api/Test/GetAsync");  // 你的目标 API
        var content = await response.Content.ReadAsStringAsync();
        return Ok(content);
    }
    catch (Exception ex)
    {
        // 记录异常
        Console.WriteLine($"Exception: {ex.Message}");
        return StatusCode(500, $"Error: {ex.Message}");
    }
}

第三方接口

	//https://localhost:55319/api/Test/GetAsync

	//第三方测试接口,配合超时时间,来达到没有响应	
	[HttpGet]
    [Route("GetAsync")]
    public async Task GetAsync(CancellationToken cancellationToken)
    {
        try
        {
            _logger.Log(LogLevel.Debug, "进入1次GetAsync方法");
            await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken);
        }
        catch (OperationCanceledException)
        {
            _logger.Log(LogLevel.Debug, $"{nameof(TestController)}.{nameof(GetAsync)} 调用取消!");
        }
        catch (Exception e)
        {
            _logger.Log(LogLevel.Debug, $"{e.Message}");
        }
    }
时序
第一次https://localhost:7161/WeatherForecast/GetAsync主动调用
访问到/https://localhost:55319/api/Test/GetAsync
3秒超时

System.Net.Http.HttpClient.ResilientClient.LogicalHandler: Information: Start processing HTTP request GET https://localhost:55319/api/Test/GetAsync

System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

重试原因:The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.。等待时间:00:00:02。重试次数:1。
System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

重试原因:The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.。等待时间:00:00:04。重试次数:2。
System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

重试原因:The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.。等待时间:00:00:08。重试次数:3。
System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

https://localhost:7161/WeatherForecast/GetAsync请求失败
[Error: The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.]
[熔断累计一次失败事件]
第二次https://localhost:7161/WeatherForecast/GetAsync主动调用
访问到/https://localhost:55319/api/Test/GetAsync
3秒超时

System.Net.Http.HttpClient.ResilientClient.LogicalHandler: Information: Start processing HTTP request GET https://localhost:55319/api/Test/GetAsync

System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

重试原因:The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.。等待时间:00:00:02。重试次数:1。
System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

重试原因:The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.。等待时间:00:00:04。重试次数:2。
System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

重试原因:The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.。等待时间:00:00:08。重试次数:3。
System.Net.Http.HttpClient.ResilientClient.ClientHandler: Information: Sending HTTP request GET https://localhost:55319/api/Test/GetAsync

https://localhost:7161/WeatherForecast/GetAsync请求失败
[熔断累计一次失败事件]

此时进入到[熔断器] 断开30秒 原因: ...
1. 第二次失败事件时,会直接进入 onBreak,此时主动请求是进来还是被拒绝?
触发 onBreak:当失败事件累计到阈值(这里是 2 次)时,熔断器立即从“Closed”(关闭)状态切换到“Open”(打开)状态,并触发 onBreak 回调。
日志示例:[熔断器] 断开30秒 原因: ...
主动请求的行为:
一旦熔断打开(Open),后续所有请求都会被立即拒绝,不会发送实际 HTTP 请求。
请求会抛出 BrokenCircuitException(Polly 特有的异常),表示熔断器已断开。[达到快速拒绝的情况]
在你的控制器中,这会导致 catch 块捕获异常,返回 500 错误:Error: The circuit is now open and is not allowing calls.。
为什么这样:熔断的目的就是快速失败,避免继续发送请求到已知故障的下游服务。

2. 30 秒过后,是怎么个测试情况?
当你主动发起下一个请求时,Polly 会检查断开时间是否已过:
如果还没到 30 秒,请求被拒绝(仍抛出 BrokenCircuitException)。
如果已过 30 秒,才切换到 Half-Open 状态,触发 onHalfOpen 回调,并允许该请求作为测试请求发送出去。
日志示例:[熔断器] 进入半开中,进入测试阶段
半开测试的情况:
Half-Open 允许下一个请求作为“测试请求”通过,发送到下游服务。[会按照配置的重试策略,主动请求一次,重试3次]
如果测试请求成功(无异常),熔断器切换到“onReset”(关闭),计数重置。
如果测试请求失败,熔断器立即回到“onBreak”,重新开始 30 秒断开周期.
只测试一次:半开状态只允许一个请求测试。如果测试成功,后续请求正常;如果失败,又回到 onBreak。

其他并发请求:
如果在测试进行中又有新请求到来,这些请求不会等待,而是立即被拒绝(抛出 BrokenCircuitException)。
不会“持续等待第一个请求的结果”,因为那是阻塞的,Polly 设计为非阻塞。


3. 测试成功了才会进入到 onReset 吗,然后计数是重新开始吗?
是的,测试成功才会进入 onReset:
如果半开测试请求成功,熔断器切换到“Closed”,并触发 onReset 回调。
日志示例:[熔断器] 进入恢复状态,重新计数,恢复打开请求
计数重新开始:
计数重置为 0,从头开始累计失败事件。
熔断器恢复正常,所有请求都可以通过,直到下次累计失败达到 2 次。

Polly 完整深度学习笔记

目录

  1. Polly 核心概念
  2. 七大策略详解
  3. 策略组合与执行顺序
  4. 高级特性
  5. 性能优化
  6. 监控与可观测性
  7. 实战模式
  8. 常见陷阱与解决方案
  9. Polly v8 新特性
  10. 生产环境检查清单

一、Polly 核心概念

1.1 什么是 Polly?

Polly 是 .NET 的弹性和瞬态故障处理库,提供:

  • 弹性策略(Resilience Policies)重试、断路、超时等
  • 响应式策略(Reactive Policies):处理已发生的故障
  • 主动策略(Proactive Policies):如超时、限流

1.2 核心术语

术语说明示例
Policy策略定义Policy. Handle<Exception>()
Execute同步执行policy.Execute(() => DoWork())
ExecuteAsync异步执行await policy.ExecuteAsync(async () => await DoWorkAsync())
Context上下文数据在策略间传递元数据
PolicyKey策略标识用于日志和监控
Result返回值处理Policy<HttpResponseMessage>

1.3 安装包

bash

# 核心包
dotnet add package Polly

# 扩展包
dotnet add package Polly.Extensions.Http          # HTTP 集成
dotnet add package Polly. Caching.Memory          # 内存缓存
dotnet add package Polly.Caching.Distributed     # 分布式缓存
dotnet add package Polly. Contrib.Simmy           # 混沌工程

# Polly v8(新版本)
dotnet add package Polly.Core
dotnet add package Polly.Extensions

二、七大策略详解

2.1 重试(Retry)

2.1.1 基础用法

C#

// 简单重试 3 次
Policy
    .Handle<HttpRequestException>()
    .Retry(3);

// 永久重试(慎用)
Policy
    .Handle<Exception>()
    .RetryForever();

// 带回调的重试
Policy
    .Handle<Exception>()
    .Retry(3, onRetry: (exception, retryCount) =>
    {
        Log.Warning($"第 {retryCount} 次重试,原因:{exception.Message}");
    });
2.1.2 等待后重试(Wait and Retry)

C#

// 固定间隔(每次等待 2 秒)
Policy
    .Handle<Exception>()
    .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(2));

// 线性递增(2s, 4s, 6s)
Policy
    .Handle<Exception>()
    .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt * 2));

// 指数退避(2s, 4s, 8s)
Policy
    .Handle<Exception>()
    .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(Math. Pow(2, retryAttempt)));

// 指数退避 + 抖动(Jitter)★ 推荐
var jitterer = new Random();
Policy
    .Handle<Exception>()
    .WaitAndRetry(3, retryAttempt =>
        TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) 
        + TimeSpan.FromMilliseconds(jitterer. Next(0, 100))
    );
2.1.3 条件重试

C#

// 仅重试特定异常
Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .Retry(3);

// 条件过滤
Policy
    .Handle<HttpRequestException>(ex => 
        ex.StatusCode == HttpStatusCode.RequestTimeout ||
        ex.StatusCode == HttpStatusCode.TooManyRequests
    )
    .Retry(3);

// 基于返回值重试(Polly<TResult>)
Policy<HttpResponseMessage>
    .HandleResult(r => ! r.IsSuccessStatusCode)
    .Or<HttpRequestException>()
    .RetryAsync(3);
2.1.4 高级回调

C#

Policy
    .Handle<Exception>()
    .WaitAndRetryAsync(
        3,
        retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
        onRetry: (outcome, timespan, retryCount, context) =>
        {
            // outcome. Exception 或 outcome.Result
            Log.Warning($"[{context.PolicyKey}] 第 {retryCount} 次重试");
            Log.Warning($"延迟:{timespan.TotalMilliseconds}ms");
            Log.Warning($"异常:{outcome.Exception?.Message}");
            
            // 记录指标
            Metrics. IncrementCounter("polly.retry", new[] {
                new KeyValuePair<string, object>("policy", context.PolicyKey),
                new KeyValuePair<string, object>("attempt", retryCount)
            });
        }
    );
2.1.5 重试最佳实践

应该重试

  • 网络波动(HttpRequestException
  • 超时(TimeoutException
  • 瞬态错误(5xx、408、429)
  • 数据库连接失败

不应该重试

  • 客户端错误(4xx,除了 408/429)
  • 业务逻辑异常(ArgumentException
  • 认证失败(401/403)
  • 资源不存在(404)

C#

// 智能重试策略
Policy
    .Handle<HttpRequestException>(ex =>
    {
        var statusCode = ex.StatusCode;
        return statusCode == HttpStatusCode.RequestTimeout          // 408
            || statusCode == HttpStatusCode.TooManyRequests         // 429
            || (int)statusCode >= 500;                              // 5xx
    })
    .Or<TimeoutException>()
    .WaitAndRetryAsync(
        3,
        retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
                      + TimeSpan.FromMilliseconds(new Random().Next(0, 100))
    );

2.2 断路器(Circuit Breaker)

2.2.1 基础断路器

C#

// 连续失败 2 次后断开 30 秒
Policy
    . Handle<Exception>()
    .CircuitBreaker(
        handledEventsAllowedBeforeBreaking:  2,
        durationOfBreak: TimeSpan.FromSeconds(30)
    );
2.2.2 高级断路器(Advanced Circuit Breaker)

C#

var breaker = Policy
    .Handle<HttpRequestException>()
    .AdvancedCircuitBreakerAsync(
        failureThreshold: 0.5,              // 失败率阈值(50%)
        samplingDuration: TimeSpan. FromSeconds(10),  // 采样窗口
        minimumThroughput: 8,               // 最小吞吐量(至少 8 次请求)
        durationOfBreak: TimeSpan.FromSeconds(30),   // 断开持续时间
        
        onBreak: (outcome, duration) =>
        {
            Log. Error($"断路器打开!持续 {duration.TotalSeconds}s");
            Log.Error($"原因:{outcome.Exception?.Message}");
            Metrics.IncrementCounter("circuit_breaker.open");
        },
        
        onReset:  () =>
        {
            Log.Info("断路器已关闭,恢复正常");
            Metrics.IncrementCounter("circuit_breaker.reset");
        },
        
        onHalfOpen: () =>
        {
            Log.Warning("断路器半开,发送探测请求");
            Metrics.IncrementCounter("circuit_breaker.half_open");
        }
    );
2.2.3 断路器状态机

Code

┌─────────────────────────────────────────────────┐
│              Closed(关闭 - 正常状态)              │
│  ┌─────────────────────────────────────────┐   │
│  │ 正常处理请求                              │   │
│  │ 统计失败率                                │   │
│  └─────────────────────────────────────────┘   │
└──────────────┬──────────────────────────────────┘
               │ 失败率 > 阈值
               ↓
┌─────────────────────────────────────────────────┐
│               Open(打开 - 断开状态)              │
│  ┌─────────────────────────────────────────┐   │
│  │ 立即拒绝所有请求                          │   │
│  │ 抛出 BrokenCircuitException              │   │
│  │ 等待 durationOfBreak                     │   │
│  └─────────────────────────────────────────┘   │
└──────────────┬──────────────────────────────────┘
               │ 等待时间结束
               ↓
┌─────────────────────────────────────────────────┐
│            Half-Open(半开 - 探测状态)            │
│  ┌──────────────────────────��──────────────┐   │
│  │ 允许 1 个测试请求                         │   │
│  │ ├─ 成功 → 转到 Closed                    │   │
│  │ └─ 失败 → 转回 Open                      │   │
│  └─────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘
2.2.4 断路器监控

C#

var breaker = Policy
    .Handle<Exception>()
    .CircuitBreakerAsync(2, TimeSpan.FromSeconds(30));

// 检查状态
switch (breaker.CircuitState)
{
    case CircuitState.Closed:
        // 正常
        break;
    case CircuitState.Open:
        // 断开
        break;
    case CircuitState.HalfOpen:
        // 半开
        break;
    case CircuitState.Isolated:
        // 手动隔离
        break;
}

// 手动操作
breaker. Isolate();  // 手动打开
breaker.Reset();    // 手动重置
2.2.5 分布式断路器

C#

// 使用 Redis 共享断路器状态
public class RedisCircuitBreakerStateStore : ICircuitBreakerStateStore
{
    private readonly IDistributedCache _cache;
    
    public async Task<CircuitBreakerState> GetStateAsync(string key)
    {
        var state = await _cache.GetStringAsync(key);
        return JsonSerializer.Deserialize<CircuitBreakerState>(state);
    }
    
    public async Task SetStateAsync(string key, CircuitBreakerState state)
    {
        await _cache.SetStringAsync(
            key, 
            JsonSerializer.Serialize(state),
            new DistributedCacheEntryOptions 
            { 
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) 
            }
        );
    }
}

2.3 超时(Timeout)

产生TimeoutRejectedException类型的抛错

2.3.1 两种超时策略

C#

// 乐观超时(Optimistic)- 依赖 CancellationToken
Policy
    . TimeoutAsync(
        TimeSpan.FromSeconds(30),
        TimeoutStrategy. Optimistic,
        onTimeoutAsync: async (context, timespan, task) =>
        {
            Log.Warning($"操作超时:{timespan.TotalSeconds}s");
            await Metrics.RecordTimeoutAsync(context.PolicyKey);
        }
    );

// 悲观超时(Pessimistic)- 强制终止
Policy
    .TimeoutAsync(
        TimeSpan. FromSeconds(30),
        TimeoutStrategy.Pessimistic  // 不等待任务完成,直接抛异常
    );
2.3.2 超时策略对比
策略工作原理优点缺点适用场景
Optimistic通过 CancellationToken 通知任务取消优雅退出,资源清理需要代码支持取消异步 I/O 操作
Pessimistic直接抛 TimeoutRejectedException强制终止,不依赖代码可能导致资源泄漏不可控的同步操作
2.3.3 超时最佳实践

C#

// ✅ 正确:传递 CancellationToken
var timeoutPolicy = Policy. TimeoutAsync(
    TimeSpan.FromSeconds(10),
    TimeoutStrategy. Optimistic
);

await timeoutPolicy.ExecuteAsync(async ct =>
{
    using var httpClient = new HttpClient();
    return await httpClient.GetAsync("https://api.example.com", ct);
}, cancellationToken);

// ❌ 错误:未传递 CancellationToken
await timeoutPolicy.ExecuteAsync(async () =>
{
    using var httpClient = new HttpClient();
    return await httpClient.GetAsync("https://api.example.com");
    // 超时后请求仍在执行!
});
2.3.4 分层超时

C#

// 每次重试 5 秒超时,总共 20 秒超时
var perTryTimeout = Policy. TimeoutAsync(TimeSpan.FromSeconds(5));
var overallTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(20));
var retry = Policy.Handle<Exception>().RetryAsync(3);

// 正确顺序:外层总超时 → 重试 → 内层单次超时
var policy = Policy. WrapAsync(overallTimeout, retry, perTryTimeout);

2.4 隔离(Bulkhead Isolation)

2.4.1 基础隔离

C#

// 最多 10 个并发请求
Policy
    .BulkheadAsync(
        maxParallelization: 10,
        maxQueuingActions: 0,  // 不排队,直接拒绝
        onBulkheadRejectedAsync: async context =>
        {
            Log. Warning("隔离舱已满,拒绝请求");
            await Metrics.IncrementCounterAsync("bulkhead.rejected");
        }
    );
2.4.2 带队列的隔离

C#

Policy
    .BulkheadAsync(
        maxParallelization: 10,    // 最多 10 个并发
        maxQueuingActions: 20,     // 队列容量 20
        onBulkheadRejectedAsync: async context =>
        {
            // 当队列也满时触发
            throw new BulkheadRejectedException("系统繁忙,请稍后重试");
        }
    );
2.4.3 计算合理的并发数

C#

// 公式:并发数 = (目标 TPS × 平均响应时间) / 1000
// 示例:
// - 目标:1000 TPS
// - 平均响应时间:50ms
// - 并发数 = (1000 × 50) / 1000 = 50

// 实际设置建议留 20% 缓冲
var maxConcurrency = (int)((1000 * 0.05) * 1.2); // = 60

var bulkhead = Policy. BulkheadAsync(
    maxParallelization: maxConcurrency,
    maxQueuingActions: maxConcurrency * 2  // 队列 = 并发数 × 2
);
2.4.4 监控隔离状态

C#

var bulkhead = Policy. BulkheadAsync(10, 20);

// 获取当前状态
var availableSlots = bulkhead.BulkheadAvailableCount;  // 可用槽位
var queuedActions = bulkhead.QueuedActions;            // 队列中的请求数

// 定期上报指标
Metrics. Gauge("bulkhead.available", availableSlots);
Metrics.Gauge("bulkhead.queued", queuedActions);
2.4.5 隔离 vs 限流

C#

// Bulkhead:限制并发执行数
var bulkhead = Policy.BulkheadAsync(10);

// Rate Limiter:限制时间窗口内的请求总数(需要 Polly v8)
var rateLimiter = new ResiliencePipelineBuilder()
    .AddRateLimiter(new RateLimiterStrategyOptions
    {
        RateLimiter = args =>
        {
            return new ConcurrencyLimiter(new ConcurrencyLimiterOptions
            {
                PermitLimit = 100,  // 每秒 100 个请求
                QueueLimit = 0
            }).AcquireAsync(1, args.Context. CancellationToken);
        }
    })
    .Build();

2.5 降级/回退(Fallback)

2.5.1 基础降级

C#

// 返回默认值
Policy<string>
    .Handle<Exception>()
    .FallbackAsync("默认值");

// 执行降级逻辑
Policy<UserProfile>
    .Handle<HttpRequestException>()
    .FallbackAsync(
        fallbackAction: async ct =>
        {
            Log. Warning("主服务失败,使用缓存数据");
            return await GetCachedUserProfile();
        },
        onFallbackAsync: async outcome =>
        {
            Log. Error($"触发降级:{outcome.Exception?.Message}");
            await Metrics.IncrementCounterAsync("fallback. triggered");
        }
    );
2.5.2 多级降级

C#

// 降级链:主服务 → 备用服务 → 缓存 → 默认值
var primaryPolicy = Policy<Data>
    .Handle<Exception>()
    .FallbackAsync(async ct => await GetFromBackupService());

var backupPolicy = Policy<Data>
    .Handle<Exception>()
    .FallbackAsync(async ct => await GetFromCache());

var cachePolicy = Policy<Data>
    .Handle<Exception>()
    .FallbackAsync(new Data { IsDefault = true });

var pipeline = Policy.WrapAsync(primaryPolicy, backupPolicy, cachePolicy);
2.5.3 基于上下文的降级

C#

Policy<HttpResponseMessage>
    .Handle<Exception>()
    .FallbackAsync(
        fallbackAction: async (outcome, context, ct) =>
        {
            var userId = context["UserId"];
            var isCritical = context. GetBoolean("IsCritical");
            
            if (isCritical)
            {
                // 关键用户返回有限功能
                return await GetLimitedResponse(userId);
            }
            else
            {
                // 普通用户返回静态页面
                return GetStaticResponse();
            }
        }
    );

// 使用时
await policy.ExecuteAsync(
    (context, ct) => CallExternalApi(ct),
    new Context("RequestKey") 
    { 
        ["UserId"] = "user123",
        ["IsCritical"] = true
    },
    cancellationToken
);

2.6 缓存(Cache)

2.6.1 内存缓存

C#

// 安装:Polly. Caching. Memory
var memoryCacheProvider = new MemoryCacheProvider(new MemoryCache(new MemoryCacheOptions()));

var cachePolicy = Policy. CacheAsync(
    memoryCacheProvider,
    TimeSpan.FromMinutes(5)
);

var result = await cachePolicy.ExecuteAsync(
    async context => await GetExpensiveData(),
    new Context("CacheKey")
);
2.6.2 分布式缓存(Redis)

C#

// 安装:Polly.Caching.Distributed
public class RedisCacheProvider : IAsyncCacheProvider
{
    private readonly IDistributedCache _cache;
    
    public async Task<(bool, TResult)> TryGetAsync<TResult>(
        string key, 
        CancellationToken ct)
    {
        var bytes = await _cache.GetAsync(key, ct);
        if (bytes == null) return (false, default);
        
        var value = JsonSerializer.Deserialize<TResult>(bytes);
        return (true, value);
    }
    
    public async Task PutAsync<TResult>(
        string key, 
        TResult value, 
        Ttl ttl, 
        CancellationToken ct)
    {
        var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
        await _cache. SetAsync(
            key, 
            bytes, 
            new DistributedCacheEntryOptions 
            { 
                AbsoluteExpirationRelativeToNow = ttl. Timespan 
            },
            ct
        );
    }
}
2.6.3 缓存策略

C#

// 绝对过期
var absoluteCache = Policy.CacheAsync(
    cacheProvider,
    TimeSpan.FromMinutes(5)  // 5 分钟后过期
);

// 滑动过期
var slidingCache = Policy.CacheAsync(
    cacheProvider,
    new RelativeTtl(TimeSpan.FromMinutes(5))  // 每次访问刷新
);

// 动态 TTL
var dynamicCache = Policy.CacheAsync(
    cacheProvider,
    (context, result) =>
    {
        // 根据结果决定缓存时间
        if (result is CriticalData)
            return new Ttl(TimeSpan.FromMinutes(1));
        else
            return new Ttl(TimeSpan. FromHours(1));
    }
);
2.6.4 缓存键策略

C#

var cachePolicy = Policy.CacheAsync(
    cacheProvider,
    TimeSpan.FromMinutes(5),
    (context) =>
    {
        // 动态生成缓存键
        var userId = context["UserId"];
        var region = context["Region"];
        return $"user:{userId}:region:{region}";
    }
);

// 使用
await cachePolicy.ExecuteAsync(
    async context => await GetUserData(context["UserId"]),
    new Context("GetUser") 
    { 
        ["UserId"] = "123",
        ["Region"] = "US"
    }
);
2.6.5 缓存穿透防护

C#

// 缓存空值,防止缓存穿透
var cachePolicy = Policy<Data>.CacheAsync(
    cacheProvider,
    TimeSpan.FromMinutes(5),
    onCacheGet: (context, key) => Log.Debug($"缓存命中:{key}"),
    onCacheMiss: (context, key) => Log.Debug($"缓存未命中:{key}"),
    onCachePut: (context, key) => Log.Debug($"写入缓存:{key}")
);

// 空值缓存(短期)
var result = await cachePolicy.ExecuteAsync(async () =>
{
    var data = await GetData();
    return data ??  new Data { IsEmpty = true };  // 缓存空对象
}, new Context("DataKey"));

2.7 限流(Rate Limiter - Polly v8)

2.7.1 固定窗口限流

C#

var rateLimiter = new ResiliencePipelineBuilder()
    .AddRateLimiter(new RateLimiterStrategyOptions
    {
        RateLimiter = args => new FixedWindowRateLimiter(
            new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,                    // 每窗口 100 个请求
                Window = TimeSpan.FromSeconds(1),     // 窗口 1QueueLimit = 0                        // 不排队
            }
        ).AcquireAsync(1, args.Context.CancellationToken)
    })
    .Build();
2.7.2 滑动窗口限流

C#

var rateLimiter = new ResiliencePipelineBuilder()
    .AddRateLimiter(new RateLimiterStrategyOptions
    {
        RateLimiter = args => new SlidingWindowRateLimiter(
            new SlidingWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromSeconds(1),
                SegmentsPerWindow = 10  // 分成 10 个子窗口
            }
        ).AcquireAsync(1, args.Context.CancellationToken)
    })
    .Build();
2.7.3 令牌桶限流

C#

var rateLimiter = new ResiliencePipelineBuilder()
    .AddRateLimiter(new RateLimiterStrategyOptions
    {
        RateLimiter = args => new TokenBucketRateLimiter(
            new TokenBucketRateLimiterOptions
            {
                TokenLimit = 100,                          // 桶容量
                TokensPerPeriod = 10,                      // ��周期补充 10 个令牌
                ReplenishmentPeriod = TimeSpan.FromSeconds(1)
            }
        ).AcquireAsync(1, args.Context.CancellationToken)
    })
    .Build();

二零、Handle重点说明(踩大坑!!!)

二一、HttpPolicyExtensions 与Policy

核心区别

特性HttpPolicyExtensionsPolicy
适用场景专为 HttpClient 设计通用策略(数据库、消息队列等)
返回类型PolicyBuilder<HttpResponseMessage>PolicyBuilderPolicyBuilder<T>
便捷性✅ 开箱即用(一行代码)⚠️ 需要手动配置
灵活性⚠️ 仅限 HTTP 场景✅ 适用任何场景
所属包Polly.Extensions. HttpPolly(核心包)

🎯 决策指南

1. 使用 HttpPolicyExtensions 的场景

推荐使用

C#

// ✅ 场景:调用外部 HTTP API
var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()  // 自动处理 5xx、408、网络错误
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));

// ✅ 场景:与 IHttpClientFactory 集成
services.AddHttpClient("MyClient")
    .AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError().RetryAsync(3));

优点

  • 🚀 一行代码搞定常见 HTTP 错误
  • 🎯 自动处理 HttpRequestException、5xx、408
  • 🔧 IHttpClientFactory 无缝集成

适用场景

  • 调用 REST API
  • 微服务间通信
  • 第三方 HTTP 服务集成

2. 使用 Policy 的场景

推荐使用

C#

// ✅ 场景:数据库操作
var dbPolicy = Policy
    .Handle<SqlException>(ex => ex.Number == 1205)  // 死锁
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(attempt));

// ✅ 场景:消息队列
var mqPolicy = Policy
    .Handle<RabbitMQException>()
    .RetryAsync(5);

// ✅ 场景:文件系统操作
var filePolicy = Policy
    .Handle<IOException>()
    .WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(100));

优点

  • 🎯 完全控制错误处理逻辑
  • 🔧 适用于任何异步/同步操作
  • 💪 支持复杂条件判断

适用场景

  • 数据库操作(EF Core、Dapper)
  • 消息队列(RabbitMQ、Kafka)
  • 文件系统操作
  • 自定义业务逻辑

三、策略组合与执行顺序

3.1 Policy Wrap 基础

C#

var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(10));
var retry = Policy.Handle<Exception>().RetryAsync(3);
var fallback = Policy.Handle<Exception>().FallbackAsync(() => "默认值");

// 从外到内包装
var pipeline = Policy.WrapAsync(fallback, retry, timeout);

// 执行顺序:fallback → retry → timeout → 实际操作
await pipeline.ExecuteAsync(async () => await DoWork());

3.2 策略顺序的黄金法则

C#

// ✅ 推荐顺序(从外到内)
var policy = Policy.WrapAsync(
    fallback,         // 1. 最外层:所有策略失败后的兜底
    circuitBreaker,   // 2. 快速失败,避免无效重试
    retry,            // 3. 重试逻辑
    timeout           // 4. 最内层:每次操作的超时控制
);

// ❌ 错误顺序
var badPolicy = Policy.WrapAsync(
    timeout,          // 错误:超时在最外层,导致多次重试共享一个总超时
    retry,
    circuitBreaker    // 错误:断路器在内层,无法阻止重试
);

3.3 复杂策略组合示例

C#

// 完整的弹性管道
public class ResiliencePipeline
{
    public static IAsyncPolicy<HttpResponseMessage> Create()
    {
        // 1. 降级策略
        var fallback = Policy<HttpResponseMessage>
            .Handle<Exception>()
            .FallbackAsync(
                new HttpResponseMessage(HttpStatusCode.OK)
                {
                    Content = new StringContent("{"message": "服务降级中"}")
                }
            );
        
        // 2. 断路器
        var circuitBreaker = Policy<HttpResponseMessage>
            . Handle<Exception>()
            .OrResult(r => ! r.IsSuccessStatusCode)
            . AdvancedCircuitBreakerAsync(
                failureThreshold: 0.5,
                samplingDuration: TimeSpan. FromSeconds(10),
                minimumThroughput: 8,
                durationOfBreak: TimeSpan.FromSeconds(30)
            );
        
        // 3. 重试策略(带抖动)
        var jitterer = new Random();
        var retry = Policy<HttpResponseMessage>
            . Handle<HttpRequestException>()
            .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
            .WaitAndRetryAsync(
                3,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
                              + TimeSpan.FromMilliseconds(jitterer.Next(0, 100))
            );
        
        // 4. 超时策略
        var timeout = Policy. TimeoutAsync<HttpResponseMessage>(
            TimeSpan.FromSeconds(10),
            TimeoutStrategy.Optimistic
        );
        
        // 5. 隔离策略
        var bulkhead = Policy. BulkheadAsync<HttpResponseMessage>(
            maxParallelization: 10,
            maxQueuingActions: 20
        );
        
        // 组合顺序:fallback → circuit → retry → bulkhead → timeout
        return Policy.WrapAsync(fallback, circuitBreaker, retry, bulkhead, timeout);
    }
}

3.4 不同场景的策略组合

场景 1:关键业务 API

C#

// 特点:低容错、需快速反馈
var criticalPolicy = Policy. WrapAsync(
    Policy.Handle<Exception>().FallbackAsync(() => AlertOps()),  // 立即告警
    Policy.TimeoutAsync(TimeSpan.FromSeconds(2)),                // 严格超时
    Policy.Handle<Exception>().RetryAsync(1)                     // 最多重试 1 次
);
场景 2:后台批处理

C#

// 特点:可容忍延迟,需保证成功
var batchPolicy = Policy.WrapAsync(
    Policy.Handle<Exception>().WaitAndRetryAsync(10, r => TimeSpan.FromMinutes(r)),
    Policy.TimeoutAsync(TimeSpan.FromMinutes(5))
);
场景 3:第三方 API 调用

C#

// 特点:不可控,需隔离影响
var thirdPartyPolicy = Policy.WrapAsync(
    fallback,              // 降级到备用数据
    circuitBreaker,        // 防止雪崩
    retry,                 // 应对瞬时故障
    bulkhead,              // 限制并发
    timeout                // 防止长时间等待
);

四、高级特性

4.1 Context 上下文传递

C#

// 在策略间共享数据
var policy = Policy
    .Handle<Exception>()
    .RetryAsync(3, onRetry: (exception, retryCount, context) =>
    {
        var requestId = context["RequestId"];
        var userId = context["UserId"];
        Log.Warning($"[{requestId}] 用户 {userId} 的请求重试第 {retryCount} 次");
    });

// 执行时传入
await policy.ExecuteAsync(
    async context =>
    {
        var userId = context["UserId"];
        return await ProcessUser(userId);
    },
    new Context("MyOperation")
    {
        ["RequestId"] = Guid.NewGuid().ToString(),
        ["UserId"] = "user123"
    }
);

4.2 PolicyRegistry 策略注册

C#

// Startup.cs
services.AddPolicyRegistry((registry) =>
{
    registry.Add("StandardRetry", Policy
        .Handle<Exception>()
        .WaitAndRetryAsync(3, r => TimeSpan.FromSeconds(Math.Pow(2, r))));
    
    registry.Add("AggressiveRetry", Policy
        . Handle<Exception>()
        .WaitAndRetryAsync(10, r => TimeSpan.FromSeconds(r)));
});

// 使用
public class MyService
{
    private readonly IPolicyRegistry<string> _registry;
    
    public MyService(IPolicyRegistry<string> registry)
    {
        _registry = registry;
    }
    
    public async Task DoWork()
    {
        var policy = _registry.Get<IAsyncPolicy>("StandardRetry");
        await policy.ExecuteAsync(() => CallApi());
    }
}

4.3 动态策略选择

C#

public class DynamicPolicySelector
{
    public IAsyncPolicy GetPolicy(RequestContext context)
    {
        return context.Priority switch
        {
            Priority.Critical => CreateCriticalPolicy(),
            Priority.High => CreateHighPolicy(),
            Priority.Normal => CreateNormalPolicy(),
            _ => CreateDefaultPolicy()
        };
    }
    
    private IAsyncPolicy CreateCriticalPolicy()
    {
        return Policy. WrapAsync(
            Policy.TimeoutAsync(1),
            Policy.Handle<Exception>().RetryAsync(1)
        );
    }
    
    private IAsyncPolicy CreateNormalPolicy()
    {
        return Policy.WrapAsync(
            Policy.TimeoutAsync(10),
            Policy.Handle<Exception>().RetryAsync(3),
            Policy.BulkheadAsync(100)
        );
    }
}

4.4 结果处理(HandleResult)

C#

// 处���返回值而非异常
var policy = Policy<HttpResponseMessage>
    .HandleResult(r => !r.IsSuccessStatusCode)  // 处理非成功响应
    .Or<HttpRequestException>()                 // 或异常
    .RetryAsync(3);

// 复杂条件
var advancedPolicy = Policy<ApiResponse>
    .HandleResult(r => 
        r.StatusCode >= 500 ||                  // 服务器错误
        r. Data == null ||                       // 无数据
        r.Data. IsStale                          // 数据过期
    )
    .FallbackAsync(new ApiResponse { IsDefault = true });

4.5 NoOp 策略(无操作)

C#

// 开发环境禁用策略
var policy = isProduction 
    ? Policy.Handle<Exception>().RetryAsync(3)
    : Policy.NoOpAsync();  // 不做任何处理

// 或使用条件包装
var conditionalPolicy = shouldUsePolicy
    ? actualPolicy
    : Policy.NoOpAsync();

五、性能优化

5.1 避免策略重复创建

C#

// ❌ 错误:每次请求都创建新策略
public async Task BadExample()
{
    var policy = Policy.Handle<Exception>().RetryAsync(3);  // 重复创建
    await policy.ExecuteAsync(() => DoWork());
}

// ✅ 正确:复用策略实例
private static readonly IAsyncPolicy _retryPolicy = 
    Policy.Handle<Exception>().RetryAsync(3);

public async Task GoodExample()
{
    await _retryPolicy.ExecuteAsync(() => DoWork());
}

5.2 使用 PolicyRegistry 集中管理

C#

// 注册时创建一次
services.AddPolicyRegistry(registry =>
{
    registry. Add("Retry", CreateRetryPolicy());
    registry.Add("CircuitBreaker", CreateCircuitBreakerPolicy());
});

// 运行时复用
private readonly IAsyncPolicy _policy;

public MyService(IPolicyRegistry<string> registry)
{
    _policy = registry.Get<IAsyncPolicy>("Retry");
}

5.3 异步策略优先

C#

// ✅ 使用异步版本
var asyncPolicy = Policy.Handle<Exception>().RetryAsync(3);
await asyncPolicy.ExecuteAsync(async () => await DoWorkAsync());

// ❌ 避免同步包装异步
var syncPolicy = Policy.Handle<Exception>().Retry(3);
syncPolicy.Execute(() => DoWorkAsync().GetAwaiter().GetResult());  // 可能死锁

5.4 减少不必要的包装

C#

// ❌ 过度包装
var overWrapped = Policy.WrapAsync(
    Policy.NoOpAsync(),  // 无用
    Policy.Handle<Exception>().RetryAsync(3),
    Policy.NoOpAsync()   // 无用
);

// ✅ 精简包装
var efficient = Policy.Handle<Exception>().RetryAsync(3);

5.5 监控策略性能

C#

var policy = Policy
    .Handle<Exception>()
    .RetryAsync(3, onRetry: (ex, retryCount, context) =>
    {
        var sw = context.Get<Stopwatch>("Stopwatch");
        Metrics.RecordRetry(sw.ElapsedMilliseconds, retryCount);
    });

await policy.ExecuteAsync(
    async context =>
    {
        var sw = Stopwatch.StartNew();
        context["Stopwatch"] = sw;
        return await DoWork();
    },
    new Context("Operation")
);

六、监控与可观测性

6.1 结构化日志

C#

var policy = Policy
    .Handle<Exception>()
    .WaitAndRetryAsync(
        3,
        r => TimeSpan.FromSeconds(Math.Pow(2, r)),
        onRetry: (outcome, timespan, retryCount, context) =>
        {
            _logger.LogWarning(
                "策略 {PolicyKey} 第 {RetryCount} 次重试," +
                "延迟 {DelayMs}ms,操作 {OperationKey}," +
                "异常:{ExceptionType} - {ExceptionMessage}",
                context.PolicyKey,
                retryCount,
                timespan. TotalMilliseconds,
                context.OperationKey,
                outcome.Exception?.GetType().Name,
                outcome. Exception?.Message
            );
        }
    )
    .WithPolicyKey("MyRetryPolicy");

6.2 指标收集

C#

public class PollyMetrics
{
    private readonly IMeterFactory _meterFactory;
    private readonly Counter<long> _retryCounter;
    private readonly Histogram<double> _retryDuration;
    
    public PollyMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("Polly");
        _retryCounter = meter.CreateCounter<long>("polly.retry.count");
        _retryDuration = meter.CreateHistogram<double>("polly.retry. duration");
    }
    
    public void RecordRetry(string policyKey, int attempt, double durationMs)
    {
        _retryCounter.Add(1, new KeyValuePair<string, object>("policy", policyKey));
        _retryDuration.Record(durationMs, 
            new KeyValuePair<string, object>("policy", policyKey),
            new KeyValuePair<string, object>("attempt", attempt)
        );
    }
}

6.3 OpenTelemetry 集成

C#

services.AddOpenTelemetry()
    .WithMetrics(builder => builder
        .AddMeter("Polly")
        .AddPrometheusExporter()
    )
    .WithTracing(builder => builder
        .AddSource("Polly")
        .AddJaegerExporter()
    );

// 在策略中使用
var policy = Policy
    .Handle<Exception>()
    .RetryAsync(3, onRetry: (ex, count, context) =>
    {
        using var activity = Activity.StartActivity("PollyRetry");
        activity?.SetTag("retry. count", count);
        activity?. SetTag("policy. key", context.PolicyKey);
    });

6.4 监控仪表盘关键指标

C#

// Prometheus 查询示例
// 1. 重试���
rate(polly_retry_count_total[5m])

// 2. 断路器状态
polly_circuit_breaker_state{state="open"}

// 3. 隔离拒绝率
rate(polly_bulkhead_rejected_total[1m])

// 4. 降级触发次数
increase(polly_fallback_count_total[1h])

// 5. P99 重试延迟
histogram_quantile(0.99, polly_retry_duration_seconds_bucket)

七、实战模式

7.1 HttpClient 集成

C#

// Startup.cs
services.AddHttpClient("ResilientClient")
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy())
    .AddPolicyHandler(GetTimeoutPolicy());

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()  // 处理 5xx 和 408
        .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
        .WaitAndRetryAsync(
            3,
            retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
        );
}

static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}

static IAsyncPolicy<HttpResponseMessage> GetTimeoutPolicy()
{
    return Policy. TimeoutAsync<HttpResponseMessage>(10);
}

// 使用
public class MyService
{
    private readonly IHttpClientFactory _factory;
    
    public async Task<Data> GetData()
    {
        var client = _factory.CreateClient("ResilientClient");
        var response = await client.GetAsync("https://api.example.com/data");
        return await response.Content.ReadFromJsonAsync<Data>();
    }
}

7.2 数据库操作

C#

var dbPolicy = Policy
    .Handle<SqlException>(ex => 
        ex.Number == 1205 ||  // 死锁
        ex.Number == -2       // 超时
    )
    .Or<TimeoutException>()
    .WaitAndRetryAsync(
        3,
        retryAttempt => TimeSpan.FromSeconds(retryAttempt),
        onRetry: (ex, timespan, retryCount, context) =>
        {
            _logger.LogWarning("数据库操作第 {RetryCount} 次重试", retryCount);
        }
    );

// 使用
await dbPolicy.ExecuteAsync(async () =>
{
    await using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();
    // 执行 SQL
});

7.3 消息队列处理

C#

var messagePolicy = Policy
    .Handle<Exception>()
    .WaitAndRetryAsync(
        5,
        retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
        onRetry: (ex, timespan, retryCount, context) =>
        {
            if (retryCount >= 5)
            {
                // 最后一次重试失败,发送到死信队列
                _deadLetterQueue.Send(context["Message"]);
            }
        }
    );

await messagePolicy.ExecuteAsync(
    async context =>
    {
        var message = context. Get<Message>("Message");
        await ProcessMessage(message);
    },
    new Context { ["Message"] = message }
);

7.4 分布式缓存 + 降级

C#

var pipeline = Policy. WrapAsync(
    // 1. 降级:缓存失败时查询数据库
    Policy<Data>
        .Handle<RedisException>()
        .FallbackAsync(async ct => await _database.GetData()),
    
    // 2. 缓存策略
    Policy.CacheAsync(
        _redisCacheProvider,
        TimeSpan.FromMinutes(5)
    ),
    
    // 3. 重试:应对网络抖动
    Policy
        .Handle<RedisException>()
        .WaitAndRetryAsync(2, r => TimeSpan.FromMilliseconds(100))
);

var data = await pipeline.ExecuteAsync(
    async () => await _redisCache.Get("key"),
    new Context("DataCache")
);

八、常见陷阱与解决方案

8.1 陷阱 1:重试风暴

C#

// ❌ 问题:多个客户端同时重试导致服务器过载
var badRetry = Policy
    .Handle<Exception>()
    .WaitAndRetry(3, r => TimeSpan.FromSeconds(r));  // 固定间隔

// ✅ 解决:添加抖动
var jitterer = new Random();
var goodRetry = Policy
    .Handle<Exception>()
    .WaitAndRetry(3, r => 
        TimeSpan.FromSeconds(r) + 
        TimeSpan.FromMilliseconds(jitterer.Next(0, 1000))
    );

8.2 陷阱 2:断路器不断开

C#

// ❌ 问题:阈值设置过高
var badBreaker = Policy
    .Handle<Exception>()
    .CircuitBreaker(100, TimeSpan.FromMinutes(1));  // 需要 100 次失败

// ✅ 解决:使用高级断路器 + 合理阈值
var goodBreaker = Policy
    .Handle<Exception>()
    .AdvancedCircuitBreaker(
        failureThreshold: 0.5,        // 50% 失败率
        samplingDuration: TimeSpan. FromSeconds(10),
        minimumThroughput:  10         // 至少 10 次请求
    );

8.3 陷阱 3:超时未生效

C#

// ❌ 问题:未传递 CancellationToken
var timeoutPolicy = Policy. TimeoutAsync(5, TimeoutStrategy.Optimistic);
await timeoutPolicy.ExecuteAsync(async () =>
{
    using var client = new HttpClient();
    return await client.GetAsync("https://slow-api.com");  // 超时无效!
});

// ✅ 解决:传递 CancellationToken
await timeoutPolicy.ExecuteAsync(async ct =>
{
    using var client = new HttpClient();
    return await client.GetAsync("https://slow-api.com", ct);
}, CancellationToken.None);

8.4 陷阱 4:策略顺序错误

C#

// ❌ 问题:超时在重试外层
var bad = Policy. WrapAsync(
    Policy.TimeoutAsync(10),  // 总共 10 秒
    Policy.Handle<Exception>().RetryAsync(5)  // 可能每次重试 2 秒,总共超时
);

// ✅ 解决:超时在重试内层
var good = Policy.WrapAsync(
    Policy.Handle<Exception>().RetryAsync(5),
    Policy.TimeoutAsync(2)  // 每次重试 2 秒
);

8.5 陷阱 5:忘记处理 BrokenCircuitException

C#

// ✅ 捕获断路器异常
try
{
    await circuitBreakerPolicy.ExecuteAsync(() => CallApi());
}
catch (BrokenCircuitException ex)
{
    _logger.LogError("断路器打开,服务不可用");
    return GetCachedResponse();  // 返回缓存数据
}
catch (Exception ex)
{
    _logger.LogError(ex, "请求失败");
    throw;
}

8.6 陷阱 6:隔离策略未限制队列

C#

// ❌ 问题:无限队列导致内存溢出
var badBulkhead = Policy. BulkheadAsync(
    maxParallelization: 10,
    maxQueuingActions: int.MaxValue  // 危险!
);

// ✅ 解决:限制队列长度
var goodBulkhead = Policy.BulkheadAsync(
    maxParallelization: 10,
    maxQueuingActions: 20,  // 队列满后拒绝
    onBulkheadRejectedAsync: async context =>
    {
        throw new TooManyRequestsException("系统繁忙");
    }
);

九、Polly v8 新特性

9.1 Resilience Pipeline

C#

// v7 语法
var policyV7 = Policy
    . Handle<Exception>()
    .WaitAndRetryAsync(3, r => TimeSpan.FromSeconds(r));

// v8 语法(推荐)
var pipelineV8 = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(1),
        BackoffType = DelayBackoffType.Exponential,
        OnRetry = args =>
        {
            Console.WriteLine($"重试 {args.AttemptNumber}");
            return ValueTask.CompletedTask;
        }
    })
    .Build();

// 执行
await pipelineV8.ExecuteAsync(
    async ct => await DoWorkAsync(ct),
    cancellationToken
);

9.2 泛型支持

C#

// v8 统一泛型接口
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        ShouldHandle = args => ValueTask.FromResult(
            args. Outcome.Exception is HttpRequestException ||
            args.Outcome.Result?. StatusCode == HttpStatusCode.TooManyRequests
        )
    })
    .Build();

9.3 Hedging 策略(对冲)

C#

// 同时发送多个请求,返回最快的结果
var hedging = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
    {
        MaxHedgedAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(500),
        ActionGenerator = args =>
        {
            return () => new ValueTask<HttpResponseMessage>(
                _httpClient.GetAsync(args.ActionContext.Get<string>("Url"))
            );
        }
    })
    .Build();

9.4 性能改进

C#

// v8 减少内存分配
// - 使用 ValueTask
// - 避免闭包捕获
// - 结构体优化

// 示例:零分配执行
await pipeline.ExecuteAsync(
    static async (state, ct) => await state.DoWorkAsync(ct),
    myState,  // 避免闭包
    cancellationToken
);

十、生产环境检查清单

10.1 策略配置

  • 重试策略添加了抖动(Jitter)
  • 重试仅针对瞬态错误(5xx/408/429)
  • 设置了合理的重试次数(3-5 次)
  • 断路器配置了回调事件(onBreak/onReset)
  • 使用了高级断路器(基于失败率)
  • 超时策略在重试内层
  • 隔离策略设置了队列上限
  • 降级策略提供了有意义的默认值

10.2 监控与日志

  • 记录了所有策略的关键事件(重试、断路、降级)
  • 使用了结构化日志(包含 PolicyKey、OperationKey)
  • 集成了指标收集(Prometheus/Application Insights)
  • 设置了告警规则(断路器打开、重试率过高)
  • 监控了策略性能影响(延迟、吞吐量)

10.3 性能优化

  • 策略实例被复用(static 或 DI 单例)
  • 使用了异步 API(ExecuteAsync)
  • 避免了不必要的策略包装
  • 使用了PolicyRegistry 集中管理
  • 在高并发场景使用了隔离策略

10.4 测试

  • 编写了单元测试(模拟故障场景)
  • 进行了集成测试(真实依赖)
  • 实施了混沌工程(Simmy 注入故障)
  • 验证了降级逻辑的正确性
  • 压测了极限场景(断路器、隔离)

10.5 文档

  • 记录了策略选择理由
  • 说明了参数配置依据(阈值、超时等)
  • 定义了告警响应流程
  • 编写了故障恢复手册

📖 延伸学习资源

  1. 官方文档

  2. 微软指南

  3. 示例代码

  4. 深入主题

    • 混沌工程:Simmy
    • 分布式追踪:OpenTelemetry 集成
    • 高级模式:Saga 模式、Outbox 模式

取消令牌(Cancellation)

1️⃣ 取消令牌的本质

  • CancellationToken = 协作式“开关”

    • 调用方主动取消 → 设置 token 状态为已取消

    • 被调用方检查 token → 决定是否继续执行

    • 关键点:token 本身不执行取消,只是“标记”取消状态

    • 特点

      1. 协作式:无法强制中断,只能被检查和响应
      2. 可传递:可以作为参数传递给异步方法或底层 API
      3. 可组合:多个 token 可用 CancellationTokenSource.CreateLinkedTokenSource 链接
  • 触发取消时:

    • 被调用方可调用 ThrowIfCancellationRequested() → 抛 TaskCanceledExceptionOperationCanceledException

⚡ 核心作用:方法内部主动判断 token 是否取消,决定是否继续执行


2️⃣ .NET 请求上下文的取消感知

  • HttpContext.RequestAborted → 框架自动提供 CancellationToken

  • 触发条件

    1. 客户端关闭浏览器 Tab / 页面刷新
    2. HttpClient 请求超时或调用方取消 token
    3. TCP 连接断开
  • 效果:所有在该请求上下文下执行的方法,如果显式使用 token,都会感知取消

⚡ 这里的 token就像一个 GUID 或标识,关联到 HttpContext 的取消状态,用于跨方法链传递取消信号


3️⃣ 方法链取消传递

  • 入口方法显式接受 CancellationToken
  • 下层方法显示传递 token → 取消信号可以跨多层(可能是 3、4、5 层)传递
  • 被调用方法在任意一层调用 ThrowIfCancellationRequested() → 会抛异常终止执行
  • 未显式传递 token → 下层方法无法感知取消,即使客户端已取消

4️⃣ ABP 框架特性

  • 仓储 / UnitOfWork / EF Core 自动使用 RequestAborted token

  • 效果

    • 入口方法没有 CancellationToken → 仓储方法仍可响应客户端取消
    • 框架内部封装 token,跨多层传递,无需手动每层传参

5️⃣ 异常捕获与健壮接口设计

  • 捕获取消异常
public async Task<string> Tests(CancellationToken token) //只有主动显示取消令牌才能捕获到取消异常抛错
{
    try
    {
        await Task.Delay(2000, token);
    }
    catch (TaskCanceledException exception)  //Message:A task was canceled.
    {
         // 仅处理 Task 取消(HttpClient 超时 / token 取消)
        return exception.Message;
    }
    catch (OperationCanceledException exception)   //Message:A task was canceled.
    {  
         // 通用取消处理(方法内部手动 token 取消)
        return exception.Message;  
    }
    catch (Exception e)    //Message:A task was canceled.
    {
        return e.Message;
    }
}
  • 原则

    1. 捕获取消异常,记录日志或做补偿
    2. 捕获其他异常,保证接口健壮
    3. 入口方法和下层方法都写 token,可确保取消信号贯穿链路

HttpClient 取消机制理清

1️⃣ 场景 A:调用方主动取消请求(客户端取消)
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var response = await httpClient.GetAsync("https://api.example.com", cts.Token);
  • token 被触发(超时或手动调用 cts.Cancel()):

    1. HttpClient 内部立即取消请求(如果还没发起请求就不会发送;如果已发送,TCP/HTTP 层尽量中止)
    2. GetAsyncTaskCanceledException → 你是调用方,代码可以捕获
  • 特点

    • 你是主动方,可以选择不再等待结果或做补偿
    • 服务端可能接收到部分请求(或连接中断),可通过 HttpContext.RequestAborted 感知

⚡ 核心:这是“调用方主动取消”,你是控制发起请求的那一方


2️⃣ 场景 B:服务端感知客户端取消
  • 客户端浏览器关闭/断开连接HttpClient 超时触发取消

  • 服务端执行请求处理时

    public async Task<IActionResult> MyApi(CancellationToken cancellationToken)
    {
        await Task.Delay(5000, cancellationToken); // 这里 cancellationToken 来自 HttpContext.RequestAborted
    }
    
  • 效果

    1. 框架把客户端取消信号转成 HttpContext.RequestAborted
    2. 异步方法内部使用这个 token → 抛 OperationCanceledException / TaskCanceledException
    3. 方法可以捕获并做日志或补偿

⚡ 核心:这是“服务端感知客户端取消”,服务端是被动方,知道调用方取消了请求


3️⃣ 对比总结
方向主动方被动方 / 感知方异常类型token 来源
调用方取消请求客户端 / HttpClient服务端可能感知TaskCanceledException本地 CancellationTokenSource
服务端感知客户端取消浏览器 / 客户端服务端方法OperationCanceledExceptionHttpContext.RequestAborted
  • 核心TaskCanceledExceptionTask 被取消的特殊化,本质上仍然是 OperationCanceledException

    ⚠️ 注意:

    • 捕获 OperationCanceledException 可以兼容所有情况,包括 TaskCanceledException
    • 捕获 TaskCanceledException 只针对 Task 相关取消
  • 主动方 → 可以在 GetAsync 前就选择不执行请求,或者在请求过程中中止

  • 被动方 → 方法内部使用 token 才能捕获取消信号

  1. 你在调用方

    • 可以用 CancellationToken 主动取消请求
    • GetAsync 会抛异常,你捕获后可以决定不再执行
  2. 你在服务端

    • 如果客户端取消了请求(浏览器关闭、HttpClient 超时)
    • 框架 token (HttpContext.RequestAborted) 会被触发
    • 你在方法内部显示使用 token 才能捕获并响应取消

6️⃣ 长循环 / CPU 密集任务支持取消

for (int i = 0; i < big; i++)
{
    if (i % 1000 == 0)
        cancellationToken.ThrowIfCancellationRequested();
    // 执行计算
}

7️⃣ 链接多个 token

var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(ctFromFramework, ctLocal);
await DoWorkAsync(linkedToken.Token);
  • 可同时响应多个来源的取消信号

8️⃣ 总结要点

  1. 协作式取消:必须在方法内部主动检查 token 或使用支持 token 的 API

  2. ABP 框架自动传递 token:仓储 / UnitOfWork / EF Core 会使用 RequestAborted token

  3. 入口方法写 token:可显式传递或覆盖框架 token,实现可控取消

  4. 异常处理

    • TaskCanceledException / OperationCanceledException → 捕获并记录日志
    • 其他异常 → 正常捕获处理
  5. 接口设计策略

    • 尊重客户端取消 → 使用框架 token
    • 保证执行 → 独立 token / 后台任务
  6. HttpClient

    • 传 token → 本地可取消,服务端可感知连接断开
    • 不传 token → 请求一直执行直到完成

✅ 通过这种设计,你的接口可以:

  • 安全捕获取消异常
  • 明确知道何时是客户端取消
  • 保证业务执行或灵活放弃
  • 支持健壮日志和补偿处理
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.Caching;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.Timing;
using Volo.Abp.VirtualFileSystem;
using App.Abp.RateLimit.Localization;
​
namespace App.Abp.RateLimit
{
    
//ABP 的 AbpTimingModule 提供了一些工具,帮助处理时间相关逻辑,比如:
//var clock = context.ServiceProvider.GetRequiredService<IClock>();
//var now = clock.Now; // 获取当前时间(受时区影响)
    [DependsOn(typeof(AbpTimingModule))]
​
//提供 本地化(i18n)支持,用于国际化和多语言翻译。
//允许在不同语言环境下提供错误消息、界面文本等。
//🔍 为什么 AbpRateLimitModule 需要它?
//限流模块可能会返回错误消息给前端:
//RateLimitException 可能会抛出多语言的错误提示,比如:
//英文:"Too many requests, please try again later."
//中文:"请求过多,请稍后再试。"
//代码中配置了本地化:
//Configure<AbpLocalizationOptions>(options =>
//{
//    options.Resources
//        .Add<RateLimitResource>("en")  // 注册资源
//        .AddVirtualJson("/App/Abp/RateLimit/Localization/Resources"); // 指定 JSON 资源路径
//});
//这样,ABP 框架会从 /App/Abp/RateLimit/Localization/Resources/en.json 读取本地化文本。
    
    [DependsOn(typeof(AbpLocalizationModule))]
    
    
// 提供 缓存支持(MemoryCache、Redis、Distributed Cache)。
//允许模块使用缓存来存储计算结果,提高性能。
    [DependsOn(typeof(AbpCachingModule))]
    public class AbpRateLimitModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            //context.Services 是 依赖注入(DI) 容器,OnRegistred 方法允许在 服务注册时 触发回调函数
            //动态注册拦截器,比如给指定的服务方法添加限流拦截器(RateLimitInterceptor)。
            //只有在 需要时 才进行注册,避免不必要的性能开销。
            context.Services.OnRegistred(RateLimitInterceptorRegistrar.RegisterIfNeeded);
​
            //作用:将 "RateLimitException" 这种自定义异常映射到 HTTP 状态码 429 Too Many Requests。
            //原因:当 API 请求超过限流策略时,可能会抛出 RateLimitException,这里定义了它的 HTTP 响应状态码,符合 RESTful 设计。
            Configure<AbpExceptionHttpStatusCodeOptions>(options =>
            {
                options.Map("RateLimitException", HttpStatusCode.TooManyRequests);
            });
​
            
            //作用:配置 默认的限流策略。在模块初始化时,给策略字段添加上默认的数据
            Configure<AbpRateLimitOptions>(options => { options.MapDefaultEffectPolicys(); });
​
            
            
//什么是 AbpVirtualFileSystemOptions?
//ABP 框架提供的虚拟文件系统(VFS)配置选项。
//允许 应用程序 访问嵌入式资源(如 JSON、XML、HTML、JS、CSS 文件)就像访问普通文件 一样。
//ABP 使用 VFS 主要是为了:统一管理资源文件(如本地化语言包、多租户配置等)。
//支持模块化开发(不同模块可以自带资源文件,而不影响主应用)。
//让资源文件打包进 DLL,而不是放在磁盘上(提高可移植性)。
            
            
//3. 为什么要用虚拟文件系统?
    //(1) 资源打包到 DLL
    //如果你不想把 .json、.html、.xml 这些文件单独存储在磁盘上,而是想直接打包进 DLL 文件,那么可以使用 嵌入式资源(Embedded Resource)。//(2) 在 ABP 模块化系统中共享资源
 //假设你开发了一个 RateLimit 模块,它有一些 JSON 配置文件,比如:///Localization/RateLimit/en.json  (英文语言包)
 ///Localization/RateLimit/zh-Hans.json  (简体中文语言包)
 //这些 JSON 文件不存储在磁盘上,而是作为嵌入式资源存储在 AbpRateLimitModule.dll 里。//当 options.FileSets.AddEmbedded<AbpRateLimitModule>() 被调用时:
 //ABP 框架知道 AbpRateLimitModule.dll 里有一些嵌入式资源文件。
            
 //这些文件可以像普通文件一样被读取:     
 //var fileProvider = context.ServiceProvider.GetRequiredService<IAbpVirtualFileProvider>();
 //var fileContent = fileProvider.GetFileInfo("/Localization/RateLimit/en.json").ReadAsString();
 //避免外部手动复制文件:不需要手动拷贝 JSON 文件到 wwwroot 目录或 appsettings.json 里,所有东西都在 DLL 里。
            
        Configure<AbpVirtualFileSystemOptions>(options => { options.FileSets.AddEmbedded<AbpRateLimitModule>(); });
​
            
            //国际本地化配置
            Configure<AbpLocalizationOptions>(options =>
            {
                options.Resources
                    //默认语言
                    .Add<RateLimitResource>("en")
                    //本地化文件路径
                    .AddVirtualJson("/App/Abp/RateLimit/Localization/Resources");
            });
        }
    }
}