我们有一个微服务架构,其中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的框架。)
我能想到的一些选择:
有人对以上内容有任何想法或建议,还是能够纠正我以上假设中的任何错误?
谢谢!
答案 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个任务(并删除已处理的任务)。
如果有更简单的(应用程序内)解决方案,并且具有相同的保证,我希望能收到您的来信!
很抱歉,答案很长,但希望对您有所帮助。