什么是用于捕获SQLException和重试的优秀C#编码样式

时间:2011-01-27 20:46:43

标签: c# sql linq

我有一个调用SQLServer函数的方法来对表执行自由文本搜索。该函数有时会在第一次调用时导致SQLException:“全文查询字符串的单词分解超时”。所以我通常想重试该请求,因为它会在后续请求中成功。构造重试逻辑的好方法是什么。目前我有以下内容:

var retryCount = 0;
var results = new List<UserSummaryDto>();
using (var ctx = new UsersDataContext(ConfigurationManager.ConnectionStrings[CONNECTION_STRING_KEY].ConnectionString))
{
    for (; ; )
    {
        try
        {
            results = ctx.SearchPhoneList(value, maxRows)
                         .Select(user => user.ToDto())
                         .ToList();
            break;
        }
        catch (SqlException)
        {
            retryCount++;
            if (retryCount > MAX_RETRY) throw;
        }
    }
}

return results;

8 个答案:

答案 0 :(得分:17)

我将异常处理更改为仅重试某些错误:

  • 1204,1205 deadlocks
  • -2 timeout
  • -1连接断开

这些是基本的“可重试”错误

catch (SqlException ex)
{
    if !(ex.Number == 1205 || ex.Number == 1204 || ... )
    {
        throw
    }
    retryCount++;
    if (retryCount > MAX_RETRY) throw;
}

编辑,我清理忘了等待,所以你不要锤击SQL框:

  • 在死锁上添加500毫秒等待
  • 超时延迟5秒

编辑2:

我是开发人员DBA,不做太多C#。 我的答案是纠正调用的异常处理......

答案 1 :(得分:12)

感谢所有反馈。我自己回答这个问题,所以我可以从给出的答案中加入元素。如果我错过了什么,请告诉我。我的方法变成:

var results = new List<UserSummaryDto>();
Retry<UsersDataContext>(ctx => results = ctx.SearchPhoneList(value, maxRows)
                                            .Select(user => user.ToDto())
                                            .ToList());
return results;

我重构了原始的重用方法。还有很多层次的筑巢。它还依赖于数据上下文的默认构造函数,这可能限制性太强。 @Martin,我考虑过你的 PreserveStackTrace 方法,但在这种情况下,我认为它确实没有增加足够的价值 - 很高兴知道以备参考,谢谢:

private const int MAX_RETRY = 2;
private const double LONG_WAIT_SECONDS = 5;
private const double SHORT_WAIT_SECONDS = 0.5;
private static readonly TimeSpan longWait = TimeSpan.FromSeconds(LONG_WAIT_SECONDS);
private static readonly TimeSpan shortWait = TimeSpan.FromSeconds(SHORT_WAIT_SECONDS);
private enum RetryableSqlErrors
{
    Timeout = -2,
    NoLock = 1204,
    Deadlock = 1205,
    WordbreakerTimeout = 30053,
}

private void Retry<T>(Action<T> retryAction) where T : DataContext, new()
{
    var retryCount = 0;
    using (var ctx = new T())
    {
        for (;;)
        {
            try
            {
                retryAction(ctx);
                break;
            }
            catch (SqlException ex)
                when (ex.Number == (int) RetryableSqlErrors.Timeout &&
                      retryCount < MAX_RETRY)
            {
                Thread.Sleep(longWait);
            }
            catch (SqlException ex)
                when (Enum.IsDefined(typeof(RetryableSqlErrors), ex.Number) &&
                      retryCount < MAX_RETRY)
            {
                Thread.Sleep(shortWait);
            }
            retryCount++;
        }
    }
}

答案 2 :(得分:8)

我对sql的retryables的枚举如下所示:

SqlConnectionBroken = -1,
SqlTimeout = -2,
SqlOutOfMemory = 701,
SqlOutOfLocks = 1204,
SqlDeadlockVictim = 1205,
SqlLockRequestTimeout = 1222,
SqlTimeoutWaitingForMemoryResource = 8645,
SqlLowMemoryCondition = 8651,
SqlWordbreakerTimeout = 30053

答案 3 :(得分:7)

这不是好的风格,但有时你必须这样做,因为你根本无法改变现有代码并且必须处理它。

我在这种情况下使用以下通用方法。请注意PreserveStackTrace()方法,它有时在重新抛出场景中非常有用。

public static void RetryBeforeThrow<T>(Action action, int retries, int timeout) where T : Exception
{
    if (action == null)
        throw new ArgumentNullException("action", string.Format("Argument '{0}' cannot be null.", "action"));

    int tries = 1;

    do
    {
        try
        {
            action();
            return;
        }
        catch (T ex)
        {
            if (retries <= 0)
            {
                PreserveStackTrace(ex);
                throw;
            }

            Thread.Sleep(timeout);
        }
    }
    while (tries++ < retries);
}

/// <summary>
/// Sets a flag on an <see cref="T:System.Exception"/> so that all the stack trace information is preserved 
/// when the exception is re-thrown.
/// </summary>
/// <remarks>This is useful because "throw" removes information, such as the original stack frame.</remarks>
/// <see href="http://weblogs.asp.net/fmarguerie/archive/2008/01/02/rethrowing-exceptions-and-preserving-the-full-call-stack-trace.aspx"/>
public static void PreserveStackTrace(Exception ex)
{
    MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic);
    preserveStackTrace.Invoke(ex, null);
}

你会这样称呼它:

RetryBeforeThrow<SqlException>(() => MethodWhichFails(), 3, 100);

答案 4 :(得分:3)

做这样的事情没有好的风格。你最好找出为什么请求第一次失败但第二次成功。

Sql Server似乎有可能最初编译执行计划,然后执行查询。因此第一次调用失败,因为组合时间超过了您的超时属性,并且第二次成功,因为执行计划已经编译并保存。

我不知道UsersDataContext是如何工作的,但可能是在实际执行查询之前你可以选择Prepare查询。

真正的答案:如果我必须这样做,我会重试一次而不是再次,就像这样:

var results = new List<UserSummaryDto>();
using (var ctx = new 
    UsersDataContext(ConfigurationManager.ConnectionStrings[CONNECTION_STRING_KEY].ConnectionString))
{
        try
        {
            results = ctx.SearchPhoneList(value, maxRows)
                         .Select(user => user.ToDto())
                         .ToList();
            break;
        }
        catch (SqlException)
        {
            try
            {
                results = ctx.SearchPhoneList(value, maxRows)
                         .Select(user => user.ToDto())
                         .ToList();
                break;
            }
            catch (SqlException)
            {
                // set return value, or indicate failure to user however
            }
        }
    }
}

return results;

虽然我可能会相信没有滥用重试过程,但你会诱使你的继任者增加重试次数作为快速修复。

答案 5 :(得分:1)

我认为使用指定重试计数的方面注释方法会产生更多结构化代码,尽管它需要一些基础结构编码。

答案 6 :(得分:0)

您可以简单地使用SqlConnectionStringBuilder属性进行sql连接重试。

var conBuilder = new SqlConnectionStringBuilder("Server=.;Database=xxxx;Trusted_Connection=True;MultipleActiveResultSets=true"); conBuilder.ConnectTimeout = 90; conBuilder.ConnectRetryInterval = 15; conBuilder.ConnectRetryCount = 6;

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

答案 7 :(得分:-7)

将相关代码拉出到自己的方法中,然后使用递归。

的伪代码:

try
{
    doDatabaseCall();
}
catch (exception e)
{
    //Check exception object to confirm its the error you've been experiencing as opposed to the server being offline.
    doDatabaseCall();
}