Skip to content

异常处理与自动重试(连接弹性) ​

概述 ​

在生产环境中,数据库操作可能因网络波动、死锁、超时等原因失败。实现弹性的数据库访问层,能够自动重试 transient errors(瞬时错误),是构建高可用企业应用的关键。

常见需要重试的错误 ​

  • 网络超时: Timeout expired
  • 死锁: Transaction was deadlocked
  • 连接失败: A network-related or instance-specific error
  • SQL Azure 限流: The database has reached its size quota

方案 1: 使用 Polly (推荐) ​

Polly 是 .NET 的弹性策略库,可与 EF Core 无缝集成。

安装 ​

bash
dotnet add package Polly
dotnet add package Microsoft.Extensions.Http.Polly

配置重试策略 ​

csharp
using Polly;
using Polly.Extensions.Http;

// Program.cs
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(30),
            errorNumbersToAdd: null); // SQL Server 内置重试
    });
});

// 自定义 Polly 策略
var retryPolicy = Policy
    .Handle<SqlException>(ex => 
        ex.Number == -2 ||  // Timeout
        ex.Number == 1205 || // Deadlock
        ex.Number == 40613   // Azure throttling
    )
    .Or<TimeoutException>()
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: (attempt, context) =>
        {
            // 指数退避: 2s, 4s, 8s
            return TimeSpan.FromSeconds(Math.Pow(2, attempt));
        },
        onRetry: (exception, timeSpan, attempt, context) =>
        {
            Log.Warning($"Retry {attempt} due to: {exception.Message}");
        });

在 Service 中使用 ​

csharp
public class OrderService
{
    private readonly AppDbContext _context;
    private readonly AsyncPolicy _retryPolicy;
    
    public OrderService(AppDbContext context, AsyncPolicy retryPolicy)
    {
        _context = context;
        _retryPolicy = retryPolicy;
    }
    
    public async Task<int> CreateOrderAsync(CreateOrderRequest request)
    {
        return await _retryPolicy.ExecuteAsync(async () =>
        {
            var order = new Order
            {
                CustomerId = request.CustomerId,
                OrderDate = DateTime.UtcNow,
                TotalAmount = request.TotalAmount
            };
            
            _context.Orders.Add(order);
            await _context.SaveChangesAsync();
            
            return order.Id;
        });
    }
    
    public async Task<List<Order>> GetOrdersAsync(string customerId)
    {
        return await _retryPolicy.ExecuteAsync(async () =>
        {
            return await _context.Orders
                .Where(o => o.CustomerId == customerId)
                .ToListAsync();
        });
    }
}

方案 2: EF Core 内置重试 ​

简单配置 ​

csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString, sqlOptions =>
    {
        sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,              // 最多重试 5 次
            maxRetryDelay: TimeSpan.FromSeconds(30), // 最大延迟 30 秒
            errorNumbersToAdd: new[] { 40613, 40197 } // 额外需要重试的错误码
        );
    });
});

查看默认重试的错误码 ​

csharp
// SQL Server 默认重试以下错误:
// 1205: Deadlock
// -2: Timeout
// 4060: Cannot open database
// 40197: Service is busy
// 40501: Quota exceeded
// 40540: Service unavailable
// 40549: Session terminated
// 40550: Too many connections
// 40551: TempDB full
// 40552: Log full
// 40553: Resource governor

方案 3: 手动实现重试 ​

csharp
public class ResilientDbContext : DbContext
{
    private readonly ILogger<ResilientDbContext> _logger;
    
    public ResilientDbContext(
        DbContextOptions<ResilientDbContext> options,
        ILogger<ResilientDbContext> logger)
        : base(options)
    {
        _logger = logger;
    }
    
    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        int retryCount = 0;
        const int maxRetries = 3;
        
        while (true)
        {
            try
            {
                return await base.SaveChangesAsync(cancellationToken);
            }
            catch (DbUpdateException ex) when (IsTransientError(ex) && retryCount < maxRetries)
            {
                retryCount++;
                var delay = TimeSpan.FromSeconds(Math.Pow(2, retryCount));
                
                _logger.LogWarning(ex, 
                    $"Transient error detected. Retrying in {delay.Seconds}s... (Attempt {retryCount}/{maxRetries})");
                
                await Task.Delay(delay, cancellationToken);
            }
        }
    }
    
    private static bool IsTransientError(DbUpdateException ex)
    {
        if (ex.InnerException is SqlException sqlEx)
        {
            return sqlEx.Number switch
            {
                -2 => true,    // Timeout
                1205 => true,  // Deadlock
                40613 => true, // Azure throttling
                _ => false
            };
        }
        
        return false;
    }
}

断路器模式(Circuit Breaker) ​

当错误率过高时,快速失败而不是持续重试。

csharp
var circuitBreakerPolicy = Policy
    .Handle<SqlException>()
    .CircuitBreakerAsync(
        exceptionsAllowedBeforeBreaking: 5, // 连续 5 次失败后断开
        durationOfBreak: TimeSpan.FromMinutes(1), // 断路 1 分钟
        onBreak: (exception, breakDelay) =>
        {
            Log.Error($"Circuit broken for {breakDelay} due to: {exception.Message}");
        },
        onReset: () =>
        {
            Log.Info("Circuit reset - service recovered");
        });

// 组合策略: 重试 + 断路器
var resiliencePolicy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy);

最佳实践 ​

✅ 推荐做法 ​

  1. 仅重试瞬时错误: 不要重试验证错误、权限错误
  2. 指数退避: 重试间隔递增,避免雪崩
  3. 设置上限: 限制最大重试次数和总耗时
  4. 记录日志: 监控重试频率,识别潜在问题
  5. 幂等性: 确保重试不会导致副作用

❌ 避免的陷阱 ​

  1. 不要重试非瞬时错误: 如约束违反、数据格式错误
  2. 不要无限重试: 必须设置合理的上限
  3. 不要忘记超时: 重试会增加总耗时
  4. 不要在事务中盲目重试: 可能导致部分提交

总结 ​

方案优点缺点适用场景
EF Core 内置简单,零配置灵活性低大多数场景
Polly强大灵活,可组合需要额外依赖复杂弹性策略
手动实现完全控制代码冗长特殊需求

建议: 优先使用 EF Core 内置重试,需要更复杂策略时引入 Polly!

基于 MIT 许可发布