知道从C#调用SQL Server时何时重试或失败?

Kra*_*ica 30 .net c# sql-server

我有一个C#应用程序,它从一个有点不稳定的环境中托管的SQL Server中获取数据.我无法解决环境问题,所以我需要尽可能优雅地处理它们.

为此,我想重新尝试基础设施故障导致的操作,例如网络故障,SQL服务器脱机,因为它们正在重新启动,查询超时等.同时,我不想要如果逻辑错误失败,则重试查询.我只是希望那些将异常冒泡到客户端.

我的问题是:区分环境问题(丢失连接,超时)和其他类型的异常(即使环境稳定会发生逻辑错误之类的事情)的最佳方法是什么.

C#中有一个常用的模式来处理这样的事情吗?例如,是否有一个属性我可以在SqlConnection对象上检查以检测失败的连接?如果没有,解决这个问题的最佳方法是什么?

对于它的价值,我的代码并不特别:

using (SqlConnection connection = new SqlConnection(myConnectionString))
using (SqlCommand command = connection.CreateCommand())
{
  command.CommandText = mySelectCommand;
  connection.Open();

  using (SqlDataReader reader = command.ExecuteReader())
  {
    while (reader.Read())
    {
      // Do something with the returned data.
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

Adr*_*tti 51

一个SqlException(可能)包装多个SQL Server错误.您可以使用Errors属性迭代它们.每个错误是SqlError:

foreach (SqlError error in exception.Errors)
Run Code Online (Sandbox Code Playgroud)

每个SqlError都有一个Class属性,您可以用来粗略地确定您是否可以重试(如果您还需要重新创建连接,则重试).来自MSDN:

  • Class <10表示您传递的信息中的错误(可能),如果您没有更正输入,则无法重试.
  • Class从11到16是"由用户生成"然后,如果用户首先不纠正他的输入,则可能再次无法做任何事情.请注意,第16类包含许多临时错误,第13 类包含死锁(感谢EvZ),因此如果您逐个处理它们,可以排除这些类.
  • Class从17到24是一般硬件/软件错误,您可以重试.当Class为20或更高时,您还必须重新建立连接.22和23可能是严重的硬件/软件错误,24表示媒体错误(应该警告用户,但如果它只是一个"临时"错误,您可能会重试).

您可以在此处找到每个班级的更详细说明.

通常,如果您使用类处理错误,则无需确切地知道每个错误(使用error.Number属性或exception.Number只是SqlError该列表中第一个的快捷方式).这样做的缺点是,当它无用时(或无法恢复错误),您可以重试.我建议采用两步法:

  • 检查已知的错误代码(列出错误代码SELECT * FROM master.sys.messages)以查看您要处理的内容(知道如何).该视图包含所有受支持语言的消息,因此您可能需要按msglangid列过滤它们(例如,英语为1033).
  • 对于其他一切依赖于错误类,重试时间Class为13或高于16(并重新连接,如果20或更高).
  • 严重性高于21(22,23和24)的错误是严重错误,很少等待也无法解决问题(数据库本身也可能被损坏).

关于高年级的一个词.如何处理这些错误并不简单,这取决于许多因素(包括应用程序的风险管理).作为简单的第一步,我在尝试写操作时不会重试22,23和24:如果数据库,文件系统或介质严重受损,那么写入新数据可能会使数据完整性恶化(SQL Server非常小心)即使在严峻的情况下,也不要为了查询而破坏数据库).损坏的服务器,取决于您的数据库网络架构,甚至可能是热交换(自动,在指定的时间后,或触发指定的触发器).始终咨询并在您的DBA附近工作.

重试策略取决于您正在处理的错误:免费资源,等待待处理操作完成,采取替代操作等.一般情况下,只有在所有错误都是"可重试"时才应重试:

bool rebuildConnection = true; // First try connection must be open

for (int i=0; i < MaximumNumberOfRetries; ++i) {
    try {
        // (Re)Create connection to SQL Server
        if (rebuildConnection) {
            if (connection != null)
                connection.Dispose();

            // Create connection and open it...
        }

        // Perform your task

        // No exceptions, task has been completed
        break;
    }
    catch (SqlException e) {
        if (e.Errors.Cast<SqlError>().All(x => CanRetry(x))) {
            // What to do? Handle that here, also checking Number property.
            // For Class < 20 you may simply Thread.Sleep(DelayOnError);

            rebuildConnection = e.Errors
                .Cast<SqlError>()
                .Any(x => x.Class >= 20);

            continue; 
        }

        throw;
    }
}
Run Code Online (Sandbox Code Playgroud)

裹在一切try/ finally妥善处置连接.有了这个简单的假天真CanRetry()功能:

private static readonly int[] RetriableClasses = { 13, 16, 17, 18, 19, 20, 21, 22, 24 };

private static bool CanRetry(SqlError error) {
    // Use this switch if you want to handle only well-known errors,
    // remove it if you want to always retry. A "blacklist" approach may
    // also work: return false when you're sure you can't recover from one
    // error and rely on Class for anything else.
    switch (error.Number) {
        // Handle well-known error codes, 
    }

    // Handle unknown errors with severity 21 or less. 22 or more
    // indicates a serious error that need to be manually fixed.
    // 24 indicates media errors. They're serious errors (that should
    // be also notified) but we may retry...
    return RetriableClasses.Contains(error.Class); // LINQ...
}
Run Code Online (Sandbox Code Playgroud)

一些相当棘手的方式找到非关键错误的列表在这里.

通常我将所有这些(样板文件)代码嵌入到一个方法中(我可以隐藏所有用于创建/配置/重新创建连接的脏东西):

public static void Try(
    Func<SqlConnection> connectionFactory,
    Action<SqlCommand> performer);
Run Code Online (Sandbox Code Playgroud)

要像这样使用:

Try(
    () => new SqlConnection(connectionString),
    cmd => {
             cmd.CommandText = "SELECT * FROM master.sys.messages";
             using (var reader = cmd.ExecuteReader()) {
                 // Do stuff
         }
    });
Run Code Online (Sandbox Code Playgroud)

请注意,当您不使用SQL Server时,也可以使用骨架(出错时重试)(实际上它可以用于许多其他操作,如I/O和网络相关的东西,所以我建议编写一般功能并广泛地重复使用它.


Stu*_*tLC 5

我不知道任何标准,但这里列出了Sql-Server我通常认为可以重试的例外情况,并且还带有 DTC 调味:

catch (SqlException sqlEx)
{
    canRetry = ((sqlEx.Number == 1205) // 1205 = Deadlock
        || (sqlEx.Number == -2) // -2 = TimeOut
        || (sqlEx.Number == 3989) // 3989 = New request is not allowed to start because it should come with valid transaction descriptor
        || (sqlEx.Number == 3965) // 3965 = The PROMOTE TRANSACTION request failed because there is no local transaction active.
        || (sqlEx.Number == 3919) // 3919 Cannot enlist in the transaction because the transaction has already been committed or rolled back
        || (sqlEx.Number == 3903)); // The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
}
Run Code Online (Sandbox Code Playgroud)

关于重试,建议在重试之间添加随机延迟,以减少例如相同的 2 个事务再次死锁的可能性

对于某些DTC相关错误,可能需要删除连接(或者最坏的情况是 SqlClient.SqlConnection.ClearAllPools()) - 否则无效连接将返回到池中。


小智 5

您可以简单地将SqlConnectionStringBuilder属性重命名为sql连接.

var conBuilder = new SqlConnectionStringBuilder(Configuration["Database:Connection"]); conBuilder.ConnectTimeout = 90; conBuilder.ConnectRetryInterval = 15; conBuilder.ConnectRetryCount = 6;

注意: - 必需.Net 4.5或更高版本.