.NET Core 基于 AOP + Polly 实现数据库死锁自动重试
一、背景问题
在高并发场景下,数据库死锁是一个常见且令人头痛的问题。当两个或多个事务互相等待对方释放锁资源时,SQL Server 会自动选择一个事务作为死锁牺牲品终止,抛出如下异常:
事务(进程 ID 61)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务。
如果直接将这个异常暴露给用户,会导致用户体验不佳。最佳实践是:捕获死锁异常并自动重试,让系统在后台自动恢复。
二、技术选型
| 技术 | 用途 | 版本 |
|---|---|---|
| Castle DynamicProxy | AOP 拦截,创建代理对象 | 5.1.0 |
| Polly | 重试策略,支持同步/异步 | 7.2.3 |
| Autofac | IoC 容器,注册拦截器 | 6.4.0 |
| Serilog | 日志记录 | 3.0.1 |
| SqlSugar | ORM 框架 | 5.x |
安装命令
bash
Install-Package Castle.Core -Version 5.1.0
Install-Package Polly -Version 7.2.3
Install-Package Autofac -Version 6.4.0
Install-Package Serilog -Version 3.0.1
Install-Package SqlSugarCore -Version 5.1.4.123
三、核心代码解读
3.1 重试拦截器实现
csharp
namespace Hyzx.Cxy.Api.Aop
{
using Castle.DynamicProxy;
using global::Serilog;
using Hyzx.Cxy.Api.Configure;
using Microsoft.Data.SqlClient;
using Polly;
using System.Reflection;
/// <summary>
/// 重试拦截器 - 用于处理数据库死锁等瞬态故障,自动重试失败的操作
/// </summary>
public class RetryInterceptorAop : IInterceptor
{
private readonly IAsyncPolicy _asyncRetryPolicy;
private readonly ISyncPolicy _syncRetryPolicy;
private static readonly ILogger Logger = Log.ForContext("SourceContext", typeof(RetryInterceptorAop));
private static readonly MethodInfo InterceptAsyncGenericHelperMethod =
typeof(RetryInterceptorAop).GetMethod(nameof(InterceptAsyncGenericHelper),
BindingFlags.NonPublic | BindingFlags.Instance);
public RetryInterceptorAop()
{
_asyncRetryPolicy = Policy
.Handle<SqlException>(ex => ex.Number == 1205)
.Or<Exception>(ex =>
ex.Message.Contains("死锁") ||
ex.Message.Contains("deadlock") ||
(ex.InnerException != null && ex.InnerException.Message.Contains("死锁")))
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: retryAttempt => TimeSpan.FromMilliseconds(100 * retryAttempt),
onRetry: (exception, timeSpan, retryCount, context) =>
{
var methodName = context.ContainsKey("method") ? context["method"]?.ToString() : "未知方法";
var controllerName = context.ContainsKey("controller") ? context["controller"]?.ToString() : "未知控制器";
Logger.Error($"[死锁重试] {controllerName}.{methodName} 第 {retryCount} 次重试,异常:{exception.Message}");
});
_syncRetryPolicy = Policy
.Handle<SqlException>(ex => ex.Number == 1205)
.Or<Exception>(ex =>
ex.Message.Contains("死锁") ||
ex.Message.Contains("deadlock") ||
(ex.InnerException != null && ex.InnerException.Message.Contains("死锁")))
.WaitAndRetry(
retryCount: 5,
sleepDurationProvider: retryAttempt => TimeSpan.FromMilliseconds(100 * retryAttempt),
onRetry: (exception, timeSpan, retryCount, context) =>
{
var methodName = context.ContainsKey("method") ? context["method"]?.ToString() : "未知方法";
var controllerName = context.ContainsKey("controller") ? context["controller"]?.ToString() : "未知控制器";
Logger.Error($"[死锁重试] {controllerName}.{methodName} 第 {retryCount} 次重试,异常:{exception.Message}");
});
}
public void Intercept(IInvocation invocation)
{
var method = invocation.Method;
//Logger.Information($"拦截到方法: {method.Name}");
// 构建 Polly Context 并填充方法名和控制器名
var pollyContext = new Polly.Context(method.Name);
pollyContext["method"] = method.Name;
var controllerFullName = invocation.TargetType?.Name;
if (!string.IsNullOrEmpty(controllerFullName) && controllerFullName.EndsWith("Controller"))
controllerFullName = controllerFullName[0..^10];
pollyContext["controller"] = controllerFullName ?? "未知控制器";
var returnType = method.ReturnType;
if (returnType == typeof(Task))
{
invocation.ReturnValue = InterceptAsyncVoid(invocation, pollyContext);
}
else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
var taskTypeArg = returnType.GetGenericArguments()[0];
var genericMethod = InterceptAsyncGenericHelperMethod.MakeGenericMethod(taskTypeArg);
// 将 invocation 和 pollyContext 一起传递给泛型方法
invocation.ReturnValue = genericMethod.Invoke(this, new object[] { invocation, pollyContext });
}
else
{
_syncRetryPolicy.Execute(() => invocation.Proceed());
}
}
private async Task InterceptAsyncVoid(IInvocation invocation, Polly.Context context)
{
await _asyncRetryPolicy.ExecuteAsync(async ctx =>
{
invocation.Proceed();
var task = (Task)invocation.ReturnValue;
await task.ConfigureAwait(false);
}, context).ConfigureAwait(false);
}
private async Task<T> InterceptAsyncGenericHelper<T>(IInvocation invocation, Polly.Context context)
{
return await _asyncRetryPolicy.ExecuteAsync(async ctx =>
{
invocation.Proceed();
var task = (Task<T>)invocation.ReturnValue;
return await task.ConfigureAwait(false);
}, context).ConfigureAwait(false);
}
}
}
关键配置说明:
| 配置项 | 值 | 说明 |
|---|---|---|
retryCount |
5 | 最多重试5次 |
sleepDurationProvider |
异步10ms,同步100ms*retryAttempt | 重试间隔策略 |
SqlException.Number == 1205 |
SQL死锁错误码 | 精准匹配SQL Server死锁 |
ex.Message.Contains("死锁") |
消息匹配 | 兼容包装异常和其他数据库 |
ex.InnerException |
内部异常检测 | 处理异常被包装的情况 |
3.2 拦截方法分发(同步/异步处理)
csharp
public void Intercept(IInvocation invocation)
{
var method = invocation.Method;
Logger.Information($"拦截到方法: {method.Name}");
// 构建 Polly Context 并填充方法名和控制器名
var pollyContext = new Polly.Context(method.Name);
pollyContext["method"] = method.Name;
var controllerFullName = invocation.TargetType?.Name;
if (!string.IsNullOrEmpty(controllerFullName) && controllerFullName.EndsWith("Controller"))
controllerFullName = controllerFullName[0..^10]; // 去掉 "Controller" 后缀
pollyContext["controller"] = controllerFullName ?? "未知控制器";
var returnType = method.ReturnType;
if (returnType == typeof(Task))
{
// 无返回值异步方法
invocation.ReturnValue = InterceptAsyncVoid(invocation, pollyContext);
}
else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
// 有返回值异步方法 - 使用反射调用泛型辅助方法
var taskTypeArg = returnType.GetGenericArguments()[0];
var genericMethod = InterceptAsyncGenericHelperMethod.MakeGenericMethod(taskTypeArg);
invocation.ReturnValue = genericMethod.Invoke(this, new object[] { invocation, pollyContext });
}
else
{
// 同步方法
_syncRetryPolicy.Execute(() => invocation.Proceed());
}
}
private async Task InterceptAsyncVoid(IInvocation invocation, Polly.Context context)
{
await _asyncRetryPolicy.ExecuteAsync(async ctx =>
{
invocation.Proceed();
var task = (Task)invocation.ReturnValue;
await task.ConfigureAwait(false);
}, context).ConfigureAwait(false);
}
private async Task<T> InterceptAsyncGenericHelper<T>(IInvocation invocation, Polly.Context context)
{
return await _asyncRetryPolicy.ExecuteAsync(async ctx =>
{
invocation.Proceed();
var task = (Task<T>)invocation.ReturnValue;
return await task.ConfigureAwait(false);
}, context).ConfigureAwait(false);
}
3.3 Autofac 注册配置
csharp
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
// 注册拦截器为单例
builder.RegisterType<RetryInterceptorAop>()
.SingleInstance();
// 注册所有 Service 类并应用拦截器
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.Where(t => t.Name.EndsWith("Service"))
.AsImplementedInterfaces()
.EnableInterfaceInterceptors() // 启用接口拦截
.InterceptedBy(typeof(RetryInterceptorAop));
}
}
关键要点:
EnableInterfaceInterceptors()- 必须通过接口调用才能触发拦截SingleInstance()- 拦截器为单例,策略对象只创建一次,避免重复开销InterceptedBy()- 指定应用的拦截器类型
3.4 死锁测试服务
csharp
public class DeadlockTestService
{
private readonly ISqlSugarClient _db;
public DeadlockTestService(ISqlSugarClient db)
{
_db = db;
}
public async Task<APIResult> aaaaaaaaa()
{
await Update1Then2(_db.CopyNew());
return APIResult.Success();
}
public async Task<APIResult> bbbbbbbbbbbbbb()
{
await Update2Then1(_db.CopyNew());
return APIResult.Success();
}
/// <summary>
/// 事务1:先更新 id=1,再更新 id=2
/// </summary>
private async Task Update1Then2(ISqlSugarClient db)
{
var result = await db.Ado.UseTranAsync(async () =>
{
await db.Updateable<h_bd_billtype>()
.SetColumns(it => it.documenttype == "标准报价单")
.Where(it => it.id == 1923677825055854592)
.ExecuteCommandAsync();
await Task.Delay(50000); // 长时间持有锁,增加死锁概率
await db.Updateable<h_bd_billtype>()
.SetColumns(it => it.documenttype == "标准询价单")
.Where(it => it.id == 1923677731350908928)
.ExecuteCommandAsync();
});
if (result.IsSuccess)
Console.WriteLine("事务1 提交成功");
else
throw new Exception(result.ErrorMessage);
}
/// <summary>
/// 事务2:先更新 id=2,再更新 id=1
/// </summary>
private async Task Update2Then1(ISqlSugarClient db)
{
var result = await db.Ado.UseTranAsync(async () =>
{
await db.Updateable<h_bd_billtype>()
.SetColumns(it => it.documenttype == "标准询价单")
.Where(it => it.id == 1923677731350908928)
.ExecuteCommandAsync();
await Task.Delay(500); // 短暂延迟
await db.Updateable<h_bd_billtype>()
.SetColumns(it => it.documenttype == "标准报价单")
.Where(it => it.id == 1923677825055854592)
.ExecuteCommandAsync();
});
if (result.IsSuccess)
Console.WriteLine("事务2 提交成功");
else
throw new Exception(result.ErrorMessage);
}
}
3.5 控制器调用
csharp
[HttpPost]
[AllowAnonymous]
[Description("死锁测试接口A")]
public async Task<APIResult> aaaaaaaaa()
{
return await _userServices.aaaaaaaaa();
}
[HttpPost]
[AllowAnonymous]
[Description("死锁测试接口B")]
public async Task<APIResult> bbbbbbbbbbbbbb()
{
return await _userServices.bbbbbbbbbbbbbb();
}
四、执行结果分析
4.1 日志输出
时间:2026-05-21 14:31:45
所在类:Hyzx.Cxy.Api.Aop.RetryInterceptorAop
等级:Information
信息:拦截到方法: bbbbbbbbbbbbbb
时间:2026-05-21 14:31:45
所在类:Hyzx.Cxy.Api.Aop.RetryInterceptorAop
等级:Information
信息:拦截到方法: aaaaaaaaa
时间:2026-05-21 14:31:47
所在类:Hyzx.Cxy.Api.Extensions.SqlSugarSetup
等级:Error
信息:sql执行错误:
UPDATE [h_bd_billtype] SET [documenttype] = '标准报价单' WHERE ([id] = 1923677825055854592)
时间:2026-05-21 14:31:47
所在类:Hyzx.Cxy.Api.Aop.RetryInterceptorAop
等级:Error
信息:[死锁重试] UsersService.bbbbbbbbbbbbbb 第 1 次重试,异常:事务(进程 ID 61)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务。
时间:2026-05-21 14:31:50
所在类:Hyzx.Cxy.Api.Aop.RetryInterceptorAop
等级:Information
信息:拦截到方法: bbbbbbbbbbbbbb
时间:2026-05-21 14:32:37
所在类:Hyzx.Cxy.Api.Aop.RetryInterceptorAop
等级:Information
信息:拦截到方法: IsAfterClosingfunction
事务1 提交成功
事务2 提交成功
4.2 执行流程
请求A(aaaaaaaaa) ──→ 锁定记录1 ──→ 等待记录2
│
请求B(bbbbbbbbbbbbb) ──→ 锁定记录2 ──→ 等待记录1 ──→ 死锁检测 ──→ B被选为牺牲品
│
触发重试 ──→ 重新执行成功
五、关键技术要点
5.1 为什么需要 CopyNew() 创建新连接?
如果多个并发任务共享同一个数据库连接,事务锁会相互影响,死锁检测可能不准确。每个任务使用独立连接可以确保事务隔离性。
csharp
// ✅ 正确:每个任务使用独立连接
await Update2Then1(db.CopyNew());
// ❌ 错误:共享连接可能导致死锁检测异常
await Update2Then1(db);
5.2 为什么同一个类内部调用不会触发重试?
原因:Castle DynamicProxy 是基于接口创建代理对象,只有通过接口调用时才会经过代理。
csharp
public class UserService : IUserService
{
public async Task A()
{
await B(); // ❌ 直接调用,不会触发拦截!
}
public async Task B() { ... }
}
// ✅ 正确:通过接口调用
public class UserService : IUserService
{
private readonly IUserService _self;
public UserService(IUserService self)
{
_self = self; // 注入自身代理
}
public async Task A()
{
await _self.B(); // ✅ 通过接口调用,触发拦截
}
public async Task B() { ... }
}
5.3 Polly 重试策略对比
| 策略类型 | 适用场景 | 代码示例 |
|---|---|---|
WaitAndRetry |
同步方法 | Policy.WaitAndRetry(5, i => TimeSpan.FromMilliseconds(100 * i)) |
WaitAndRetryAsync |
异步方法 | Policy.WaitAndRetryAsync(5, i => TimeSpan.FromMilliseconds(10)) |
RetryForever |
必须成功的操作 | 不推荐用于数据库操作 |
CircuitBreaker |
熔断保护 | 结合重试使用 |
六、总结与注意事项
6.1 适用场景
- ✅ 数据库操作(INSERT/UPDATE/DELETE)
- ✅ 幂等性操作(重试不会产生副作用)
- ⚠️ 非幂等操作需要谨慎(如扣减库存)
- ❌ 查询操作(一般不需要重试)
6.2 最佳实践
- 重试次数:建议 3-5 次,过多重试会增加系统负担
- 重试间隔 :建议使用递增策略(如
TimeSpan.FromMilliseconds(100 * retryAttempt)) - 异常范围:只捕获特定异常(死锁),避免吞掉其他严重错误
- 日志记录:记录每次重试,方便排查问题
- 监控告警:当重试次数达到上限时发送告警
6.3 完整架构图
┌─────────────────────────────────────────────────────────────┐
│ API Controller │
│ [HttpPost] public async Task<APIResult> aaaaaaaaa() │
└──────────────────────────┬────────────────────────────────┘
│ 通过接口调用
▼
┌─────────────────────────────────────────────────────────────┐
│ Autofac 动态代理 (Castle DynamicProxy) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ RetryInterceptorAop 拦截器 │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Polly 重试策略: │ │ │
│ │ │ - 异常类型: SqlException(1205) │ │ │
│ │ │ - 重试次数: 5次 │ │ │
│ │ │ - 间隔: 10ms/100ms*retryAttempt │ │ │
│ │ │ - onRetry: 记录日志 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────┬────────────────────────────────┘
│ invocation.Proceed()
▼
┌─────────────────────────────────────────────────────────────┐
│ Service 业务层 │
│ await db.Ado.UseTranAsync(async () => { ... }) │
│ 使用 db.CopyNew() 创建独立连接 │
└──────────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Database │
│ 死锁 → 重试 → 成功 │
└─────────────────────────────────────────────────────────────┘
七、常见问题解答
Q1:如何验证重试是否生效?
A :查看日志中是否出现 [死锁重试] 关键字。
Q2:拦截器注册为单例会有线程安全问题吗?
A :不会。Polly 的 IAsyncPolicy 和 ISyncPolicy 是线程安全的,可以安全地在多个线程间共享。
Q3:同步和异步策略的重试间隔为什么不同?
A:异步操作通常有更好的并发性能,重试间隔可以更小;同步操作重试间隔递增可以给系统更多恢复时间。
Q4:如何处理非幂等操作?
A:对于非幂等操作(如扣减库存),建议:
- 使用分布式锁保证唯一性
- 在重试前检查操作是否已执行
- 使用消息队列实现最终一致性
本文代码已验证通过,可以直接复制使用。如有疑问欢迎留言讨论!
版权声明:本文为原创文章,转载请注明出处。