为死锁异常实现重试逻辑

43

我已经实现了一个通用的存储库,想知道是否有一种聪明的方法来在死锁异常的情况下实现重试逻辑?

这种方法应该适用于所有存储库方法。所以有没有办法避免在每个单独的方法中编写“try/catch-使用重试计数再次调用方法”?

欢迎任何建议。

我的存储库代码:

public class GenericRepository : IRepository
{
    private ObjectContext _context;

    public List<TEntity> ExecuteStoreQuery<TEntity>(string commandText, params object[] parameters) where TEntity : class
    {
        List<TEntity> myList = new List<TEntity>();

        var groupData = _context.ExecuteStoreQuery<TEntity>(commandText, parameters);

        return myList;
    }


    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class
    {          
        var entityName = GetEntityName<TEntity>();
        return _context.CreateQuery<TEntity>(entityName);
    }

    public IEnumerable<TEntity> GetAll<TEntity>() where TEntity : class
    {
        return GetQuery<TEntity>().AsEnumerable();
    }

编辑:

1.解决方案:

稍作修改自chris.house.00的解决方案

 public static T DeadlockRetryHelper<T>(Func<T> repositoryMethod, int maxRetries)
    {
        var retryCount = 0;

        while (retryCount < maxRetries)
        {
            try
            {
                return repositoryMethod();
            }
            catch (System.Data.SqlClient.SqlException ex)
            {
                if (ex.Number == 1205)// Deadlock                         
                    retryCount++;
                else
                    throw;                   
            }
        }
        return default(T);
    }

你可以这样调用它:

    public TEntity FirstOrDefault<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return RetryUtility.DeadlockRetryHelper<TEntity>( () =>p_FirstOrDefault<TEntity>(predicate), 3);
    }

    protected TEntity p_FirstOrDefault<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class
    {
        return GetQuery<TEntity>().FirstOrDefault<TEntity>(predicate);
    }

一个try语句有什么问题吗? - Antarr Byrd
2
我认为OP想要避免在每个单独的repo方法中重复使用try...catch块。 - Sean H
确保您的存储库能够处理死锁重试可能很困难。即使您将调用集中到存储库,每个存储库函数仍然需要进行测试。 - Paul Williams
6个回答

42
这样行吗:
public T DeadlockRetryHelper<T>(Func<T> repositoryMethod, int maxRetries)
{
  int retryCount = 0;

  while (retryCount < maxRetries)
  {
    try
    {
      return repositoryMethod();
    }
    catch (SqlException e) // This example is for SQL Server, change the exception type/logic if you're using another DBMS
    {
      if (e.Number == 1205)  // SQL Server error code for deadlock
      {
        retryCount++;
      }
      else
      {
        throw;  // Not a deadlock so throw the exception
      }
      // Add some code to do whatever you want with the exception once you've exceeded the max. retries
    }
  }
}

使用以上代码,您的重试逻辑都在该方法中,您只需要将存储库方法作为委托传递即可。

1
要小心只捕获死锁异常。否则,您可能只是重复错误,或者更糟的是,多次执行具有相同副作用的相同功能。 - Paul Williams
6
如果您用尽了重试机会,该函数会在不抛出异常的情况下返回。 - Ian Boyd
1
你的代码很脆弱,因为repositoryMethod()不仅可以在自己的SQL事务中运行,还可以在闭包中使用事务作为参数。在这种情况下,你应该重试整个调用堆栈。 - Artur Udod
7
在重试之间应暂停一小段时间,以便锁持有者有时间释放其锁。还应暂停一个随机时间间隔,以防止相互死锁在同时重试并再次发生死锁。 - Dan Bechard
20
在 C# 6 中,catch (SqlException ex) when (ex.Number == 1205) 表示捕获特定的 SQL 异常(SqlException),仅当该异常的错误代码(Number)等于 1205 时才执行相应的操作。 - Jerry Joseph
显示剩余3条评论

35

我知道这是一篇旧文章,但想分享一个更新的答案。

EF 6现在有一个内置解决方案,它可以设置执行策略,这只需要实现一次。您可以创建一个继承自DbExectutionStrategy的类,并重写ShouldRetryOn()虚方法。您可以创建一个静态类用于包含常量字段值的异常,这些值是可重试代码,然后循环每个值来确定当前抛出的sql异常是否与可重试代码列表匹配...

 public static class SqlRetryErrorCodes
{
    public const int TimeoutExpired = -2;
    public const int Deadlock = 1205;
    public const int CouldNotOpenConnection = 53;
    public const int TransportFail = 121;
}

public class MyCustomExecutionStrategy : DbExecutionStrategy
{
    public MyCustomExecutionStrategy(int maxRetryCount, TimeSpan maxDelay) : base(maxRetryCount, maxDelay) { }

     private readonly List<int> _errorCodesToRetry = new List<int>
    {
        SqlRetryErrorCodes.Deadlock,
        SqlRetryErrorCodes.TimeoutExpired,
        SqlRetryErrorCodes.CouldNotOpenConnection,
        SqlRetryErrorCodes.TransportFail
    };
    protected override bool ShouldRetryOn(Exception exception)
    {
        var sqlException = exception as SqlException;
        if (sqlException != null)
        {
            foreach (SqlError err in sqlException.Errors)
            {
                // Enumerate through all errors found in the exception.
                if (_errorCodesToRetry.Contains(err.Number))
                {
                    return true;
                }
            }
        }
        return false;
    }
}

最后,一旦您设置了自定义的执行策略,您只需创建另一个继承自DbConfiguration的类,该类具有一个公共构造函数,用于设置执行策略:

 public class MyEfConfigurations : DbConfiguration
    {
        public MyEfConfigurations()
        {
            SetExecutionStrategy("System.Data.SqlClient",() => new MyCustomExecutionStrategy(5,TimeSpan.FromSeconds(10)));
        }
    }

8
添加了功能。只需正确设置策略即可。
我的重试策略:
public class EFRetryPolicy : DbExecutionStrategy
{
    public EFRetryPolicy() : base()
    {
    }
    //Keep this constructor public too in case it is needed to change defaults of exponential back off algorithm.
    public EFRetryPolicy(int maxRetryCount, TimeSpan maxDelay): base(maxRetryCount, maxDelay)
    {
    }
    protected override bool ShouldRetryOn(Exception ex)
    {

        bool retry = false;

        SqlException sqlException = ex as SqlException;
        if (sqlException != null)
        {
            int[] errorsToRetry =
            {
                1205,  //Deadlock
                -2,    //Timeout
            };
            if (sqlException.Errors.Cast<SqlError>().Any(x => errorsToRetry.Contains(x.Number)))
            {
                retry = true;
            }

        }          
        return retry;
    }
}

告诉 EF 应用我的策略:
public class EFPolicy: DbConfiguration
{
    public EFPolicy()
    {
        SetExecutionStrategy(
            "System.Data.SqlClient",
            () => new EFRetryPolicy());
    }
}

资料来源:

此处所述,重试策略将不适用于用户发起的事务(使用 TransactionScope 创建的事务)。如果使用,则会出现错误 The configured execution strategy does not support user initiated transactions


4

虽然这个解决方案可行,但我更希望不必担心需要重试的 ActionFunc 参数数量。如果您使用一个通用的 Action 创建一个单独的重试方法,您可以在 lambda 中处理要调用的方法的所有变化:

public static class RetryHelper
{

    public static void DeadlockRetryHelper(Action method, int maxRetries = 3)
    {
        var retryCount = 0;

        while (retryCount < maxRetries)
        {
            try
            {
                method();
                return;
            }
            catch (System.Data.SqlClient.SqlException ex)
            {
                if (ex.Number == 1205)// Deadlock           
                {
                    retryCount++;
                    if (retryCount >= maxRetries)
                        throw;
                    // Wait between 1 and 5 seconds
                    Thread.Sleep(new Random().Next(1000, 5000));
                }
                else
                    throw;
            }
        }

    }
}

那么就像这样使用:

RetryHelper.DeadlockRetryHelper(() => CopyAndInsertFile(fileModel));

1
你有没有考虑过使用某种策略注入?例如,你可以使用Unity拦截器来捕获所有的存储库调用。然后你只需要在拦截器中编写重试逻辑一次,而不是在每个方法中重复多次。

听起来就像我正在寻找的东西。你有示例或类似用途的链接吗? - user1638662

0
我使用了MiguelSlv在上面帖子中提供的以下解决方案,它按预期为我工作。它简单易用。
EntityFramework 6添加了ExecutionStrategy功能。所需的全部就是正确设置策略。
我的重试策略:
public class EFRetryPolicy : DbExecutionStrategy
{
    public EFRetryPolicy() : base()
    {
    }
    //Keep this constructor public too in case it is needed to change defaults of exponential back off algorithm.
    public EFRetryPolicy(int maxRetryCount, TimeSpan maxDelay): base(maxRetryCount, maxDelay)
    {
    }
    protected override bool ShouldRetryOn(Exception ex)
    {

        bool retry = false;

        SqlException sqlException = ex as SqlException;
        if (sqlException != null)
        {
            int[] errorsToRetry =
            {
                1205,  //Deadlock
                -2,    //Timeout
            };
            if (sqlException.Errors.Cast<SqlError>().Any(x => errorsToRetry.Contains(x.Number)))
            {
                retry = true;
            }
        }          
        return retry;
    }
}

告诉 EF 应用此策略。
public class EFPolicy: DbConfiguration
{
    public EFPolicy()
    {
        SetExecutionStrategy(
            "System.Data.SqlClient",
                () => new EFRetryPolicy());
    }
}

来源:

使用Entity Framework 6实现连接弹性 微软文档 如此处所述,重试策略将无法与用户启动的事务(使用TransactionScope创建的事务)一起使用。如果使用,您将收到错误消息:“配置的执行策略不支持用户启动的事务”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接