Polly 重试库完全解析
十年河东,十年河西,莫欺少年穷,学无止境,精益求精 🚀
❓ 问题 1:一直错误怎么办?
防止无限重试的 4 道防线
┌─────────────────────────────────────────────────────────┐ │ 防线 1:Polly 内部重试次数限制(已配置 3 次) │ ├─────────────────────────────────────────────────────────┤ │ 防线 2:失败任务单独记录(不阻塞后续任务) │ ├─────────────────────────────────────────────────────────┤ │ 防线 3:熔断器策略(连续失败 5 次 → 暂停 30 秒) │ ├─────────────────────────────────────────────────────────┤ │ 防线 4:最大重试轮次(每任务最多重试 2 轮) │ └─────────────────────────────────────────────────────────┘
📖 一、Polly 完全入门
1️⃣ Polly 是什么?
┌─────────────────────────────────────────────────────────┐ │ Polly = .NET 弹性策略库 │ │ 官网:https://github.com/App-vNext/Polly │ │ 版本:8.4.0(你项目中使用的版本) │ └─────────────────────────────────────────────────────────┘ 作用: ✅ 重试(Retry)- 临时失败自动重试 ✅ 熔断(Circuit Breaker)- 连续失败暂停调用 ✅ 超时(Timeout)- 防止长时间等待 ✅ 回退(Fallback)- 失败时提供备选方案 ✅ 缓存(Cache)- 减少重复调用 ✅ 限流(Rate Limiter)- 控制调用频率
2️⃣ Polly 核心策略对比
| 策略 | 作用 | 适用场景 | 你的项目 |
|---|---|---|---|
| Retry | 失败后重试 | 网络波动、临时故障 | ✅ 已使用 |
| Circuit Breaker | 连续失败熔断 | 服务不可用 | ⏳ 建议添加 |
| Timeout | 超时控制 | 防止长时间等待 | ✅ 已使用(CancellationToken) |
| Fallback | 失败回退 | 提供备选方案 | ❌ 可选 |
| Bulkhead | 并发限制 | 防止资源耗尽 | ❌ 可选 |
| Rate Limiter | 限流 | 控制调用频率 | ❌ 可选 |
3️⃣ Retry 策略详解(你正在用的)
基础版:固定间隔重试
var retryPolicy = Policy .Handle<Exception>() .WaitAndRetryAsync( retryCount: 3, // 重试 3 次 sleepDurationProvider: retry => // 每次等待时间 TimeSpan.FromSeconds(1), // 固定 1 秒 onRetry: (outcome, timeSpan, retryNumber, context) => { Console.WriteLine($"重试 {retryNumber}/3,等待 {timeSpan.TotalSeconds}s"); } );
进阶版:指数退避(你的项目)
var retryPolicy = Policy .Handle<Exception>() .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: retry => TimeSpan.FromSeconds(Math.Pow(2, retry)), // 2^1=2s, 2^2=4s, 2^3=8s onRetry: (outcome, timeSpan, retryNumber, context) => { Console.WriteLine($"重试 {retryNumber}/3,等待 {timeSpan.TotalSeconds}s,原因:{outcome.Exception.Message}"); } );
为什么用指数退避?
┌─────────────────────────────────────────────────────────┐
│ 重试间隔对比 │
├─────────────────────────────────────────────────────────┤
│ 固定间隔:1s → 1s → 1s (可能加重服务器压力) │
│ 指数退避:2s → 4s → 8s (给系统恢复时间) ✅ │
│ 线性递增:1s → 2s → 3s (折中方案) │
└─────────────────────────────────────────────────────────┘
4️⃣ Circuit Breaker 熔断器(防止无限重试)
什么是熔断器?
┌─────────────────────────────────────────────────────────┐ │ 灵感来源:电路保险丝 │ │ │ │ 正常状态 → 连续失败 5 次 → 熔断状态 → 等待 30 秒 → 半开状态 │ │ ↓ ↓ ↓ ↓ │ │ 允许调用 跳闸保护 拒绝调用 试探调用 │ └─────────────────────────────────────────────────────────┘
三种状态
┌─────────────────────────────────────────────────────────┐ │ 状态转换图 │ └─────────────────────────────────────────────────────────┘ ┌──────────────┐ │ Closed │ ← 正常状态,允许调用 │ (闭合) │ └──────┬───────┘ │ 连续失败 5 次 ↓ ┌──────────────┐ │ Open │ ← 熔断状态,拒绝调用 │ (断开) │ └──────┬───────┘ │ 等待 30 秒 ↓ ┌──────────────┐ │ Half-Open │ ← 半开状态,试探 1 次 │ (半开) │ └──────┬───────┘ │ 成功 → Closed │ 失败 → Open
代码示例
// 熔断器策略:连续失败 5 次 → 熔断 30 秒 var circuitBreaker = Policy .Handle<Exception>() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 5, // 允许失败 5 次 durationOfBreak: TimeSpan.FromSeconds(30), // 熔断 30 秒 onBreak: (outcome, timeSpan) => { Console.WriteLine($"⚠️ 熔断!等待 {timeSpan.TotalSeconds}s"); }, onReset: () => { Console.WriteLine("✅ 熔断复位,恢复正常"); }, onHalfOpen: () => { Console.WriteLine("🔶 半开状态,试探调用"); } ); // 使用熔断器 await circuitBreaker.ExecuteAsync(async () => { await SimulateTaskExecutionAsync(task, token); });
5️⃣ 策略组合(Polly 最强特性)
PolicyWrap:重试 + 熔断 + 超时
// 1️⃣ 重试策略 var retryPolicy = Policy .Handle<Exception>() .WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(Math.Pow(2, retry))); // 2️⃣ 熔断策略 var circuitBreaker = Policy .Handle<Exception>() .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)); // 3️⃣ 超时策略 var timeoutPolicy = Policy .TimeoutAsync(TimeSpan.FromSeconds(30)); // 4️⃣ 组合策略(执行顺序:外层 → 内层) var policyWrap = Policy.WrapAsync(timeoutPolicy, circuitBreaker, retryPolicy); // 使用 await policyWrap.ExecuteAsync(async () => { await SimulateTaskExecutionAsync(task, CancellationToken.None); });
执行流程:
用户调用 ↓ ┌─────────────────┐ │ Timeout │ ← 最外层:30 秒超时 └────────┬────────┘ ↓ ┌─────────────────┐ │ Circuit Breaker │ ← 中间层:连续失败熔断 └────────┬────────┘ ↓ ┌─────────────────┐ │ Retry │ ← 最内层:重试 3 次 └────────┬────────┘ ↓ 实际执行
🔧 二、防止无限重试的完整方案
方案 1:熔断器 + 重试(推荐)
using Polly; using Polly.CircuitBreaker; using Polly.Retry; using Polly.Timeout; public class TaskExecutor { private readonly AsyncRetryPolicy<TaskItem> _retryPolicy; private readonly AsyncCircuitBreakerPolicy _circuitBreaker; private readonly AsyncTimeoutPolicy _timeoutPolicy; private readonly PolicyWrap<TaskItem> _policyWrap; public TaskExecutor(ILogger<TaskExecutor> logger) { // ✅ 重试:最多 3 次,指数退避 _retryPolicy = Policy<TaskItem> .Handle<Exception>() .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: retry => TimeSpan.FromSeconds(Math.Pow(2, retry)), onRetry: (outcome, timeSpan, retryNumber, context) => { logger.LogWarning( "任务 {TaskId} 重试 {Retry}/{Max},等待 {Wait}s,原因:{Error}", context["TaskId"], retryNumber, 3, timeSpan.TotalSeconds, outcome.Exception.Message); } ); // ✅ 熔断:连续失败 5 次 → 熔断 30 秒 _circuitBreaker = Policy .Handle<Exception>() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 5, durationOfBreak: TimeSpan.FromSeconds(30), onBreak: (outcome, timeSpan) => { logger.LogError("⚠️ 熔断器打开!连续失败 5 次,暂停 {Seconds}s", timeSpan.TotalSeconds); }, onReset: () => { logger.LogInformation("✅ 熔断器复位,恢复正常"); } ); // ✅ 超时:30 秒 _timeoutPolicy = Policy.TimeoutAsync<TaskItem>(TimeSpan.FromSeconds(30)); // ✅ 组合策略 _policyWrap = Policy.WrapAsync(_timeoutPolicy, _circuitBreaker, _retryPolicy); } public async Task<TaskItem> ExecuteTaskAsync(TaskItem task, CancellationToken token) { try { return await _policyWrap.ExecuteAsync(async (ctx) => { task.LastAttemptTime = DateTime.Now; await SimulateTaskExecutionAsync(task, token); task.IsCompleted = true; task.ErrorMessage = null; return task; }, new Context { ["TaskId"] = task.Id }); } catch (BrokenCircuitException) { task.ErrorMessage = "熔断器打开,服务暂时不可用"; task.RetryCount++; return task; } catch (TimeoutRejectedException) { task.ErrorMessage = "任务超时(30 秒)"; task.RetryCount++; return task; } catch (Exception ex) { task.ErrorMessage = ex.Message; task.RetryCount++; return task; } } }
方案 2:失败任务重试轮次限制
public class TaskProgress { // ✅ 记录每个任务的重试轮次 public Dictionary<ulong, int> TaskRetryRounds { get; set; } = new(); // ✅ 最大重试轮次 public int MaxRetryRounds { get; set; } = 2; } public async Task ExecuteBatchAsync(List<TaskItem> tasks, TaskProgress progress, CancellationToken token) { var failedTasks = new List<TaskItem>(); foreach (var task in tasks) { if (token.IsCancellationRequested) break; var result = await ExecuteTaskAsync(task, token); if (result.IsCompleted) { progress.CompletedCount++; } else { // ✅ 检查重试轮次 if (!progress.TaskRetryRounds.ContainsKey(task.Id)) progress.TaskRetryRounds[task.Id] = 0; progress.TaskRetryRounds[task.Id]++; // ✅ 未超过最大轮次 → 加入重试队列 if (progress.TaskRetryRounds[task.Id] <= progress.MaxRetryRounds) { failedTasks.Add(task); Console.WriteLine($"🔄 任务 {task.Id} 加入重试队列 (轮次 {progress.TaskRetryRounds[task.Id]}/{progress.MaxRetryRounds})"); } else { progress.FailedTaskIds.Add(task.Id); Console.WriteLine($"❌ 任务 {task.Id} 达到最大重试轮次,标记为永久失败"); } } } // ✅ 重试失败任务 if (failedTasks.Count > 0) { Console.WriteLine($"\n🔄 开始重试 {failedTasks.Count} 个任务..."); await ExecuteBatchAsync(failedTasks, progress, token); } }
📊 三、Polly 8.x API 速查表
| 策略 | 创建方法 | 关键参数 |
|---|---|---|
| Retry | WaitAndRetryAsync(retryCount, sleepDurationProvider, onRetry) |
重试次数、等待时间、回调 |
| Circuit Breaker | CircuitBreakerAsync(exceptionsAllowed, durationOfBreak, onBreak, onReset) |
失败阈值、熔断时间 |
| Timeout | TimeoutAsync<TResult>(TimeSpan) |
超时时间 |
| Fallback | FallbackAsync<TResult>(fallbackAction) |
回退操作 |
| Policy Wrap | WrapAsync(outer, middle, inner) |
策略组合顺序 |
核心就 3 个词:重试、熔断、超时。组合使用
关于熔断(假设最大5次)/重试次数(假设最大3次)/超时(假设最大执行时长30秒)的说明
假设有300个待执行的任务,任务21 【重试3次均失败】-【此时熔断次数加1】,任务22【重试3次均失败】-【此时熔断次数加1】,任务23【重试2次均失败,第3次超时】-【此时熔断次数加1】,任务24【第一次执行超时,遇到超时继续重试3次】-【此时熔断次数加1】,任务25 【重试3次均失败】-【此时熔断次数加1】,此时达到最大熔断次数5次,程序会暂停执行,直到熔断结束
📊 熔断器计数逻辑
┌─────────────────────────────────────────────────────────┐ │ 熔断器:连续失败 5 次 → 熔断 │ ├─────────────────────────────────────────────────────────┘ 任务 1 执行: ┌─────────────────────────────────────────────────────────┐ │ 第 1 次执行 → 失败 │ │ 重试 1/3 → 成功 ✅ │ │ │ │ 熔断器视角:这次调用成功了!不计入失败 │ │ 熔断器失败计数:0 │ └─────────────────────────────────────────────────────────┘ 任务 3 执行: ┌─────────────────────────────────────────────────────────┐ │ 第 1 次执行 → 失败 │ │ 重试 1/3 → 失败 │ │ 重试 2/3 → 失败 │ │ 重试 3/3 → 失败 ❌ │ │ │ │ 熔断器视角:这次调用最终失败了!计入失败计数 │ │ 熔断器失败计数:1(任务 3 算 1 次,不是 3 次) │ └─────────────────────────────────────────────────────────┘
🔄 PolicyWrap 执行顺序
┌─────────────────────────────────────────────────────────┐ │ PolicyWrap: Timeout → CircuitBreaker → Retry │ └─────────────────────────────────────────────────────────┘ 执行流程: 用户调用 ExecuteTaskAsync(任务 3) ↓ ┌─────────────────────────────────────────────────────────┐ │ 1️⃣ Timeout 外层 │ │ ┌─────────────────────────────────────────────────┐ │ │ │ 2️⃣ CircuitBreaker 中间熔断层 │ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ │ │ 3️⃣ Retry 内层 │ │ │ │ │ │ │ │ │ │ │ │ 执行任务 3 │ │ │ │ │ │ → 失败 → 重试 1/3 → 失败 │ │ │ │ │ │ → 重试 2/3 → 失败 → 重试 3/3 → 失败 │ │ │ │ │ │ │ │ │ │ │ │ 最终结果:失败 ❌ │ │ │ │ │ └─────────────────────────────────────────┘ │ │ │ │ │ │ │ │ CircuitBreaker 看到:1 次失败 │ │ │ │ 失败计数:0 → 1 │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘
📋 触发熔断的条件
┌─────────────────────────────────────────────────────────┐ │ 需要 5 个任务都最终失败(重试 3 次后仍失败) │ └─────────────────────────────────────────────────────────┘ 当前状态: 任务 1 → 重试 1 次后成功 → 熔断器计数:0 任务 2 → 直接成功 → 熔断器计数:0 任务 3 → 重试 3 次后失败 → 熔断器计数:1 ⚠️ 任务 4 → 直接成功 → 熔断器计数:1 任务 5 → 重试 2 次后成功 → 熔断器计数:1 任务 6 → 重试 3 次后失败 → 熔断器计数:2 ⚠️ 任务 7 → 重试 3 次后失败 → 熔断器计数:3 ⚠️ 任务 8 → 重试 3 次后失败 → 熔断器计数:4 ⚠️ 任务 9 → 重试 3 次后失败 → 熔断器计数:5 🔥 触发熔断! 输出: ⚠️ 熔断器打开!连续失败 5 次,暂停 30s
🎯 熔断器位置示意图
┌─────────────────────────────────────────────────────────┐ │ PolicyWrap 层级结构 │ └─────────────────────────────────────────────────────────┘ 调用 ExecuteTaskAsync ↓ ┌───────────────────────────────────────┐ │ Timeout (最外层) │ │ ┌─────────────────────────────┐ │ │ │ CircuitBreaker (计数层) ⭐ │ │ │ │ ┌───────────────────┐ │ │ │ │ │ Retry (内部重试) │ │ │ │ │ │ │ │ │ │ │ │ 任务执行 │ │ │ │ │ │ → 失败 → 重试 1 │ │ │ │ │ │ → 失败 → 重试 2 │ │ │ │ │ │ → 失败 → 重试 3 │ │ │ │ │ │ → 最终失败 ❌ │ │ │ │ │ └───────────────────┘ │ │ │ │ │ │ │ │ CircuitBreaker 看到: │ │ │ │ "这次调用失败了" → 计数 +1 │ │ │ └─────────────────────────────┘ │ └───────────────────────────────────────┘ ⭐ 熔断器在 Retry 外层,所以看不到内部重试细节
熔断器正常工作! 只是需要 5 个任务最终失败才会触发

浙公网安备 33010602011771号