目标
在高并发与下游故障时保护服务与数据库,防刷、防雪崩,维持核心路径可用。
为什么需要这些机制
- 保护可用性与 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 Requests | 503 Service Unavailable |
| . NET实现 | FixedWindow/SlidingWindow/TokenBucket | ConcurrencyLimiter |
| 是否排队 | 通常不排队,直接拒绝 | 可以排队等待 |
主流方案
方案 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
配置限流策略
在你的 HttpApiHostModule 或 WebModule 中配置:
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 每分钟 100 次
Window = 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 : Attribute
在ABP中EnableRateLimitingAttribute特性在只在控制器被捕获,方法级是不生效的,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 > 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 >= 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)), // 指数退避:2、4、8秒
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 完整深度学习笔记
目录
一、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), // 窗口 1 秒
QueueLimit = 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
核心区别
| 特性 | HttpPolicyExtensions | Policy |
|---|---|---|
| 适用场景 | 专为 HttpClient 设计 | 通用策略(数据库、消息队列等) |
| 返回类型 | PolicyBuilder<HttpResponseMessage> | PolicyBuilder 或 PolicyBuilder<T> |
| 便捷性 | ✅ 开箱即用(一行代码) | ⚠️ 需要手动配置 |
| 灵活性 | ⚠️ 仅限 HTTP 场景 | ✅ 适用任何场景 |
| 所属包 | Polly.Extensions. Http | Polly(核心包) |
🎯 决策指南
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 文档
- 记录了策略选择理由
- 说明了参数配置依据(阈值、超时等)
- 定义了告警响应流程
- 编写了故障恢复手册
📖 延伸学习资源
-
官方文档
-
微软指南
-
示例代码
-
深入主题
- 混沌工程:Simmy
- 分布式追踪:OpenTelemetry 集成
- 高级模式:Saga 模式、Outbox 模式
取消令牌(Cancellation)
1️⃣ 取消令牌的本质
-
CancellationToken = 协作式“开关”
-
调用方主动取消 → 设置 token 状态为已取消
-
被调用方检查 token → 决定是否继续执行
-
关键点:token 本身不执行取消,只是“标记”取消状态
-
特点:
- 协作式:无法强制中断,只能被检查和响应
- 可传递:可以作为参数传递给异步方法或底层 API
- 可组合:多个 token 可用
CancellationTokenSource.CreateLinkedTokenSource链接
-
-
触发取消时:
- 被调用方可调用
ThrowIfCancellationRequested()→ 抛TaskCanceledException或OperationCanceledException
- 被调用方可调用
⚡ 核心作用:方法内部主动判断 token 是否取消,决定是否继续执行
2️⃣ .NET 请求上下文的取消感知
-
HttpContext.RequestAborted → 框架自动提供 CancellationToken
-
触发条件:
- 客户端关闭浏览器 Tab / 页面刷新
- HttpClient 请求超时或调用方取消 token
- 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;
}
}
-
原则:
- 捕获取消异常,记录日志或做补偿
- 捕获其他异常,保证接口健壮
- 入口方法和下层方法都写 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()):- HttpClient 内部立即取消请求(如果还没发起请求就不会发送;如果已发送,TCP/HTTP 层尽量中止)
GetAsync抛TaskCanceledException→ 你是调用方,代码可以捕获
-
特点:
- 你是主动方,可以选择不再等待结果或做补偿
- 服务端可能接收到部分请求(或连接中断),可通过
HttpContext.RequestAborted感知
⚡ 核心:这是“调用方主动取消”,你是控制发起请求的那一方
2️⃣ 场景 B:服务端感知客户端取消
-
客户端浏览器关闭/断开连接 或 HttpClient 超时触发取消
-
服务端执行请求处理时:
public async Task<IActionResult> MyApi(CancellationToken cancellationToken) { await Task.Delay(5000, cancellationToken); // 这里 cancellationToken 来自 HttpContext.RequestAborted } -
效果:
- 框架把客户端取消信号转成
HttpContext.RequestAborted - 异步方法内部使用这个 token → 抛
OperationCanceledException/TaskCanceledException - 方法可以捕获并做日志或补偿
- 框架把客户端取消信号转成
⚡ 核心:这是“服务端感知客户端取消”,服务端是被动方,知道调用方取消了请求
3️⃣ 对比总结
| 方向 | 主动方 | 被动方 / 感知方 | 异常类型 | token 来源 |
|---|---|---|---|---|
| 调用方取消请求 | 客户端 / HttpClient | 服务端可能感知 | TaskCanceledException | 本地 CancellationTokenSource |
| 服务端感知客户端取消 | 浏览器 / 客户端 | 服务端方法 | OperationCanceledException | HttpContext.RequestAborted |
-
✅ 核心:
TaskCanceledException是 Task 被取消的特殊化,本质上仍然是OperationCanceledException⚠️ 注意:
- 捕获
OperationCanceledException可以兼容所有情况,包括TaskCanceledException - 捕获
TaskCanceledException只针对 Task 相关取消
- 捕获
-
主动方 → 可以在
GetAsync前就选择不执行请求,或者在请求过程中中止 -
被动方 → 方法内部使用 token 才能捕获取消信号
-
你在调用方:
- 可以用
CancellationToken主动取消请求 GetAsync会抛异常,你捕获后可以决定不再执行
- 可以用
-
你在服务端:
- 如果客户端取消了请求(浏览器关闭、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️⃣ 总结要点
-
协作式取消:必须在方法内部主动检查 token 或使用支持 token 的 API
-
ABP 框架自动传递 token:仓储 / UnitOfWork / EF Core 会使用 RequestAborted token
-
入口方法写 token:可显式传递或覆盖框架 token,实现可控取消
-
异常处理:
TaskCanceledException/OperationCanceledException→ 捕获并记录日志- 其他异常 → 正常捕获处理
-
接口设计策略:
- 尊重客户端取消 → 使用框架 token
- 保证执行 → 独立 token / 后台任务
-
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");
});
}
}
}