消息队列和数据库-解决双倍写入问题

305 阅读4分钟

开发一个现代的应用程序意味着为云而开发,正常运行时间、可扩展性、地理分布和低延迟是最重要的关注点。这导致了基于事件驱动的微服务的应用架构的广泛采用。将一个应用程序的元素分解成微服务,使我们能够(例如)独立地扩展不同的服务。这只是为云计算架构应用程序的最有效方式。

然而,拥抱事件驱动的微服务也带来了一些挑战。由于有这么多不同的服务同时在运行,它们之间的通信可能成为一个挑战。

例如,如果一个微服务需要向另一个服务发送一些数据或请求,但另一个服务很忙,怎么办?如果这两个服务必须互相等待,我们就会失去微服务架构的一些效率。

这个问题的一个解决方案是消息队列。

什么是消息队列?

消息队列本质上是一个中间存储队列,它允许微服务之间进行异步通信。消息队列允许一个服务向另一个服务发送一个 "消息",即使另一个服务还没有准备好接收它。

例如,在下图中,服务1可能有需要发送的消息给服务2。使用一个消息队列,它可以根据需要发送这些消息并继续运行,不管服务2是否准备好接收它们。然后,这些消息被储存在队列中,直到服务2检索它们。

how message queuing works on a basic level

这使得整个系统更有效,更容易扩展。通过解耦服务1和服务2,我们可以使它们各自运行而不必等待对方,同时仍然允许它们通过中间的消息队列进行异步通信。

这也有助于我们减少级联故障的风险。如果我们的服务必须同步通信,而一个服务失败了,其他试图与该服务通信的服务也可能失败,然后与这些服务通信的服务也会失败,以此类推。

然而,当我们想在两个地方(如消息队列和数据库)存储相同的数据时,使用消息队列会导致一些隐秘的问题。

什么是双重写入问题?

有时,我们需要一个服务将同一条数据发送到两个存储位置,同时确保它们之间的一致性。例如,当一个特定的事件发生时,我们可能想用相同的信息同时更新数据库和消息队列(或消息队列系统,如Apache Kafka)。这就是所谓的双重写--我们把相同的数据写到两个不同的地方。

但是,如果这两个更新中的一个成功了,另一个失败了,会发生什么?这就是双写问题;如果我们试图在一个分布式系统中更新两个独立的存储方案,而没有一些额外的措施来确保它们之间的一致性,最终我们将得到一个不一致的状态。

the dual write problem with the database and a message queue such as Kafka

双重写入问题很容易在我们的雷达下飞过,因为只要我们的数据库和消息队列(例如)都在正常运行,就不会出现不一致的情况。而当不一致出现时,我们可能并不总是注意到它们。

然而,从长远来看,忽视双重写入问题是不可持续的。最终,一个故障、错误或中断将导致不一致,对你的应用程序产生负面影响--很可能是你的业务。

事务性收件箱模式

解决这个问题的一种方法是一种被称为事务性收件箱模式的设计方法。这种方法需要一个交易型数据库,如CockroachDB。

它的工作原理是这样的:我们不是将数据发送到两个不同的地方,而是发送一个单一的事务,将数据的两个独立副本存储在数据库中。一个副本存储在相关的数据库表中,另一个副本存储在一个发件箱表中,随后我们将从该表中更新另一个存储位置。例如,我们可能会将发件箱连接到Kafka,或其他一些消息队列系统。

换句话说,这是一个两步的过程。首先,我们使用一个单一的事务来更新数据库的两个部分(相关的表和我们的事务性发件箱),这使我们能够保证要么两个更新都提交,要么都不提交。

然后,我们把更新从事务性发件箱推送到消息队列中。如果收件箱中的消息没能到达消息队列,也不会丢失数据或一致性。因为数据已经安全地存储在数据库中,我们可以简单地重试。

然而,这种方法确实需要一个额外的服务或工作:我们需要将事件从数据库发件箱移到消息队列中。那么,我们如何做到这一点呢?