由于许多新应用程序是作为微服务系统构建的,因此双重写入已成为一个普遍的问题。它们是导致数据不一致的最常见原因之一。更糟的是,许多开发人员甚至都不知道双重写入是什么。
什么是双重写入?
双重写入描述了您在两个个系统(例如数据库和Apache Kafka)中更改数据时的情况,而没有额外的层来确保两个服务上的数据一致性。
只要两个操作都成功,一切就OK了。即使第一笔交易失败,也还是可以的。但是,如果您成功提交了第一笔交易而第二笔交易失败,则说明您遇到了问题。您的系统现在处于不一致状态,没有容易修复的方法。
分布式事务不再是一种选择
过去,当我们构建整体/.单体/Monolith时,我们使用分布式事务来避免这种情况。分布式事务使用两阶段提交协议。它将事务的提交过程分为两个步骤,并确保所有系统的ACID原则。
但是,如果我们要构建微服务系统,则不会使用分布式事务。这些交易需要锁,并且无法很好地扩展。他们还需要所有涉及的系统同时启动和运行。
那你该怎么办呢?
3个无效的“解决方案”
当我在会议演讲中或在一个研讨会上与与会者讨论此主题时,我经常听到以下3条建议之一:
- 是的,我们知道此问题,我们没有解决方案。但这还不错。到目前为止,什么都没有发生。让我们保持原样。
- 让我们将与Apache Kafka的交互移动到提交后监听器。
- 在提交数据库事务之前,让我们将事件写入Kafka中的主题。
好吧,很明显,建议1的风险很大。它可能在大多数时间都有效。但是迟早,您将在服务存储的数据之间创建越来越多的不一致之处。
因此,让我们集中讨论选项2和3。
2. 在提交后监听器中发布事件
在提交后监听器中发布事件是一种非常流行的方法。它确保仅在数据库事务成功时才发布事件。但是,很难解决Kafka崩溃或任何其他原因阻止您发布事件的情况。
您已经提交了数据库事务。因此,您无法轻松地还原这些更改。当您尝试在Kafka中发布事件时,其他事务可能已经使用并修改了该数据。
您可能会尝试将故障保留在数据库中,并运行常规的清理作业以尝试恢复失败的事件。这可能看起来像是一个合理的解决方案,但是它有一些缺陷:
- 仅当您可以将失败的事件保留在数据库中时,它才有效。如果数据库事务失败,或者您的应用程序或数据库崩溃,然后才能存储有关失败事件的信息,则将丢失它。
- 仅当事件本身没有引起问题时,它才起作用。
- 如果在清除作业恢复失败的事件之前,另一个操作为该业务对象创建了一个事件,则事件将混乱。
这些似乎是假设的情况,但这就是我们正在准备的事情。本地事务,分布式事务和确保最终一致性的方法的主要思想是,绝对要确保您不会造成任何(永久)不一致。
提交后侦听器无法确保这一点。因此,让我们看一下其他选项。
3. 在提交数据库事务之前发布事件
在讨论了为何提交后监听器不起作用之后,通常会建议使用这种方法。如果在提交之后发布事件会导致问题,您只需在提交事务之前发布它,对吗?
好吧,不……让我解释一下……
如果您无法发布事件,则在提交事务之前发布事件使您可以回滚事务。那就对了。
但是,如果数据库事务失败,该怎么办?
您的操作可能违反唯一约束,或者同一数据库记录上可能有2个并发更新。提交期间将检查所有数据库约束,并且不能确保它们都不会失败。数据库事务也彼此隔离,因此如果不使用锁,就无法阻止并发更新。但这带来了新的可伸缩性问题。简而言之,您的数据库事务可能会失败,并且您无能为力,或者对此无能为力。
如果发生这种情况,则您的活动已经发布。其他微服务可能已经观察到它并触发了一些业务逻辑。您无法撤回活动。
如前所述,撤消操作失败的原因相同。您也许可以构建一个大多数情况下都可以使用的解决方案。但是您无法创建绝对安全的东西。
如何避免双重写入?
您可以选择几种方法来避免双重写入。但是您需要知道,如果不使用分布式事务,则只能构建最终一致的系统。
总体思路是将流程分为多个步骤。这些步骤中的每个步骤仅适用于一个数据存储,例如数据库或Apache Kafka。这使您能够使用本地事务,所涉及系统之间的异步通信以及异步的,可能无限的重试机制。
如果您只想在服务之间复制数据或通知其他服务已发生事件,则可以将发件箱模式与Debezium等变更数据捕获实现一起使用。我在以下文章中详细解释了这种方法:
- 使用Hibernate实现发件箱模式
- 使用Debezium用CDC实现发件箱模式
而且,如果您需要实施涉及多个服务的一致的写入操作,则可以使用SAGA模式。我将在以下文章之一中对其进行详细说明。
结论
双重写入常常被低估,许多开发人员甚至都不知道潜在的数据不一致。
如本文所述,在没有分布式事务或确保最终一致性的算法的情况下,写入2个或更多系统可能会导致数据不一致。如果使用多个本地事务,则无法处理所有错误情况。
避免这种情况的唯一方法是将通信分为多个步骤,并且在每个步骤中仅写入一个外部系统。SAGA模式和变更数据捕获实现(例如Debezium)使用这种方法来确保对多个系统的一致写入操作或将事件发送到Apache Kafka。