执行顺序操作的最佳实践

时间:2020-03-06 15:05:36  来源:igfitidea点击:

一起执行几项任务的最佳方法是什么,如果一项任务失败,则不应完成下一项任务?我知道这是否是数据库操作,那么我应该使用事务,但是我在谈论不同类型的操作,如下所示:

所有任务必须通过:

发送电子邮件
ArchiveReportsInDatabase
创建文件

在上述情况下,所有任务都必须通过,否则整个批处理操作必须回滚。

解决方案

在C#中

返回SendEmail()&& ArchiveResportsInDatabase()&& CreateAFile();

如果语言允许,这是非常整洁的:

  • 将任务放在代码块或者函数指针的数组中。
  • 遍历数组。
  • 如果有任何块返回失败,则中断。

另一个想法:

try {
    task1();
    task2();
    task3();
    ...
    taskN();
}
catch (TaskFailureException e) {
    dealWith(e);
}

我们没有提到我们使用的是哪种编程语言/环境。如果是.NET Framework,则可能需要看一下这篇文章。它描述了Microsoft Robotics Studio中的并发和控制运行时,它允许我们将各种规则应用于一组(异步)事件:例如,我们可以等待它们中的任意数量完成,如果一个事件失败,则可以取消;等等。它也可以在多个线程中运行,因此我们获得了一种非常强大的处理方法。

我们没有指定环境。在Unix shell脚本中,&&运算符就是这样做的。

SendEmail () {
  # ...
}
ArchiveReportsInDatabase () {
  # ...
}
CreateAFile () {
  # ...
}

SendEmail && ArchiveReportsInDatabase && CreateAFile

一些建议:

在分布式方案中,可能需要某种两阶段提交协议。本质上,我们向所有参与者发送一条消息,说"准备做X"。然后,每个参与者都必须发送答复,说"好,我保证我可以做X"或者"不,不能做"。如果所有参与者都保证可以完成,则发送消息告诉他们要完成。 "保证"可以根据需要严格。

另一种方法是为每个操作提供某种撤消机制,然后具有如下逻辑:

try:
    SendEmail()
    try:
        ArchiveReportsInDatabase()
        try:
             CreateAFile()
        except:
            UndoArchiveReportsInDatabase()
            raise
    except:
        UndoSendEmail()
        raise
except:
    // handle failure

(我们不希望代码看起来像这样;这只是说明逻辑应该如何流动。)

如果使用的语言使用排序电路评估(Java和Cdo),则只需执行以下操作:

return SendEmail() && ArchiveResportsInDatabase() && CreateAFile();

如果所有函数都返回true,则返回true,并在第一个函数返回false时立即停止。

例外通常对这种事情有好处。伪Java / JavaScript / C ++代码:

try {
    if (!SendEmail()) {
        throw "Could not send e-mail";
    }

    if (!ArchiveReportsInDatabase()) {
        throw "Could not archive reports in database";
    }

    if (!CreateAFile()) {
        throw "Could not create file";
    }

    ...

} catch (Exception) {
    LogError(Exception);
    ...
}

如果方法本身抛出异常,那就更好了:

try {
    SendEmail();
    ArchiveReportsInDatabase();
    CreateAFile();
    ...

} catch (Exception) {
    LogError(Exception);
    ...
}

这种风格的一个很好的结果是,当我们沿着任务链向下移动时,代码不会越来越缩进。所有方法调用都保持相同的缩进级别。缩进太多会使代码难以阅读。

此外,我们在代码中只有一点可以进行错误处理,日志记录,回滚等。

要真正做到这一点,我们应该使用异步消息传递模式。我刚刚完成了一个使用nServiceBus和MSMQ进行此操作的项目。

基本上,每个步骤都是通过将消息发送到队列来进行的。当nServiceBus发现在队列中等待的消息时,它将调用与该消息类型相对应的Handle方法。这样,每个单独的步骤都可以独立地失败和重试。如果某一步骤失败,则该消息将最终进入错误队列,因此我们以后可以轻松地重试该消息。

建议的这些纯代码解决方案并不那么健壮,因为如果某个步骤失败,我们将无法在将来仅重试该步骤,并且我们将不得不实施某些情况下甚至不可能实现的回滚代码。

回滚是困难的AFAIK,实际上只有两种方法可以解决。两阶段提交协议或者补偿事务。我们确实必须找到一种以这些方式中的一种来组织任务的方法。

通常,更好的主意是利用其他人的辛勤工作,并使用已内置2PC或者补偿功能的技术。这就是RDBMS如此受欢迎的原因之一。

因此,具体细节取决于任务...但是这种模式相当简单:

class Compensator {
   Action Action { get; set; }
   Action Compensate { get; set; }
}

Queue<Compensator> actions = new Queue<Compensator>(new Compensator[] { 
   new Compensator(SendEmail, UndoSendEmail),
   new Compensator(ArchiveReportsInDatabase, UndoArchiveReportsInDatabase),
   new Compensator(CreateAFile, UndoCreateAFile)
});

Queue<Compensator> doneActions = new Queue<Compensator>();
while (var c = actions.Dequeue() != null) {
   try {
      c.Action();
      doneActions.Add(c);
   } catch {
      try {
        doneActions.Each(d => d.Compensate());
      } catch (EXception ex) {
        throw new OhCrapException("Couldn't rollback", doneActions, ex);
      }
      throw;
   }
}

当然,对于特定任务,我们可能会很幸运。

  • 显然,RDBMS工作已经可以包装在事务中。
  • 如果我们使用的是Vista或者Server 2008,则可以使用事务性NTFS来涵盖CreateFile方案。
  • 电子邮件有点棘手-我不知道周围有任何2PC或者补偿器(不过,如果有人指出Exchange有一个2PC或者补偿器,我只会感到有些惊讶),所以我可能会使用MSMQ编写通知并订阅者将其领取并最终通过电子邮件发送。到那时,事务确实包括只将消息发送到队列,但这可能已经足够了。

所有这些都可以参与System.Transactions事务,因此状态应该很好。