基于 Polly v8 编写,适用于 .NET 6+ 项目。结合 Shopify GraphQL 批量调用场景进行实战演示。
目录
- [Polly 是什么](#Polly 是什么 "#1-polly-%E6%98%AF%E4%BB%80%E4%B9%88")
- 安装
- 核心策略
- 3.1 Retry(重试)
- 3.2 [Circuit Breaker(断路器)](#Circuit Breaker(断路器) "#32-circuit-breaker%E6%96%AD%E8%B7%AF%E5%99%A8")
- 3.3 Timeout(超时)
- 3.4 RateLimiter(限流)
- 3.5 ConcurrencyLimiter(并发隔离)
- 3.6 Fallback(回退)
- [策略组合 Pipeline](#策略组合 Pipeline "#4-%E7%AD%96%E7%95%A5%E7%BB%84%E5%90%88-pipeline")
- 执行顺序详解
- 线程安全
- [完整实战:Shopify 批量调用](#完整实战:Shopify 批量调用 "#7-%E5%AE%8C%E6%95%B4%E5%AE%9E%E6%88%98shopify-%E6%89%B9%E9%87%8F%E8%B0%83%E7%94%A8")
- [v7 vs v8 差异速查](#v7 vs v8 差异速查 "#8-v7-vs-v8-%E5%B7%AE%E5%BC%82%E9%80%9F%E6%9F%A5")
1. Polly 是什么
Polly 是 .NET 生态中最主流的弹性与瞬态故障处理库 ,由 .NET Foundation 官方维护,采用 BSD-3-Clause 开源协议,完全免费可商用。
它允许你用声明式的方式组合多种弹性策略(重试、断路器、超时、限流、隔离、回退),构建一个弹性执行管道(ResiliencePipeline) 。所有策略都是线程安全的,天然适合多线程/并发场景。
典型应用场景:
- 调用外部 HTTP API(如 Shopify、AWS、Azure)时网络抖动
- 数据库瞬态连接失败
- 消息队列临时不可用
- 需要控制并发度防止资源耗尽
2. 安装
bash
# 核心库(必须)
dotnet add package Polly.Core
# 限流(需要 System.Threading.RateLimiting)
dotnet add package Polly.RateLimiting
# 如果要配合 HttpClientFactory
dotnet add package Microsoft.Extensions.Http.Resilience
3. 核心策略
3.1 Retry(重试)
当操作失败时,按指定规则自动重试。这是最常用的策略。
关键参数:
| 参数 | 说明 |
|---|---|
MaxRetryAttempts |
最大重试次数(不包括首次尝试) |
Delay |
首次重试前的等待时间 |
BackoffType |
退避类型:Fixed(固定)、Linear(线性)、Exponential(指数) |
UseJitter |
是否加随机抖动,防止多个线程同时重试(惊群效应) |
ShouldHandle |
定义哪些异常/结果需要重试 |
OnRetry |
重试前的回调,可记录日志 |
示例:Shopify 遇到 429 时指数退避重试
csharp
using Polly;
using Polly.Retry;
var retryPipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
{
MaxRetryAttempts = 5,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential, // 1s, 2s, 4s, 8s, 16s
UseJitter = true, // 加随机偏移,错峰重试
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => (int)r.StatusCode == 429) // HTTP 429
.HandleResult(r => IsGraphQLThrottled(r)) // GraphQL THROTTLED
.Handle<HttpRequestException>(), // 网络异常
OnRetry = args =>
{
Console.WriteLine($"第 {args.AttemptNumber} 次重试,等待 {args.RetryDelay.TotalSeconds}s");
return ValueTask.CompletedTask;
}
})
.Build();
// 使用
var response = await retryPipeline.ExecuteAsync(
async token => await httpClient.GetAsync("https://api.example.com", token),
CancellationToken.None
);
重要 :
UseJitter = true在批量调用场景下非常关键。如果 5 个线程同时触发 429,不加抖动的话它们会在完全相同的时刻醒来重试,再次一起撞墙。
3.2 Circuit Breaker(断路器)
当连续失败次数达到阈值时,快速失败一段时间(不再真正调用下游),给下游服务恢复的机会。就像电路跳闸。
关键参数:
| 参数 | 说明 |
|---|---|
FailureRatio |
触发断路器的失败比例(如 0.5 表示 50% 失败率) |
SamplingDuration |
统计失败率的采样窗口 |
MinimumThroughput |
采样窗口内最少需要多少请求才判断 |
BreakDuration |
断路器打开后持续多久 |
ShouldHandle |
定义哪些异常/结果算失败 |
示例:Shopify 连续失败 50% 时熔断 30 秒
csharp
var circuitBreakerPipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
FailureRatio = 0.5, // 50% 失败率触发
SamplingDuration = TimeSpan.FromSeconds(30), // 统计最近 30 秒
MinimumThroughput = 10, // 至少 10 个请求才判断
BreakDuration = TimeSpan.FromSeconds(30), // 熔断 30 秒
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.HandleResult(r => (int)r.StatusCode >= 500)
.Handle<HttpRequestException>(),
OnOpened = args =>
{
Console.WriteLine($"⚠️ 断路器打开!持续 {args.BreakDuration.TotalSeconds}s");
return ValueTask.CompletedTask;
},
OnClosed = args =>
{
Console.WriteLine("✅ 断路器关闭,恢复正常");
return ValueTask.CompletedTask;
}
})
.Build();
三种状态:
- Closed(关闭):正常通过请求
- Open(打开):快速失败,不调用下游
- Half-Open(半开):熔断时间结束后,放少量请求试探,成功则关闭,失败则重新打开
适用场景:Shopify 服务完全宕机或大面积限流时,避免你的程序疯狂重试,浪费线程和 CPU。
3.3 Timeout(超时)
为整个操作设置一个总时间上限,防止某个请求卡死(比如 Shopify 偶尔响应极慢)。
两种模式:
TimeoutStrategyOptions.TimeoutType = TimeoutType.Optimistic:使用CancellationToken,协作式取消TimeoutStrategyOptions.TimeoutType = TimeoutType.Pessimistic:强制超时,即使代码不检查 CancellationToken
示例:单个请求最多等 10 秒
csharp
var timeoutPipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(10),
OnTimeout = args =>
{
Console.WriteLine($"⏱️ 请求超时!已等待 {args.Timeout.TotalSeconds}s");
return ValueTask.CompletedTask;
}
})
.Build();
注意 :Timeout 应该放在 Pipeline 的最外层(最先 Add),这样它控制的是整个 Pipeline 的总时间(包括重试的等待时间)。
3.4 RateLimiter(限流)
基于 .NET 原生的 System.Threading.RateLimiting,控制请求速率。适合客户端主动限速。
常见限流器:
| 类型 | 说明 |
|---|---|
FixedWindowRateLimiter |
固定窗口,每秒/每分 N 个 |
SlidingWindowRateLimiter |
滑动窗口,更平滑 |
TokenBucketRateLimiter |
令牌桶,可突发 |
ConcurrencyLimiter |
限制并发数(见 3.5) |
示例:每秒最多 10 个请求
csharp
using System.Threading.RateLimiting;
var rateLimiterPipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(new SlidingWindowRateLimiter(
new SlidingWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromSeconds(1),
SegmentsPerWindow = 10,
QueueLimit = 5 // 超限时最多排队 5 个,其余抛异常
}))
.Build();
Shopify 场景不适用:因为 Shopify 是动态积分制(不同查询成本不同),固定速率限流器无法精确匹配。建议不用 RateLimiter,靠 Retry 兜底即可。
3.5 ConcurrencyLimiter(并发隔离)
限制同时执行的请求数量,防止线程池耗尽。Polly v8 中替代了旧版的 Bulkhead。
关键参数:
| 参数 | 说明 |
|---|---|
PermitLimit |
最大并发数 |
QueueLimit |
队列中最多排多少个,超出的抛异常 |
示例:最多 5 个并发,队列排 10 个
csharp
var concurrencyPipeline = new ResiliencePipelineBuilder()
.AddConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 5,
QueueLimit = 10
})
.Build();
与 SemaphoreSlim 的区别 :Polly 的 ConcurrencyLimiter 是策略级的,可以和其他策略组合;SemaphoreSlim 是代码级的。两者可以配合使用。
3.6 Fallback(回退)
当所有策略都失败时,返回一个默认值 或执行降级逻辑,避免直接抛异常给上层。
示例:Shopify 查询失败时返回缓存数据
csharp
var fallbackPipeline = new ResiliencePipelineBuilder<string>()
.AddFallback(new FallbackStrategyOptions<string>
{
FallbackAction = args =>
{
// 返回默认值或从缓存读取
return new ValueTask<string>("{"data":null,"fromCache":true}");
},
OnFallback = args =>
{
Console.WriteLine("🔄 触发降级,返回默认值");
return ValueTask.CompletedTask;
}
})
.Build();
适用场景:非关键数据查询(如商品详情展示),失败时显示"暂无数据"比直接报错更友好。
4. 策略组合 Pipeline
Polly 的强大之处在于可以组合多个策略 成一个执行管道。用 ResiliencePipelineBuilder 链式调用即可。
csharp
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddTimeout(TimeSpan.FromSeconds(30)) // 最外层:总时间上限
.AddConcurrencyLimiter(5, 10) // 限制并发
.AddRetry(new RetryStrategyOptions<...> { ... }) // 失败时重试
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<...> { ... }) // 连续失败熔断
.Build();
5. 执行顺序详解
这是 Polly 最容易踩坑的地方。
Pipeline 的策略是外层包裹内层 的,先 Add 的策略在最外层,后 Add 的策略在内层。
csharp
builder
.AddStrategyA() // 最外层:请求先经过 A
.AddStrategyB() // 中间层
.AddStrategyC() // 最内层:实际业务代码在这里执行
执行流程(正向进入,反向退出):
css
请求 → [StrategyA] → [StrategyB] → [StrategyC] → 执行业务代码
返回 ← [StrategyA] ← [StrategyB] ← [StrategyC] ← 结果/异常
常见组合的正确顺序
| 场景 | 推荐顺序(从外到内) | 原因 |
|---|---|---|
| 超时 + 重试 | Timeout → Retry |
Timeout 控制总时间(含重试等待) |
| 限流 + 重试 | RateLimiter → Retry |
重试不额外抢配额 |
| 熔断 + 重试 | CircuitBreaker → Retry |
先熔断判断,再决定重试 |
| 并发 + 重试 | ConcurrencyLimiter → Retry |
先拿到并发许可,再重试 |
| 完整组合 | Timeout → CircuitBreaker → ConcurrencyLimiter → Retry |
总时间 → 熔断 → 并发 → 重试 |
错误顺序的陷阱
csharp
// ❌ 错误:Retry 在外层,RateLimiter 在内层
builder
.AddRetry(...) // 每次重试都重新进入内层
.AddRateLimiter(...) // 每次重试都要重新抢许可 → 正常请求被挤掉
// ✅ 正确:RateLimiter 在外层,Retry 在内层
builder
.AddRateLimiter(...) // 只控制"能否进入管道",拿到许可后不再检查
.AddRetry(...) // 失败后在当前线程等待,不重新抢许可
6. 线程安全
所有 Polly 策略都是线程安全的,可以创建一个 Pipeline 实例,在多个线程/任务中共享使用。
csharp
// 创建一个全局 Pipeline(推荐作为单例)
public static readonly ResiliencePipeline<HttpResponseMessage> SharedPipeline =
new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRetry(...)
.Build();
// 多个线程安全共享
var tasks = Enumerable.Range(0, 100).Select(i =>
SharedPipeline.ExecuteAsync(async token =>
await httpClient.GetAsync($"/api/items/{i}", token),
CancellationToken.None)
);
await Task.WhenAll(tasks);
最佳实践 :Pipeline 创建有一定开销,建议作为单例复用,不要每次请求都重新 Build。
7. 完整实战:Shopify 批量调用
场景描述
- 需要批量调用 Shopify GraphQL Admin API
- 积分池由多个项目/品牌共享,不可硬编码积分值
- 使用 5 个长期工作线程处理队列
- 只有收到 429/THROTTLED 时才退避重试
- 一个线程重试时不能阻塞其他线程
完整代码
csharp
using System.Text;
using System.Threading.Channels;
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
using Polly.Timeout;
/// <summary>
/// Shopify 批量任务处理器
/// 5 个长期工作线程 + Polly 弹性管道
/// </summary>
public class ShopifyBatchProcessor : IDisposable
{
private readonly HttpClient _httpClient;
private readonly Channel<ShopifyJob> _queue;
private readonly SemaphoreSlim _semaphore;
private readonly ResiliencePipeline<string> _pipeline;
private readonly CancellationTokenSource _cts = new();
private readonly List<Task> _workers = new();
public ShopifyBatchProcessor(HttpClient httpClient, int workerCount = 5)
{
_httpClient = httpClient;
_semaphore = new SemaphoreSlim(workerCount);
// 无界队列:随时扔任务,不阻塞生产者
_queue = Channel.CreateUnbounded<ShopifyJob>(
new UnboundedChannelOptions
{
SingleReader = false, // 多个 worker 同时读
SingleWriter = false // 多个生产者同时写
});
// 构建弹性管道(从外到内)
_pipeline = new ResiliencePipelineBuilder<string>()
// 最外层:总时间上限(包含所有重试等待时间)
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(60),
OnTimeout = args =>
{
Log($"⏱️ 请求总超时(含重试):{args.Timeout.TotalSeconds}s");
return ValueTask.CompletedTask;
}
})
// 第二层:断路器,Shopify 大面积故障时快速失败
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<string>
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30),
ShouldHandle = new PredicateBuilder<string>()
.Handle<ShopifyThrottledException>()
.Handle<HttpRequestException>(),
OnOpened = args =>
{
Log($"⚠️ 断路器打开!暂停 {args.BreakDuration.TotalSeconds}s");
return ValueTask.CompletedTask;
},
OnClosed = _ =>
{
Log("✅ 断路器关闭,恢复调用");
return ValueTask.CompletedTask;
}
})
// 最内层:重试策略,遇到 429/THROTTLED 时指数退避
.AddRetry(new RetryStrategyOptions<string>
{
MaxRetryAttempts = 5,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential, // 1s, 2s, 4s, 8s, 16s
UseJitter = true, // 关键:防止 5 个线程同时醒来重试
ShouldHandle = new PredicateBuilder<string>()
.Handle<ShopifyThrottledException>() // GraphQL THROTTLED
.Handle<HttpRequestException>(), // 网络异常
OnRetry = args =>
{
Log($"🔄 第 {args.AttemptNumber} 次重试,等待 {args.RetryDelay.TotalSeconds:F1}s | " +
$"异常:{args.Outcome.Exception?.Message}");
return ValueTask.CompletedTask;
}
})
.Build();
// 启动工作线程
for (int i = 0; i < workerCount; i++)
{
_workers.Add(Task.Run(() => WorkerLoop(i)));
}
}
/// <summary>
/// 提交一个任务到队列,返回可 await 的 Task
/// </summary>
public Task<T> EnqueueAsync<T>(Func<CancellationToken, Task<T>> work, string jobName = "")
{
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
var job = new ShopifyJob
{
Name = jobName,
Execute = async ct =>
{
try
{
var result = await work(ct);
tcs.SetResult(result);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
}
};
_queue.Writer.TryWrite(job);
return tcs.Task;
}
/// <summary>
/// 发送 Shopify GraphQL 查询,自动走弹性管道
/// </summary>
public async Task<string> QueryGraphQLAsync(string query, string variables = "{}",
CancellationToken ct = default)
{
return await _pipeline.ExecuteAsync(async token =>
{
var payload = new
{
query = query,
variables = variables
};
var json = System.Text.Json.JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(
"admin/api/2024-07/graphql.json", content, token);
var body = await response.Content.ReadAsStringAsync(token);
// 检查 HTTP 429
if ((int)response.StatusCode == 429)
{
throw new ShopifyThrottledException("HTTP 429 Too Many Requests");
}
// 检查 GraphQL 200 OK 但业务错误 THROTTLED
if (body.Contains(""code":"THROTTLED"") ||
body.Contains(""code":"MAXIMUM_CALCULATED_COST_EXCEEDED""))
{
throw new ShopifyThrottledException("GraphQL THROTTLED");
}
// 检查其他 GraphQL 错误
if (body.Contains(""errors":["))
{
throw new InvalidOperationException($"GraphQL Error: {body}");
}
return body;
}, ct);
}
/// <summary>
/// 工作线程主循环
/// </summary>
private async Task WorkerLoop(int workerId)
{
await foreach (var job in _queue.Reader.ReadAllAsync(_cts.Token))
{
await _semaphore.WaitAsync(_cts.Token);
try
{
Log($"[Worker {workerId}] 处理: {job.Name}");
await job.Execute(_cts.Token);
}
catch (OperationCanceledException) when (_cts.Token.IsCancellationRequested)
{
break; // 正常退出
}
catch (Exception ex)
{
Log($"[Worker {workerId}] 异常: {ex.Message}");
// 工作线程不死,继续处理下一个
}
finally
{
_semaphore.Release();
}
}
}
public void Dispose()
{
_queue.Writer.Complete();
_cts.Cancel();
_cts.Dispose();
_semaphore.Dispose();
}
private void Log(string message) => Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {message}");
private class ShopifyJob
{
public string Name { get; set; } = "";
public required Func<CancellationToken, Task> Execute { get; set; }
}
}
/// <summary>
/// Shopify 限流异常
/// </summary>
public class ShopifyThrottledException : Exception
{
public ShopifyThrottledException(string message) : base(message) { }
}
使用示例
csharp
// 初始化(建议作为单例注册到 DI)
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://your-shop.myshopify.com/"),
DefaultRequestHeaders =
{
{ "X-Shopify-Access-Token", "your-access-token" }
}
};
using var processor = new ShopifyBatchProcessor(httpClient, workerCount: 5);
// 批量提交 1000 个任务(每批 200 个只是逻辑分组,实际全部进同一个队列)
var allTasks = new List<Task<string>>();
for (int batch = 0; batch < 5; batch++) // 5 批,每批 200 个
{
for (int i = 0; i < 200; i++)
{
int itemId = batch * 200 + i;
var task = processor.EnqueueAsync(
async ct => await processor.QueryGraphQLAsync(
$"{{ product(id: "gid://shopify/Product/{itemId}") {{ title variants(first: 5) {{ edges {{ node {{ title }} }} }} }} }}",
"{}",
ct),
jobName: $"Product-{itemId}"
);
allTasks.Add(task);
}
Console.WriteLine($"第 {batch + 1} 批 200 个已提交到队列");
// 不需要等待上一批完成!下一批立即提交
}
// 等待全部完成(可选)
var results = await Task.WhenAll(allTasks);
Console.WriteLine($"全部完成,成功 {results.Length} 个");
架构图
scss
┌─────────────────────────────────────────────────────────────┐
│ 生产者(主线程) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 任务 1 │ │ 任务 2 │ │ 任务 N │ ──→ Channel 队列 │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5 个 Worker 线程 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SemaphoreSlim(5) → 同时最多 5 个任务在执行 │ │
│ │ │ │
│ │ 每个 Worker 执行 Pipeline(从外到内): │ │
│ │ 1. Timeout(60s) ──→ 总时间上限 │ │
│ │ 2. CircuitBreaker ──→ 连续失败熔断 │ │
│ │ 3. Retry(指数退避+Jitter) ──→ 429 时重试 │ │
│ │ └── 在当前线程等待,不阻塞其他 Worker │ │
│ │ 4. 实际 HTTP 请求 → Shopify GraphQL API │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
8. v7 vs v8 差异速查
| 特性 | Polly v7 | Polly v8 |
|---|---|---|
| 核心 API | Policy.Handle<> |
ResiliencePipelineBuilder |
| 组合方式 | Policy.Wrap() |
builder.AddXxx().AddYyy() |
| 限流 | BulkheadPolicy |
AddConcurrencyLimiter / AddRateLimiter |
| 执行 | policy.ExecuteAsync(...) |
pipeline.ExecuteAsync(...) |
| 异步 | Context 传参 |
直接在 lambda 中闭包 |
| 性能 | 较好 | 更好(减少分配) |
| 推荐 | 维护中 | ✅ 新项目首选 |
v7 写法(旧版,了解即可)
csharp
// v7 写法
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
var circuitBreaker = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
// 组合
var combined = Policy.WrapAsync(circuitBreaker, retryPolicy);
v8 写法(推荐)
csharp
// v8 写法
var pipeline = new ResiliencePipelineBuilder()
.AddRetry(new RetryStrategyOptions { ... })
.AddCircuitBreaker(new CircuitBreakerStrategyOptions { ... })
.Build();
附录:常见错误码速查
| 场景 | 异常/状态码 | Polly 策略 |
|---|---|---|
| 临时网络断开 | HttpRequestException |
Retry |
| HTTP 429 Too Many Requests | StatusCode 429 | Retry + Jitter |
| Shopify GraphQL THROTTLED | THROTTLED in body |
Retry + 自定义异常 |
| 服务端 500/502/503 | StatusCode 5xx | Retry + CircuitBreaker |
| 请求卡死无响应 | 超时 | Timeout |
| 下游服务完全宕机 | 持续失败 | CircuitBreaker |
| 非关键数据查询失败 | 任何异常 | Fallback |