使用 Rebus 和事务范围中登记的数据库连接发布消息时机器崩溃

Machine crash when publishing a message with Rebus and a database connection enlisted in a transaction scope

假设我在事务范围内登记了 Rebus 和数据库连接(例如 sql 服务器连接)。数据库连接上会执行一些数据库操作,Rebus 会发布一些消息,事务范围不会升级到 MSDTC(我检查过 Windows 上没有分布式事务,而且这个场景适用于 Linux 以及不支持 MSDTC 的地方)。 Complete() 在事务作用域上被调用,它指示数据库连接和 Rebus 进行提交。现在假设数据库连接先提交并成功,在 Rebus 提交(= 发布消息)之前,机器崩溃了。会发生什么?我能想到这些场景:

  1. 已提交数据库操作,但未发布任何消息(状态不正确)。
  2. 数据库操作被回滚(不确定由谁回滚,因为 MSDTC 不会参与,并且当机器重新启动时,我认为没有人会检查崩溃期间事务发生了什么),并且没有消息已发布(正确状态)。
  3. 提交数据库操作,并在机器重启后(由谁?)发布消息(正确状态)。

另外,我检查了与 NServiceBus 相同的场景,当与 MSMQ 一起使用时,事务范围升级到 MSDTC,NServiceBus 的创建者声称事务范围总是会有一个正确的结果——要么全部提交或全部回滚,无论机器是否在事务范围的任何点崩溃。

处理消息时,Rebus 按以下顺序执行工作:

  1. 您的处理程序已执行(可能涉及数据库事务以提交您自己的工作)
  2. 外发消息已发送
  3. 传入消息已从队列中删除

由于世界充满了失败,您的程序可能会在这些步骤之间(或期间!)的任何时候失败。

如果在 (1) 之前或期间发生故障,则没有问题,因为您自己的工作(至少在这种情况下)是在可以自动回滚的事务中执行的。

如果在 (1) 之后和完全完成 (3) 之前有些事情失败了,那么您将体验 Rebus 的 "at least once"-交付保证,这意味着 – 如果发生这样的失败 – 消息将至少被处理一次,这意味着它可能被处理两次,如果你不幸的话,甚至可能更多。

这是无法回避的事实,因此如果您关心这种情况,则需要制作您的消息处理程序 idempotent

幂等性可以通过多种方式实现:有时通过操作本身的幂等性(例如简单地更新接收到的数据,将某些字段的值设置为消息中的值等),有时通过依赖关于能够丢弃过时数据(例如,如果您可以将数据的 "last changed" 值与消息中的更新时间戳进行比较)。

但有时,如果您的系统因处理重新发送的消息而最终处于不良状态,则您需要精心编写代码以摆脱它,例如通过将已处理消息的消息 ID 存储在具有唯一约束的 table 中。

棘手的部分是:真正的幂等性要求您模拟所有公开可见的行为,在第二次处理消息时也是如此。这意味着在处理您的消息时,所有消息 sent/published 也必须第二次发送和发布。

正如您可能想象的那样,实现真正的幂等性并不总是微不足道的。

(...) the creators of NServiceBus claim that there would be always a correct outcome of the transaction scope - either all committed or all rolled back, no matter if the machine crashes at any point of the transaction scope (...)

对于分布式事务和两阶段提交,这不可能是真的,因为有可能出现第三种结果:所有事务在准备阶段确认,然后其中一个在提交阶段失败(因为网络中断、磁盘已满或其他一些不可恢复的问题)——那么事务协调器别无选择,只能让事务挂起,需要人工干预才能让世界继续。

Also, I checked the same scenario with NServiceBus, and when used with MSMQ, the transaction scope is escalated to MSDTC, and the creators of NServiceBus claim that there would be always a correct outcome of the transaction scope - either all committed or all rolled back, no matter if the machine crashes at any point of the transaction scope.

正如@mookid8000 提到的,没有 100% gua运行tees,即使在使用分布式 t运行sactions 时也是如此。原因是2 generals problem. But you could say that using distributed transactions beats everything else in terms of reliability. Unfortunately, it creates a lot of overhead and Serializable locks on your data in SQL Server. Oracle doesn't even support it。大多数 DBA 讨厌这个是有原因的。我已经使用 MSMQ 和 SQL 服务器构建了系统,运行 很好,但它需要一些思考。

另一件事是大多数资源不支持分布式 t运行 操作。如云中的一切、RabbitMQ 和许多其他技术。

@mookid8000 提到的一个很好的解决方案是将每条传入消息的标识符存储到数据库中,并验证该消息是否已被处理。但它并不止于此。想象一个事件正在发布,标识符为 1b068720-b558-4edf-9ebd-7142bc8cd3c0。然后我们试图告诉队列它可以删除消息,但是由于错误我们没有这样做。我们什么时候将消息标识符存储在数据库中并提交了 t运行saction?有没有成功?如果我们再次处理传入的消息,是否会在数据库中找到标识符?可能,但该标识符是在我们发布事件之前还是之后提交的?每一步都可能失败!

问题是,活动会再次发布吗?因为如果它会,用什么标识符?可能是一个新的独特的,比如 00d13f2b-ce5b-4880-9a5b-2cb541015902。这里的问题是,接收端点如何知道这是同一条逻辑消息并且不应该处理它,因为我们已经处理了消息但使用了另一个标识符?我们需要尝试确保该事件确实已发布,而且如果我们再次发布它,它也具有完全相同的标识符。否则,另一边的幂等性很难,甚至不可能!

这就是 Outbox Pattern 的用武之地。

如您所见,构建分布式系统并确保它们是故障安全的并不是那么容易。如果您有更多问题,可以随时reach out