在中小规模系统中,Dapper以其轻量和高效深受开发者喜爱,但原生IDbConnection并不具备故障恢复能力。网络闪断、SQL Server死锁、连接池耗尽等瞬时错误会让普通查询直接抛异常。Polly是.NET生态中成熟的容错库,能用声明式策略包裹任意委托,实现重试、熔断、超时等逻辑。把两者结合,可以用极少代码让数据层具备弹性。

一、为什么需要重试策略
瞬时故障(transient failure)通常指那些无需人工介入、稍后重试即可成功的错误,例如TCP连接被重置、数据库引擎检测到死锁并回滚、Azure SQL限制连接数等。这类异常如果直接冒泡到接口层,用户体验就是白屏或报错,而实际上后台可能半秒后就恢复了。
Dapper本身只负责SQL映射,不处理连接生命周期策略。若在每次调用处手写for循环重试,既重复又容易遗漏事务清理。Polly把重试逻辑抽象为可复用的Policy对象,业务代码只需包一层Execute,即可获得一致的重试行为。
二、基础重试策略封装
先安装包:Polly和System.Data.SqlClient(或Microsoft.Data.SqlClient)。下面示例展示一个固定重试三次的策略,仅对特定异常重试。
using Polly;
using System;
using System.Data.SqlClient;
using System.Data;
public class DapperRetryHelper
{
// 定义重试策略:对超时、SqlException中特定错误码重试
private static readonly Policy _retryPolicy = Policy
.Handle<SqlException>(ex => ex.Number == -2 || ex.Number == 1205)
.Or<TimeoutException>()
.WaitAndRetry(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(200 * attempt),
onRetry: (exception, timespan, count, context) =>
{
Console.WriteLine($"第{count}次重试,原因:{exception.Message}");
});
public static void ExecuteWithRetry(string connStr, Action<IDbConnection> action)
{
_retryPolicy.Execute(() =>
{
using (var conn = new SqlConnection(connStr))
{
conn.Open();
action(conn);
}
});
}
}
上述代码中,WaitAndRetry会在每次重试前等待渐增时间。Handle子句限定只重试死锁(1205)和超时(-2),避免对主键冲突等永久错误做无用重试。使用using确保连接释放,即便重试也每次新建连接,防止旧连接处于错误状态。
调用时业务层完全无感知:DapperRetryHelper.ExecuteWithRetry(connStr, conn => conn.Execute("update Users set Score=Score+1 where Id=@id", new { id }));。这样一行就带上了弹性能力。
三、指数退避与断路器组合
固定间隔在高并发下可能造成重试风暴。更优做法是指数退避,并加入断路器防止下游持续故障时雪崩。
using Polly;
using Polly.CircuitBreaker;
using System;
using System.Data.SqlClient;
using System.Data;
public class AdvancedRetry
{
private static readonly Policy _policy = Policy
.Handle<SqlException>(ex => ex.Number == 1205 || ex.Number == -2)
.WaitAndRetry(
5,
attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
(ex, ts, n, ctx) => Console.WriteLine($"retry {n} after {ts.TotalSeconds}s"))
.Wrap(Policy
.Handle<SqlException>()
.CircuitBreaker(
exceptionsAllowedBeforeBreaking: 10,
durationOfBreak: TimeSpan.FromSeconds(30)));
public static T Execute<T>(string connStr, Func<IDbConnection, T> func)
{
return _policy.Execute(() =>
{
using (var conn = new SqlConnection(connStr))
{
conn.Open();
return func(conn);
}
});
}
}
Wrap方法将重试包在断路器之外:先重试,若短时间大量失败则跳闸,后续请求直接抛BrokenCircuitException,给数据库喘息时间。指数退避(2、4、8、16、32秒)分散了重试时刻,降低对故障节点的冲击。
需注意断路器状态是进程内单例,若部署多实例,各自独立统计。在容器横向扩容场景下,可结合分布式计数器,但多数内部系统单机策略已足够。
四、事务与重试的注意事项
重试时若已开启事务,必须连同事务一起丢弃重建,否则已回滚的事务不能再次提交。正确模式是在Policy内部开启事务,而非外部。
using Polly;
using System;
using System.Data;
using System.Data.SqlClient;
public class TxRetry
{
private static readonly Policy _p = Policy
.Handle<SqlException>(e => e.Number == 1205)
.Retry(3);
public static void DoTx(string connStr)
{
_p.Execute(() =>
{
using (var conn = new SqlConnection(connStr))
{
conn.Open();
using (var tx = conn.BeginTransaction())
{
try
{
conn.Execute("insert into Log values(getdate())", transaction: tx);
conn.Execute("update Counter set V=V+1", transaction: tx);
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
}
}
});
}
}
死锁往往因锁顺序不同引发。重试前确保事务已Rollback,新连接重新拿锁,才能自然化解。若把事务放到Execute外,第一次死锁回滚后,外层事务对象失效,第二次执行会报异常。
另外,读操作可不加事务直接重试;写操作必须保证幂等,例如用唯一业务号防止重复插入,避免重试导致双写。
五、策略配置建议
不同操作应匹配不同策略。下面给出参考表:
| 操作类型 | 重试次数 | 退避 | 断路器 |
|---|---|---|---|
| 单行查询 | 3 | 固定100ms | 否 |
| 批量更新 | 5 | 指数 | 是 |
| 报表统计 | 2 | 固定500ms | 否 |
将Policy实例注册为单例,避免每次建策略产生开销。在ASP.NET Core中可用DI提供IServiceCollection单例化RetryPolicyFactory。
最后提醒,重试不是银弹。对参数错误、权限不足等确定性异常不应重试,需在Handle中精确过滤,才能构建健壮的Dapper弹性连接层。
DapperPollyretry_policy修改时间:2026-08-09 16:36:19