同步数据库和Kafka生产者之间的事务

时间:2018-09-06 15:31:55

标签: apache-kafka spring-transactions spring-kafka distributed-transactions

我们有一个微服务架构,其中Kafka被用作服务之间的通信机制。一些服务具有自己的数据库。假设用户调用了服务A,这将导致在该服务的数据库中创建一条记录(或一组记录)。此外,此事件应作为Kafka主题的一项报告给其他服务。确保仅在成功更新Kafka主题(实质上是围绕数据库更新和Kafka更新创建分布式事务)时才写入数据库记录的最佳方法是什么?

我们正在考虑使用spring-kafka(在Spring Boot WebFlux服务中),我可以看到它有一个KafkaTransactionManager,但据我了解,这更多是关于Kafka事务本身(确保而不是在两个系统之间同步事务(请参见here),而不是在两个Kafka生产者和消费者之间保持一致:“ Kafka不支持XA,您必须应对DB tx可能在Kafka tx滚动时提交的可能性背部。”)。此外,我认为此类依赖于Spring的事务框架,至少就我目前所知,该框架是线程绑定的,如果使用反应性方法(例如WebFlux)在操作的不同部分执行该方法,则该类将无法工作不同的线程。 (我们使用reactive-pg-client,因此是手动处理交易,而不是使用Spring的框架。)

我能想到的一些选择:

  1. 请勿将数据写入数据库:仅将其写入Kafka。然后使用使用者(在服务A中)更新数据库。看来这可能不是最有效的,并且会出现问题,因为用户调用的服务无法立即看到它应该刚刚创建的数据库更改。
  2. 不要直接写到Kafka:仅写到数据库,并使用Debezium之类的内容将更改报告给Kafka。这里的问题是,更改是基于单个数据库记录的,而要存储在Kafka中的业务重大事件可能涉及多个表中数据的组合。
  3. 首先写入数据库(如果失败,则不执行任何操作,仅引发异常)。然后,在写入Kafka时,假设写入可能会失败。使用内置的自动重试功能可以使其保持尝试一段时间。如果最终完全失败,请尝试写入死信队列,并为管理员创建某种手动机制以将其解决。而且,如果写入DLQ失败(即Kafka完全关闭),只需以其他方式记录(例如,记录到数据库),然后再次创建某种手动机制,供管理员进行分类即可。

有人对以上内容有任何想法或建议,还是能够纠正我以上假设中的任何错误?

谢谢!

3 个答案:

答案 0 :(得分:6)

我建议使用方法2稍有改动的变体。

仅写到您的数据库中,但除了实际的表写操作外,还将“事件”写到同一数据库内的特殊表中;这些事件记录将包含您需要的汇总。以最简单的方式,您只需插入另一个实体,例如由JPA映射,该JPA包含带有聚合有效负载的JSON属性。当然,可以通过事务侦听器/框架组件的某种方式使之自动化。

然后使用Debezium仅从该表中捕获更改并将其流式传输到Kafka。这样一来,您就可以同时拥有两种状态:最终在Kafka中保持一致的状态(Kafka中的事件可能会落后或重新启动后第二次可能会看到一些事件,但最终它们会反映数据库状态),而无需进行分布式事务,以及您需要的业务级别事件语义。

(免责声明:我是Debezium的负责人;很有趣的是,我只是在撰写一篇博客文章,详细讨论这种方法)

这是帖子

https://debezium.io/blog/2018/09/20/materializing-aggregate-views-with-hibernate-and-debezium/

https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/

答案 1 :(得分:2)

上述所有方法都是解决问题的最佳方法,并且是定义明确的模式。您可以在下面提供的链接中进行探索。

模式:交易发件箱

通过将事件或消息保存在数据库的“发件箱”中来发布事件或消息。 http://microservices.io/patterns/data/transactional-outbox.html

模式:轮询发布者

通过轮询数据库中的发件箱来发布消息。 http://microservices.io/patterns/data/polling-publisher.html

模式:事务日志拖尾

通过尾随事务日志发布对数据库所做的更改。 http://microservices.io/patterns/data/transaction-log-tailing.html

答案 2 :(得分:1)

首先,我必须说我既不是Kafka,也不是Spring专家,但我认为在编写独立资源时,这在概念上更具挑战性,因此该解决方案应适合您的技术堆栈。此外,我应该说这种解决方案试图在没有Debezium这样的外部组件的情况下解决问题,因为在我看来,每个其他组件都会给测试,维护和运行应用程序带来挑战,而在选择这种选项时,这些应用程序常常被低估了。同样,并非每个数据库都可以用作Debezium源。

为确保我们在谈论相同的目标,让我们在简化的航空公司示例中阐明情况,在该示例中,客户可以购买机票。成功下单后,客户将收到由外部消息传递系统(我们必须与之交谈的系统)发送的消息(邮件,推送通知等)。

在传统的JMS世界中,我们的数据库(存储订单的地方)和JMS提供者之间存在XA事务,它看起来如下所示:客户端将订单设置为我们在我们开始事务的应用程序。该应用程序将订单存储在其数据库中。然后,该消息将发送到JMS,您可以提交事务。两家公司都在参与交易,即使他们正在与自己的资源进行交流。由于XA交易保证了ACID,所以我们没事。

让游戏中带上卡夫卡(或其他无法参与XA交易的资源)。由于不再有协调器同步两个事务,因此下面的主要思想是将处理分为具有持久状态的两个部分。

当您将订单存储在数据库中时,您还可以将消息(包含汇总数据)存储在您要随后发送给Kafka的同一数据库中(例如,作为CLOB列中的JSON)。相同的资源–保证使用ACID,到目前为止一切正常。现在,您需要一种机制来轮询“ KafkaTasks”表,以查找应发送到Kafka主题的新任务(例如,使用计时器服务,也许可以在Spring中使用@Scheduled注释)。将消息成功发送到Kafka后,您可以删除任务条目。这样可以确保仅在订单也成功存储在应用程序数据库中时,才向Kafka发送消息。我们是否获得了与使用XA交易时相同的保证?不幸的是,没有,因为仍然有可能对Kafka进行写操作,但是删除任务失败。在这种情况下,重试机制(您需要使用问题中提到的机制)将重新处理任务,然后发送两次消息。如果您的业务案例对这种“至少一次”的保证感到满意,那么您可以在这里完成一个imho半复杂的解决方案,该解决方案可以很容易地实现为框架功能,因此并不是每个人都需要打扰细节。

如果您需要“完全一次”,则无法将状态存储在应用程序数据库中(在这种情况下,“删除任务”是“状态”),而必须将其存储在Kafka中(假设您拥有两个Kafka主题之间的ACID保证)。例如:假设您在表中有100个任务(ID为1到100),并且任务作业处理前10个任务。您将Kafka消息写入其主题,将另一条ID为10的消息写入您的主题。全部都在同一个Kafka交易中。在下一个周期中,您将使用主题(值为10),并使用该值来获取下一个10个任务(并删除已处理的任务)。

如果有更简单的(应用程序内)解决方案,并且具有相同的保证,我希望能收到您的来信!

很抱歉,答案很长,但希望对您有所帮助。