.NET Core 基于 AOP + Polly 实现数据库死锁自动重试

.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 最佳实践

  1. 重试次数:建议 3-5 次,过多重试会增加系统负担
  2. 重试间隔 :建议使用递增策略(如 TimeSpan.FromMilliseconds(100 * retryAttempt)
  3. 异常范围:只捕获特定异常(死锁),避免吞掉其他严重错误
  4. 日志记录:记录每次重试,方便排查问题
  5. 监控告警:当重试次数达到上限时发送告警

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 的 IAsyncPolicyISyncPolicy 是线程安全的,可以安全地在多个线程间共享。

Q3:同步和异步策略的重试间隔为什么不同?

A:异步操作通常有更好的并发性能,重试间隔可以更小;同步操作重试间隔递增可以给系统更多恢复时间。

Q4:如何处理非幂等操作?

A:对于非幂等操作(如扣减库存),建议:

  1. 使用分布式锁保证唯一性
  2. 在重试前检查操作是否已执行
  3. 使用消息队列实现最终一致性

本文代码已验证通过,可以直接复制使用。如有疑问欢迎留言讨论!


版权声明:本文为原创文章,转载请注明出处。

相关推荐
米高梅狮子6 小时前
Ceph 分布式存储 部署
linux·运维·数据库·分布式·ceph·docker·华为云
yuzhiboyouye6 小时前
所有的 SQL 都要经过 Explain 优化,是什么意思
数据库·sql
洛水水6 小时前
Redis 实现限流功能的几种方法
数据库·redis·缓存
l1t6 小时前
DeepSeek总结的postgresql 数据分析师 vs width_bucket()
数据库·postgresql
米高梅狮子6 小时前
Redis
数据库·redis·mysql·缓存·docker·容器·github
dinl_vin6 小时前
FastAPI 系列 ·(四):数据库集成——SQLAlchemy 2.0 异步 ORM 与 Alembic 迁移
java·数据库·fastapi
坚定信念,勇往无前7 小时前
electron-vite 安装better-sqlite3
javascript·数据库·electron
大明者省7 小时前
Ubuntu22.04 宝塔面板与 XFCE 远程桌面端口兼容性分析
运维·服务器·数据库·笔记
liudanzhengxi8 小时前
巧用ULN2003A轻松扩展单片机IO口
数据库·mongodb