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(任务 31 次,不是 3 次)              │
└─────────────────────────────────────────────────────────┘

🔄 PolicyWrap 执行顺序

┌─────────────────────────────────────────────────────────┐
│ PolicyWrap: Timeout → CircuitBreaker → Retry            │
└─────────────────────────────────────────────────────────┘

执行流程:

用户调用 ExecuteTaskAsync(任务 3)
    ↓
┌─────────────────────────────────────────────────────────┐
│ 1️⃣ Timeout 外层                                          │
│    ┌─────────────────────────────────────────────────┐  │
│    │ 2️⃣ CircuitBreaker 中间熔断层                         │  │
│    │    ┌─────────────────────────────────────────┐  │  │
│    │    │ 3️⃣ Retry 内层                            │  │  │
│    │    │                                         │  │  │
│    │    │ 执行任务 3                                 │  │  │
│    │    │ → 失败 → 重试 1/3 → 失败                   │  │  │
│    │    │ → 重试 2/3 → 失败 → 重试 3/3 → 失败        │  │  │
│    │    │                                         │  │  │
│    │    │ 最终结果:失败 ❌                          │  │  │
│    │    └─────────────────────────────────────────┘  │  │
│    │                                                 │  │
│    │ CircuitBreaker 看到:1 次失败                      │  │
│    │ 失败计数:01                                  │  │
│    └─────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

📋 触发熔断的条件

┌─────────────────────────────────────────────────────────┐
│ 需要 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 个任务最终失败才会触发

 

posted @ 2026-08-11 14:30  天才卧龙  阅读(9)  评论(0)    收藏  举报