如何为Dapper配置死锁恢复能力,无需修改现有30+仓储代码
方案解答
首先明确:Dapper本身未提供全局弹性策略的内置配置入口,以下是3种无需修改现有仓储代码、仅需调整启动/公共配置的落地方案:
方案1:IDbConnection装饰器实现(最推荐,改动量最小)
你可以自定义实现一个封装了Polly策略的ResilientDbConnection装饰器类,完整实现IDbConnection接口,所有数据库操作相关的方法内部自动通过你的SqlResiliencyPolicy包裹执行:
public class ResilientDbConnection : IDbConnection { private readonly IDbConnection _innerConnection; private readonly IAsyncPolicy _resiliencyPolicy; public ResilientDbConnection(IDbConnection innerConnection, IAsyncPolicy resiliencyPolicy) { _innerConnection = innerConnection; _resiliencyPolicy = resiliencyPolicy; } // 所有IDbConnection的同步方法实现,内部套策略 public int Execute(string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) { return _resiliencyPolicy.Execute(() => _innerConnection.Execute(sql, param, transaction, commandTimeout, commandType)); } // 异步方法同理套ExecuteAsync public Task<int> ExecuteAsync(string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null, CancellationToken cancellationToken = default) { return _resiliencyPolicy.ExecuteAsync(() => _innerConnection.ExecuteAsync(sql, param, transaction, commandTimeout, commandType, cancellationToken)); } // 其余IDbConnection接口成员(Open、Close、BeginTransaction等)全部透传给_innerConnection即可,需要弹性的方法单独套策略 public string ConnectionString { get => _innerConnection.ConnectionString; set => _innerConnection.ConnectionString = value; } public int ConnectionTimeout => _innerConnection.ConnectionTimeout; public string Database => _innerConnection.Database; public ConnectionState State => _innerConnection.State; public IDbTransaction BeginTransaction() => _innerConnection.BeginTransaction(); public IDbTransaction BeginTransaction(IsolationLevel il) => _innerConnection.BeginTransaction(il); public void ChangeDatabase(string databaseName) => _innerConnection.ChangeDatabase(databaseName); public void Close() => _innerConnection.Close(); public IDbCommand CreateCommand() => _innerConnection.CreateCommand(); public void Dispose() => _innerConnection.Dispose(); public void Open() => _innerConnection.Open(); #if NET public Task OpenAsync(CancellationToken cancellationToken = default) => _innerConnection.OpenAsync(cancellationToken); #endif }
然后仅需要在启动类的DI注册逻辑中,替换原有IDbConnection的注册逻辑即可,完全不需要修改任何仓储层代码:
// 原来的注册逻辑 // services.AddScoped<IDbConnection>(sp => new SqlConnection(Configuration.GetConnectionString("Default"))); // 替换为 services.AddScoped<IDbConnection>(sp => { var innerConn = new SqlConnection(Configuration.GetConnectionString("Default")); var policy = sp.GetRequiredService<SqlResiliencyPolicy>(); // 你自己实现的策略实例 return new ResilientDbConnection(innerConn, policy); });
方案2:AOP动态代理实现
如果你不想自己实现完整的IDbConnection接口,可以使用Castle DynamicProxy等动态代理库,拦截所有IDbConnection的方法调用,自动套上弹性策略:
- 定义拦截器,在拦截到
Execute/Query等Dapper调用方法时,通过Polly策略执行原逻辑 - 在DI注册时,将
IDbConnection的实现替换为代理对象即可,同样无需修改仓储代码。
方案3:自定义Dapper扩展方法覆盖
如果你的项目所有数据库调用都使用Dapper官方扩展方法,可以自定义一套和Dapper扩展方法同名、同签名的扩展方法,内部通过你的Polly策略封装后再调用原生Dapper方法,只要将你的扩展方法所在的命名空间设置为全局引用,原有仓储代码不需要任何修改即可自动生效。
现有实现优化建议
如果你已经有初步实现,可以从以下几个维度优化:
- 死锁识别精准度:仅捕获SqlException中错误号为1205(SQL Server死锁错误码)的异常触发重试,避免无关异常被重试引发业务问题
- 策略隔离:如果不同仓储的重试策略有差异,可支持按仓储/业务场景配置不同的策略实例,不要全局共用一个固定策略
- 上下文日志:重试逻辑中添加SQL语句、重试次数、执行耗时等上下文日志,方便后续排查死锁问题
- 事务适配:如果你的业务中有事务逻辑,需要确保重试时会回滚原有失败事务、重新开启新事务执行,避免在已失效的事务上下文下重试
- 告警埋点:添加重试次数阈值告警,当某类SQL死锁触发重试的频率过高时主动通知研发人员排查SQL优化空间。
内容的提问来源于stack exchange,提问作者VinZCodz
相关产品推荐
相关产品推荐

