最终与数据库和消息队列记录保持一致

Posted

技术标签:

【中文标题】最终与数据库和消息队列记录保持一致【英文标题】:Eventual consistency with both database and message queue records 【发布时间】:2017-06-22 13:19:19 【问题描述】:

我有一个应用程序,我需要在数据库中存储一些数据(例如 mysql),然后在消息队列中发布一些数据。我的问题是:如果应用程序在存储到数据库后崩溃,我的数据将永远不会写入消息队列然后丢失(因此无法保证我的系统的最终一致性)。 我该如何解决这个问题?

【问题讨论】:

您需要一个跟踪器来知道哪些消息已发送,哪些消息未发送 这可能有助于vimeo.com/111998645 【参考方案1】:

我假设你有一个无损消息队列,一旦你得到写入数据的确认,队列就保证有记录。

基本上,您需要一个循环,其中包含可以回滚的事务或数据库中的状态。一笔交易的伪代码是:

开始交易 插入数据库 写入消息队列 消息队列确认后,提交事务

就个人而言,我可能会这样做:

以“待定”(或类似的)状态插入数据库 写入消息队列 消息确认后,将状态更改为“已提交”(或类似的内容)

在从失败中恢复的情况下,您可能需要检查消息队列以查看是否有任何“待处理”记录实际写入队列。

【讨论】:

感谢您的回答,我还是有一些问题: 1)如果我在提交事务之前写入消息队列,那么队列中的消息可能会在事务提交之前被处理。 2)如果我的ID是数据库生成的,我不能将它包含在发布到队列中的消息中。 这是一种非常简单的方法,使得不一致非常不太可能发生。它不保证一致性,因为在写入队列和提交到数据库之间可能会发生故障。这种方法很有趣,因为它很简单,并且对于许多实际应用来说可能是一个很好的平衡!除了写入消息队列外,它通常用于“保存到数据库并联系另一个系统”,因为在数据库之外只有 一个 副作用。请注意具有提交失败场景的数据库系统,例如 Galera Cluster。 @ayorosmage 通常数据库在插入时分配 ID,而不是在提交时分配,因此您应该有权访问该 ID。不过,您对消息队列超越数据库事务提出了一个很好的观点。消费系统可以就这个 ID 联系生产系统,而生产系统将不知道它。我想知道这有多大可能...... 事务中的成功操作并不能保证 100% 事务本身将无错误地提交。这可能会导致不一致,因为到那时消息已经发送。而且,如果您决定将“发送消息”部分保留在事务之外,那么如果消息队列不可用,则必须回滚持久化的更改(因此对外界可见)。因此,系统的另一个组件可能已经在看到最终将回滚的数据库更新时采取了行动。【参考方案2】:

添加到@Gordon Linoff 所说的内容,假设持久消息传递(类似于 MSMQ?)方法/处理程序将是事务性的,因此如果全部成功,消息将写入队列并将数据写入您的视图模型,如果失败了,一切都会失败……

为了缓解 ID 问题,您需要使用 GUID 而不是 DB 生成的密钥(如果您使用消息传递,则无论如何都需要删除参照完整性并将 GUID 作为密钥引入)。

还有一个建议,不要更新数据库,而是只插入/更新插入(待处理的行,然后是完成的行),并让读者根据最新的行进行数据的投影(例如)

【讨论】:

【参考方案3】:

我有一个应用程序,我需要在数据库中存储一些数据(例如 mysql),然后在消息队列中发布一些数据。我的问题是:如果应用程序在存储到数据库后崩溃,我的数据将永远不会写入消息队列然后丢失(因此无法保证我的系统的最终一致性)。我该如何解决这个问题?

在这种特殊情况下,答案是从数据库中加载队列数据。

也就是说,您将需要排队的消息写入数据库,在用于写入数据的同一事务中。然后,异步地从数据库中读取该数据,并将其写入队列。

见 Reliable Messaging without Distributed Transactions,作者 Udi Dahan。

如果应用程序崩溃,恢复很简单——在重新启动期间,您可以查询数据库中所有未确认的消息,然后再次发送它们。

请注意,此设计确实希望消息的使用者是为at least once delivery 设计的。

【讨论】:

非常有用的视频。但令我惊讶的是,没有已知的图书馆在做这个过程的“一部分” 还需要一个支持事务的数据库。因此我认为像 mongodb 这样的 nosql 数据库不能用于此。 应该有域事件是幂等的吗?是否有其他变体使用非幂等域事件来实现最终一致性? 即使我们使用表将数据写入队列,并在队列成功后删除记录,我们仍然会遇到同样的问题不是吗?在 t1 我们读取,t2 我们排队到 MQ 和 t3 我们得到确认,在 t4 我们清除数据库中的记录。如果该过程在 t3-t4 之间失败,我们仍然会遇到同样的问题。我知道 MQ 不会完全接受一次交付,但我想在这里表达你的想法 @SajithSilva 这就是为什么作者提到了至少一次交付的构造。【参考方案4】:

恐怕答案(VoiceOfUnreason,Udi Dahan)只是将问题扫到地毯下。地毯下的问题是:应该如何设计从数据库到队列的数据移动,以便消息只发布一次(没有 XA)。如果您解决了这个问题,那么您可以通过任何其他业务逻辑轻松扩展该概念。

CAP theorem 清楚地告诉你限制。

XA 交易不是 100% 防弹的解决方案,但在我看来是我见过的所有其他交易中最好的。

【讨论】:

从技术上讲,他们已将要求降低为“消息必须是幂等的”。这很有帮助。在该级别通常可以实现,例如在消费端使用INSERT IGNORE【参考方案5】:

将消息作为事务的一部分写入是一个好主意,但它有多个缺点,例如

如果你的

一个。数据库/语言不支持事务

b.交易是耗时的操作

c。您不能在响应服务呼叫时等待队列响应。

d。如果你的数据库已经处于压力之下,写消息会加剧工作量增加的影响。

最佳做法是使用数据库流。大多数现代数据库都支持流(Dynamodb、mongodb、orcale 等)。您有正在运行的数据库流消费者,它从数据库流中读取数据并写入队列或使缓存无效,添加到搜索索引器等。一旦所有这些都成功,您将流项目标记为已处理。

这种方法的优点

    它适用于多区域部署且出现区域故障的情况。 (您应该从区域流中读取数据并为所有区域数据存储进行水合。)

    没有写入更多记录的开销或队列的性能瓶颈。

    您可以将此模式用于其他数据源,例如缓存、排队、搜索。

缺点

    您可能需要调用多个服务来构造适当的消息。

    一个数据库流可能不足以构建适当的消息。

    确保流的可靠性,例如redis stream is not reliable

注意这种方法也不能保证完全一次语义。消费者逻辑应该是幂等的并且应该能够处理重复的消息

【讨论】:

如前所述,重申一下,数据库/更改流推送仅在单个建立连接后发生的事件以使用数据。这是大多数时候的主要缺点。

以上是关于最终与数据库和消息队列记录保持一致的主要内容,如果未能解决你的问题,请参考以下文章

使用消息队列解决分布式事务一致性问题

关于消息队列的一些思考

消息队列价值思考

消息队列常见使用场景梳理

用了这么久的消息队列,你知道为什么需要它吗?

消息队列(三)kafka的一致性和失败处理策略