Appearance
异常处理与自动重试(连接弹性)
概述
在生产环境中,数据库操作可能因网络波动、死锁、超时等原因失败。实现弹性的数据库访问层,能够自动重试 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);最佳实践
✅ 推荐做法
- 仅重试瞬时错误: 不要重试验证错误、权限错误
- 指数退避: 重试间隔递增,避免雪崩
- 设置上限: 限制最大重试次数和总耗时
- 记录日志: 监控重试频率,识别潜在问题
- 幂等性: 确保重试不会导致副作用
❌ 避免的陷阱
- 不要重试非瞬时错误: 如约束违反、数据格式错误
- 不要无限重试: 必须设置合理的上限
- 不要忘记超时: 重试会增加总耗时
- 不要在事务中盲目重试: 可能导致部分提交
总结
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| EF Core 内置 | 简单,零配置 | 灵活性低 | 大多数场景 |
| Polly | 强大灵活,可组合 | 需要额外依赖 | 复杂弹性策略 |
| 手动实现 | 完全控制 | 代码冗长 | 特殊需求 |
建议: 优先使用 EF Core 内置重试,需要更复杂策略时引入 Polly!