[C#]如何在LINQ to SQL中引入重试逻辑来处理超时?

时间:2010-04-07 11:04:37

标签: c# linq linq-to-sql

我需要找到在超时的情况下为我的数据库调用添加重试机制的方法,LINQ to SQL用于在我的代码中调用一些sprocs ...

using (MyDataContext dc = new MyDataContext())
{
    int result = -1; //denote failure
    int count = 0;

    while ((result < 0) && (count < MAX_RETRIES))
    {
        result = dc.myStoredProc1(...);
        count++;
    }

    result = -1;
    count  = 0;
    while ((result < 0) && (count < MAX_RETRIES))
    {
        result = dc.myStoredProc2(...);
        count++;
    }

    ...

    ...
}

不确定上述代码是否正确或是否带来任何复杂情况。

在MAX_RETRIES到达之后抛出异常会很好,但我不知道如何以及在哪里适当地抛出它们: - )

任何帮助表示赞赏。

4 个答案:

答案 0 :(得分:3)

如果您从数据库中获得超时,那么可能它将在几毫秒之后及时响应。

按照你的建议重新进行紧凑循环可能使情况更糟,因为你会给数据库服务器带来不应有的负担,以及在调用代码中绑定一个线程。 在每次重试之间引入等待时间会更安全。

对于更高级的方案,您可以考虑使用渐进式等待模式,在此模式中,如果仍然有超时,则可以在开始时更频繁地重试,然后使用更长和更长的时间间隔。

您可能还想查看本书Release It!中的断路器设计模式,以及该书中描述的许多其他模式和反模式。

State pattern非常适合实施断路器。

答案 1 :(得分:1)

就个人而言,我会在这里使用递归。它使代码更清晰,因为您拥有的唯一“额外代码”是函数的参数。例如:

private MyResult Foo(MyParameters mp, int repeatCall)
{
    var result = null;

    try
    {
        result = mp.dc.myStoredProc(...);
    }
    catch (MyException err)
    {
        if (repeatCall > 0)
        {
            result = Foo(mp, repeatCall - 1);
        }
        else
        {
            throw;
        }
    }

    return result;
}

我认为这是递归的理想示例。无论调用什么,都不需要关注循环,它可以使代码更清晰。

答案 2 :(得分:0)

正如Mark Seemann所说的那样,使用重试政策来处理超时并不是一个好主意。然而,考虑到延迟,这毕竟是一个好主意。要实现它,您可以使用执行操作方法的custom action invoker,并在发生SQL异常时进行重试。这样,您就不必在每个操作方法的代码中处理重试策略。

我们的系统中没有数据库超时,但我使用相同的技术以通用方式处理sql读取死锁。

答案 3 :(得分:0)

我们使用类似的方法(最好为每次重试实例化一个新的EF上下文):

对不起,但是无法包含SqlExceptionUtil.IsSqlServerErrorType()的代码(过于自定义和多层)。

static public T ExecuteRetryable<T>(
    Func<T> function,
    out int actualAttempts,
    string actionDescriptionForException = "SQL",
    int maxTries = 3,
    int pauseMaxMillis = 1000,
    int pauseMinMillis = 0,
    bool alsoPauseBeforeFirstAttempt = false,
    bool allowRetryOnTimeout = false)
{
    Exception mostRecentException = null;

    for (int i = 0; i < maxTries; i++)
    {
        // Pause at the beginning of the loop rather than end to catch the case when many servers
        // start due to inrush of requests (likely).  Use a random factor to try and avoid deadlock 
        // in the first place.
        //
        if (i > 0 || alsoPauseBeforeFirstAttempt)
            Thread.Sleep(new Random
            (
                // Default Initializer was just based on time, help the random to differ when called at same instant in different threads.
                (Int32)((DateTime.Now.Ticks + Thread.CurrentThread.GetHashCode() + Thread.CurrentThread.ManagedThreadId) % Int32.MaxValue)
            )
            .Next(pauseMinMillis, pauseMaxMillis));

        actualAttempts = i + 1;

        try
        {
            T returnValue = function();
            return returnValue;
        }
        catch (Exception ex)
        {
            // The exception hierarchy may not be consistent so search all inner exceptions.
            // Currently it is DbUpdateException -> UpdateException -> SqlException 
            //
            if (!SqlExceptionUtil.IsSqlServerErrorType(ex, SqlServerErrorType.Deadlock) &&                    
                (!allowRetryOnTimeout || !SqlExceptionUtil.IsSqlServerErrorType(ex, SqlServerErrorType.Timeout)))
                throw;

            mostRecentException = ex;
        }
    }

    throw new Exception(
        "Unable to perform action '" + actionDescriptionForException + "' after " + maxTries +
        " tries with pauses of [" + pauseMinMillis + "," + pauseMaxMillis + "]ms due to multiple exceptions.",
        mostRecentException);
}

用法:

List<SomeTableEntity> result = DatabaseHelpers.ExecuteRetryable<List<SomeTableEntity>>(() =>
    {           
        using (EfCtx ctx = new EfCtx())
        {
            return ctx.SomeTable.Where(...).ToList()
        }
    }, out int actualAttempts, allowRetryOnTimeout: true);

如果有人展示如何伪装自定义Linq构造背后的包装代码(如WithRetry(...)),那将是很好的选择。