3

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

所有任务必须通过:

SendEmail ArchiveReportsInDatabase CreateAFile

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

4

10 回答 10

4

回滚很困难-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,那么您可以使用Transactional NTFS来覆盖您的 CreateFile 方案。
  • 电子邮件有点棘手 - 我不知道它周围有任何 2PC 或补偿器(如果有人指出 Exchange 有一个,我只会有点惊讶)所以我可能会使用MSMQ编写通知并让订阅者拿起它并最终通过电子邮件发送它。那时,您的事务实际上只涉及将消息发送到队列,但这可能已经足够了。

所有这些都可以参与到System.Transactions事务中,所以你的状态应该很好。

于 2008-10-03T20:53:36.817 回答
2

在 C# 中

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

于 2008-10-03T19:43:37.423 回答
1

另一个想法:

try {
    task1();
    task2();
    task3();
    ...
    taskN();
}
catch (TaskFailureException e) {
    dealWith(e);
}
于 2008-10-03T19:52:17.767 回答
1

几个建议:

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

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

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

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

于 2008-10-03T19:53:56.903 回答
0

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

  1. 将您的任务放在一组代码块或函数指针中。
  2. 遍历数组。
  3. 如果任何块返回失败,则中断。
于 2008-10-03T19:46:31.427 回答
0

您没有提到您正在使用什么编程语言/环境。如果是 .NET Framework,您可能想看看这篇文章。它描述了来自 Microsoft 的 Robotics Studio 的并发和控制运行时,它允许您对一组(异步)事件应用各种规则:例如,您可以等待任意数量的事件完成,如果一个事件失败则取消,等等。它也可以在多个线程中运行,所以你得到了一个非常强大的做事的方法。

于 2008-10-03T19:52:26.563 回答
0

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

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

SendEmail && ArchiveReportsInDatabase && CreateAFile
于 2008-10-03T19:52:43.817 回答
0

如果您使用的语言使用排序电路评估(Java 和 C#),您可以简单地执行以下操作:

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

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

于 2008-10-03T19:57:49.580 回答
0

例外通常对这类事情有好处。伪 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);
    ...
}

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

此外,您在代码中有一个点用于错误处理、日志记录、回滚等。

于 2008-10-03T20:00:31.180 回答
0

要真正做到正确,您应该使用异步消息传递模式。我刚刚完成了一个使用nServiceBus和 MSMQ 完成的项目。

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

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

于 2008-10-03T20:14:20.467 回答