Dapper怎么结合Polly实现数据库重试策略

来源:中国站长站作者:守望者头衔:草根站长
导读:本期聚焦于小伙伴创作的《Dapper怎么结合Polly实现数据库重试策略》,敬请观看详情。数据库瞬时故障常导致Dapper查询意外失败。Polly提供的重试与断路器策略能透明地包裹数据库调用,在连接超时或死锁时自动恢复。本文给出SqlConnection配合Policy.Execute的封装方式,比较固定间隔与指数退避的差异,并说明如何将上下文事务与重试联动,避免脏写。读完可搭建一套轻量弹性数据访问层。

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

Dapper怎么结合Polly实现数据库重试策略

一、为什么需要重试策略

瞬时故障(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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。