故障转移这个话题听起来高大上,其实核心就一件事:主数据库挂了,应用程序要能自动连上备用数据库,而不是直接抛异常让用户看到报错页面。C#生态里实现这件事有好几条路,有的只需要改一行连接字符串,有的则需要自己写一套重试和切换逻辑。下面我们把常见的几种方案逐一拆开讲清楚,包括它们各自的适用场景和容易踩的坑。

一、连接字符串层面的故障转移配置
最简单也最优先考虑的方式,是在连接字符串里直接声明备用服务器。对于SQL Server的数据库镜像场景,可以使用Failover Partner参数。当主服务器无法连接时,SqlClient会自动尝试连接镜像服务器,应用程序代码完全不需要修改。
// 数据库镜像场景下的连接字符串
string connStr = "Data Source=主库地址;Failover Partner=镜像库地址;" +
"Initial Catalog=OrderDb;Integrated Security=True;";
如果使用的是SQL Server 2012之后推荐的AlwaysOn可用性组,则应该使用MultiSubnetFailover=True配合可用性组监听器名称。这个参数会让客户端并行尝试解析出来的所有IP地址,而不是串行等待超时,跨子网切换的时间可以从几十秒缩短到几秒。
string connStr = "Server=AG监听器名称;Database=OrderDb;" +
"Integrated Security=True;MultiSubnetFailover=True;" +
"Connect Timeout=15;";
这里有个细节要注意:Connect Timeout一定要设置得比故障检测时间合理,默认15秒在跨机房场景下可能偏短。另外,连接字符串配置只对新建连接生效,如果故障发生时连接池里还缓存着指向旧主库的连接,就会抛出连接失败的异常,这就引出了下一个话题。
二、用Polly实现连接重试与自动切换
连接字符串解决的是“往哪儿连”的问题,而实际生产中还需要处理“连不上怎么办”。微软官方的Enterprise Library和开源的Polly库都提供了重试策略,这里以更主流的Polly为例,演示如何封装一个具备故障转移能力的执行入口。
// NuGet安装:Polly
var retryPolicy = Policy
.Handle<SqlException>(ex => IsTransientError(ex.Number)) // 只重试瞬态错误
.Or<TimeoutException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (exception, delay, attempt, context) =>
{
// 记录日志,超过一定失败次数后切换到备用连接字符串
if (attempt >= 2)
{
context["connectionString"] = _standbyConnStr;
}
});
await retryPolicy.ExecuteAsync(async ctx =>
{
var connStr = (string)(ctx.Contains("connectionString")
? ctx["connectionString"] : _primaryConnStr);
await using var conn = new SqlConnection(connStr);
await conn.OpenAsync();
// 执行数据库操作
}, new Dictionary<string, object>());
这段代码的关键点有两个。第一,IsTransientError要根据自己的数据库判断哪些错误号值得重试,SQL Server常见的瞬态错误号包括49918、49919、49920、4060、40197等,盲目重试非瞬态错误只会浪费资源。第二,重试和故障转移是两个层次的动作:重试应对的是短暂抖动,故障转移应对的是实例级别故障,两者配合才能既不误切也不漏切。
三、Entity Framework Core中的高可用配置
现在大量项目用的是EF Core,好消息是EF Core从2.1开始内置了连接弹性支持,通过EnableRetryOnFailure方法可以启用执行策略,它默认包含了重试逻辑。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
"Server=AG监听器;Database=OrderDb;Integrated Security=True;MultiSubnetFailover=True;",
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: new[] { 4060 });
});
}
需要注意一个坑:一旦启用了执行策略,手动开启的显式事务必须用CreateExecutionStrategy包装,否则EF Core会直接抛出InvalidOperationException。因为重试机制无法自动判断事务中哪些操作已经执行过,需要开发者显式委托整个事务块的执行方式。这一点在写订单、支付这类强事务代码时特别容易中招。
四、连接池感知与健康检查
很多人配置了故障转移之后发现“不生效”,问题往往出在连接池上。ADO.NET的连接池默认不知道主备切换发生了,池中缓存的死连接会导致后续请求持续失败,直到连接被清理。.NET Framework 4.5之后的SqlClient引入了连接池感知能力,配合MultiSubnetFailover=True,故障发生后会主动清除指向旧主库的连接块。而在.NET Core / .NET 5+的Microsoft.Data.SqlClient中,还提供了ClearAllPools方法供手动清理。
// 在检测到故障转移事件后,主动清理连接池
public async Task OnFailoverDetectedAsync()
{
SqlConnection.ClearAllPools();
_logger.LogWarning("检测到主库故障,已清理连接池并切换到备用库");
}
再进一步,可以在应用层做一个简单的健康检查循环,每隔几秒对主库执行一次SELECT 1,连续失败N次后触发切换动作并更新全局的连接字符串提供者。配合内存缓存或者配置中心,还能做到多实例应用同时切换,避免一部分节点连主库、一部分连备库的脑裂状态。
总结
C#实现数据库故障转移,推荐的做法是分层处理:底层靠连接字符串的Failover Partner或MultiSubnetFailover让驱动自己具备切换能力,中间层用Polly或EF Core的执行策略处理瞬态错误重试,上层用健康检查和连接池清理兜底。三层各司其职,才能真正扛住主库宕机这种突发状况。切忌只依赖单一手段,比如只配了重试却没处理连接池,故障转移的切换瞬间照样会抛出一批异常。
C#数据库故障转移连接字符串Failover Partner修改时间:2026-09-06 13:52:37