在C#中为同步/异步任务添加重试/回滚机制的最佳方法是哪种?

时间:2016-05-23 14:33:38

标签: c# asp.net .net design-patterns

想象一下WebForms应用程序,其中有一个名为CreateAll()的主方法。我可以按步骤逐步描述方法任务的过程:

1)存储到数据库(更新/创建Db项目3-4次)

2)启动新主题

3)Result1 =调用soap服务,并使用超时阈值检查状态,并在x分钟后。它继续(状态现在正常,这并不意味着失败)

4)存储到数据库(更新/创建Db项目3-4次)

5)result2 =调用肥皂服务(以火灾和遗忘的方式)

6)更新配置文件(实际上从result1获取)

7)通过使用回调请求,它检查前端的每x秒,结果2的状态,UI显示进度条。如果过程完成(100%),则表示成功

我正在考虑所有这些都是可以按类型分组的任务。基本上有几种类型的操作:

  • Type1:数据库事务
  • 类型2:服务通信/交易
  • Type3:配置文件I / O事务

我想为现有实现添加回滚/重试机制,并使用面向任务的体系结构并重构现有的遗留代码。

我发现C#中的 Memento Design Pattern 命令模式之类的东西可以帮助达到这个目的。我还发现了msdn 重试模式描述有趣。我真的不知道,我希望有人带领我做出最安全和最好的决定......

您能否建议我保留现有实现和流程的最佳方法,但将其包装在一般的抽象重试/回滚/任务列表实现中?

最终实现必须能够在每种情况下重试(无论任何任务或一般故障,例如整个createAll流程中的超时等),并且还会有一个回滚决策列表,其中应用程序必须能够回滚所有任务已经完成了。

我想要一些例子来解决这个耦合代码。

可能有帮助的PseudoCode:

class something
{  
    static result CreateAll(object1 obj1, object2 obj2 ...)
    {
        //Save to database obj1
        //...
        //Update to database obj1 
        //
        //NEW THREAD
       //Start a new thread with obj1, obj2 ...CreateAll
       //...          
     } 

    void CreateAllAsync()
    {
        //Type1 Save to database obj1
        //...
        //Type1 Update to database obj2

        //Type2 Call Web Service to create obj1 on the service (not async)

        while (state != null && now < times)
        {
            if (status == "OK")
            break;      
            else
            //Wait for X seconds
        }

        //Check status continue or general failure
        //Type1 Update to database obj2 and obj1

        //Type2 Call Web Service to create obj2 on the service (fire and forget)

        //Type3 Update Configuration File
        //Type1 Update to database obj2 and obj1
        //..   

    return;
}

//Then the UI takes the responsibility to check the status of result2

5 个答案:

答案 0 :(得分:13)

使用Polly查看重试方案,这似乎与您的Pseudo代码完全一致。本答案的最后是文档中的示例。您可以执行各种重试方案,重试和等待等。例如,您可以多次重试一个完整的事务,或者多次重试一组幂等操作,然后在重试时重新编写补偿逻辑政策最终失败了。

memento模式更适用于在文字处理器(Ctrl-Z和Ctrl-Y)中找到的撤消重做逻辑。

要查看的其他有用模式是简单队列,持久队列甚至服务总线,以便为您提供最终的一致性,而无需让用户等待所有内容成功完成。

// Retry three times, calling an action on each retry 
// with the current exception and retry count
Policy
    .Handle<DivideByZeroException>()
    .Retry(3, (exception, retryCount) =>
    {
        // do something 
    });

基于您的伪代码的示例可能如下所示:

static bool CreateAll(object1 obj1, object2 obj2)
{
     // Policy to retry 3 times, waiting 5 seconds between retries.
     var policy =
         Policy
              .Handle<SqlException>()
              .WaitAndRetry(3, count =>
              {
                 return TimeSpan.FromSeconds(5); 
              });

       policy.Execute(() => UpdateDatabase1(obj1));
       policy.Execute(() => UpdateDatabase2(obj2));
  }

答案 1 :(得分:5)

您可以选择Command模式,其中每个命令都包含所有必要的信息,如连接字符串,服务URL,重试计数等。在此之上,您可以考虑,数据流块来执行管道。

高级别视图enter image description here

更新:意图是分离关注。重试逻辑仅限于一个类,它是现有命令的包装器。 您可以进行更多分析,并提供适当的命令,调用者和接收者对象,并添加回滚功能。

public abstract class BaseCommand
{
    public abstract RxObservables Execute();
}

public class DBCommand : BaseCommand
{
    public override RxObservables Execute()
    {
        return new RxObservables();
    }
}

public class WebServiceCommand : BaseCommand
{
    public override RxObservables Execute()
    {
        return new RxObservables();
    }
}

public class ReTryCommand : BaseCommand // Decorator to existing db/web command
{
    private readonly BaseCommand _baseCommand;
    public RetryCommand(BaseCommand baseCommand)
    {
         _baseCommand = baseCommand
    }
    public override RxObservables Execute()
    {
        try
        {
            //retry using Polly or Custom
            return _baseCommand.Execute();
        }
        catch (Exception)
        {
            throw;
        }
    }
}

public class TaskDispatcher
{
    private readonly BaseCommand _baseCommand;
    public TaskDispatcher(BaseCommand baseCommand)
    {
        _baseCommand = baseCommand;
    }

    public RxObservables ExecuteTask()
    {
        return _baseCommand.Execute();
    }
}

public class Orchestrator
{
    public void Orchestrate()
    {
        var taskDispatcherForDb = new TaskDispatcher(new ReTryCommand(new DBCommand));
        var taskDispatcherForWeb = new TaskDispatcher(new ReTryCommand(new WebCommand));
        var dbResultStream = taskDispatcherForDb.ExecuteTask();
        var WebResultStream = taskDispatcherForDb.ExecuteTask();
    }
}

答案 2 :(得分:3)

对我而言,这听起来像是分布式交易&#39;,因为你有不同的资源(数据库,服务通信,文件i / o),并希望进行可能涉及所有这些事务的交易。

在C#中,您可以使用Microsoft Distributed Transaction Coordinator解决此问题。对于每个资源,您需要一个资源管理器。对于数据库,比如sql server和file i / o,据我所知,它已经可用了。对于其他人,你可以自己开发。

例如,要执行这些事务,您可以使用Text="{x:Bind ViewModel.MySuperString}"类,如下所示:

TransactionScope

(取自here

的例子

要开发自己的资源管理器,您必须实现using (TransactionScope ts = new TransactionScope()) { //all db code here // if an error occurs jump out of the using block and it will dispose and rollback ts.Complete(); } ,这可能是一项相当复杂的任务。这是一个简短的example

答案 3 :(得分:2)

可能有助于您实现目标的一些代码。

public static class Retry
{
   public static void Do(
       Action action,
       TimeSpan retryInterval,
       int retryCount = 3)
   {
       Do<object>(() => 
       {
           action();
           return null;
       }, retryInterval, retryCount);
   }

   public static T Do<T>(
       Func<T> action, 
       TimeSpan retryInterval,
       int retryCount = 3)
   {
       var exceptions = new List<Exception>();

       for (int retry = 0; retry < retryCount; retry++)
       {
          try
          { 
              if (retry > 0)
                  Thread.Sleep(retryInterval);
              return action();
          }
          catch (Exception ex)
          { 
              exceptions.Add(ex);
          }
       }

       throw new AggregateException(exceptions);
   }
}

如下所示致电并重试:

int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);

参考:http://gist.github.com/KennyBu/ac56371b1666a949daf8

答案 4 :(得分:2)

嗯......听起来真的很糟糕。你不能打开一个交易,写一些东西到数据库,然后在公园里遛狗。因为交易有这种令人讨厌的习惯,即为每个人锁定资源。这消除了您的最佳选择:分布式事务。

我会执行所有操作并准备一个反向脚本。如果操作成功,我会清除脚本。否则我会跑它。但这对于潜在的陷阱是开放的,脚本必须准备好处理它们。例如 - 如果在中期有人已经更新了您添加的记录,该怎么办?或根据您的价值计算出汇总数?

仍然:构建一个反向脚本是一个简单的解决方案,那里没有火箭科学。只是

List<Command> reverseScript;

然后,如果你需要回滚:

using (TransactionScope tx= new TransactionScope()) {
    foreach(Command cmd in reverseScript) cmd.Execute();
    tx.Complete();
}