事务用例:Spring Cloud Stream Kafka Binder 中的 Outbox 模式策略

工程 | Soby Chacko | 2023年10月24日 | ...

本博客系列中的其他部分

第一部分:Spring Cloud Stream Kafka 应用程序中的事务简介

第二部分:Spring Cloud Stream Kafka 应用程序中的生产者启动事务

第三部分:Spring Cloud Stream Kafka 应用程序中与外部事务管理器的同步

第四部分:使用 Spring Cloud Stream 和 Apache Kafka 的事务回滚策略

第五部分:Spring Cloud Stream Kafka 应用程序中 Apache Kafka 的精确一次语义

在本博客系列的最后一部分中,我们将深入探讨一个相对较新的设计模式,该模式最初由Chris Richardson提出,但从 Spring Cloud Stream 的角度来看。我们将了解 Outbox 模式是什么,它是如何工作的,以及在使用 Spring Cloud Stream 和 Apache Kafka 时的一些适应策略。请参阅此处的描述,以了解 Outbox 模式的运作方式。

Outbox 模式的快速总结

简而言之,Outbox 模式通过严格避免两阶段提交 (2PC),确保数据库或外部系统的数据交付和消息系统发布在一个原子单元内完成。

在 Outbox 模式中,开发者需要遵循以下步骤:

  1. 处理器方法接收消息。
  2. 在其逻辑中,它首先以事务方式与数据库交互,然后在同一事务中在名为 Outbox 的特定表中创建一个新记录。
  3. 外部进程查询此 Outbox 表并将消息发布到 Kafka。
  4. 一旦 Kafka 发布成功,该记录就会从 Outbox 表中删除。

这是一个流程图:

outbox-pattern-txn-blog-part-6

结果是事件的**端到端**流程在语义上以事务方式完成。我们写道“语义上”,因为更新消息系统的进程(在本例中)位于数据库事务之外,但实现了事务系统保证的数据完整性保证。如果数据库写入成功,下游进程会看到这一点并将记录从 Outbox 表发布到 Kafka 主题。如果数据库事务不成功,则不会写入任何内容到 Kafka。需要注意的是,在 Kafka 发布部分和删除 Outbox 记录期间,我们仍然需要使用同步。

使用 Outbox 模式的⼀个重要好处是,它避免了复杂的事务策略,例如分布式两阶段提交 (2-PC) 或使用单个共享事务资源协调各种提交等。但通过引入一些额外的进程,例如将事件持久化到 Outbox 表,然后基于此让另一个进程将事件发布到消息代理,它仍然提供了分布式事务的语义好处。

在 Spring Cloud Stream 中适应 Outbox 模式

Outbox 模式适用于许多涉及消息代理的不同用例。如果您的用例特别需要使用此模式,您可以按照规定的方式实现此模式。但是,在本博客中,如果你是 Spring 和 Apache Kafka 用户并且可以放宽遵循 Outbox 模式的严格规则,我们将向您展示一些针对这些用例的替代策略。

尽管从概念上讲,当应用程序想要避免 2PC 时,Outbox 设计模式对于一般的消息系统来说是一个很好的抽象,正如我们在本系列的第三部分中讨论的那样,对于 Apache Kafka 和 Spring Cloud Stream,如果您不需要 Outbox 模式的全面支持,则有一些选择。首先,实现中存在复杂性,例如应用程序需要为 Outbox 保持额外的数据库表、额外的代码来使用它然后发布到 Kafka、更多的代码来在消息发布后显式地从它中删除它等等。

在编写 Spring Cloud Stream Kafka 应用程序时,我们可以通过依赖 Spring Cloud Stream 通过 Spring for Apache Kafka 提供的事务支持来避免这种复杂性。

想象一下,为与上述相同的订单服务编写的服务,但将其重写为事务性 Spring Cloud Stream 应用程序。与原始 Outbox 模式避免 2PC 的前提一样,我们也不必在此模型中使用带有分布式事务管理器的两阶段提交。同时,我们还可以避免需要额外的 Outbox 表和查询它的外部代码,然后将其发布到 Kafka 主题。在使用 Spring Cloud Stream Kafka 生态系统中的事务支持时,所有这些都可以在单个原子单元的范围内完成。正如我们在第三部分的详细分析中看到的,Kafka 事务与数据库事务同步。

将此作为 Outbox 模式的替代策略时,需要记住一些注意事项。此处提出的想法**并非** Outbox 模式提供的完全语义等效项。如果您的用例需要那种级别的保证,建议直接使用 Outbox 模式。在下文中,我们将指出解决方案缺乏 Outbox 模式全部保证的情况。

生产者启动应用程序中的 Outbox 模式语义

在本系列的第二部分中,我们看到了生产者启动的事务。

@Autowired
Sender sender;

@PostMapping("/send-data")
public void sendData() throws InterruptedException {
   sender.send(streamBridge, repository);
}

@Component
static class Sender {

   @Transactional
   public void send(StreamBridge streamBridge, OrderRepository repository){
       Order order = new Order();
       order.setId("order-id");

       Order savedOrder = repository.save(order);

       OrderEvent event = new OrderEvent();
       event.setId(savedOrder.getId());
       event.setType("OrderType");
       streamBridge.send("process-out-0", event);
   }
}

工作流的主要触发器是一个 REST 端点,它调用一个用@Transactional注解的方法。事务拦截器启动 JPA 事务,并发生数据库操作,但由于方法处于事务中间,因此不会作为其一部分进行提交。之后,我们通过StreamBridge send 方法发布到 Kafka。StreamBridge使用的KafkaTemplate使用事务性生产者工厂(假设我们设置了transaction-id-prefix)。事务性资源不是启动新的 Kafka 事务,而是与 JPA 事务同步。当方法退出时,JPA 首先提交,然后是同步的 Kafka 提交。正如您所看到的,它通过使用不同的策略实现了 Outbox 模式提出的相同结果。

这是一个此流程的可视化表示:

producer-init-txn-blog-part-6

从该图可以看出,端到端流程作为单个事务上下文的一部分运行,并且此解决方案不需要任何额外的 Outbox 表和外部进程来查询它,然后只发布到 Kafka 等等。**但是有一个重要的警告。**如果应用程序在数据库操作后崩溃,则不会向 Kafka 发送任何数据,这会使应用程序处于不一致状态。如果您的应用程序无法承受这种不一致性,最佳解决方案是依赖 Outbox 模式(或使用适当的 2-PC 策略)。

在消费-处理-生产应用中使用Outbox模式语义

对于消费-处理-生产类型的应用程序,情况更为复杂,因为Apache Kafka的Spring消息监听容器在消费记录后会启动一个Kafka事务。

让我们回顾一下我们在本系列第3篇博客中看到的消费-处理-生产模式的代码。

@Bean
public Consumer<OrderEvent> process(TxCode txCode) {
   return txCode::run;
}

@Component
class TxCode {

   @Transactional
   void run(OrderEvent orderEvent) {
       Order order = new Order();
       order.setId(orderEvent.getId());

       Order savedOrder = repository.save(order);

       OrderEvent event = new OrderEvent();
       event.setId(savedOver.getId());
       event.setType("OrderType");
       streamBridge.send("process-out-0", event);
   }
}

这段代码以事务方式向数据库和Kafka发布消息。

消息监听容器启动Kafka事务,然后我们使用@Transactional用JPA事务包装我们的内部运行方法。如果数据库操作成功,我们将发布到Kafka主题,并且Kafka发布操作使用在此过程开始时由消息监听容器创建的相同事务资源。方法退出后,JPA提交,一旦控制权返回到消息监听容器,它就提交Kafka事务。

以下是图示说明

cons-process-prod-txn-blog-part-6

通过这种方式,我们可以保持实现非常简洁,无需额外的数据库设置和外部进程来查询表并将数据发布到Kafka,无需同步、删除outbox记录和其他复杂操作。

特殊注意事项

与生产者启动的场景一样,这里也有一些需要注意的地方。如果应用程序在中间崩溃,例如在数据库操作之后,此解决方案不提供任何容错能力。在这种情况下,没有任何记录发布到Kafka,这会使应用程序处于不一致状态。您需要编写应用程序级别的安全措施,例如幂等消费者和其他类似策略,以确保应用程序在此不一致期间正常工作,但这可能容易出错且不太实用。因此,在这种情况下,您最好的选择是考虑使用正确的outbox模式或实现一些两阶段提交(2-PC)策略。

结论

在本系列中,我们学习了事务的基础知识,在本文中,我们了解了在应用程序需要使用outbox模式时,可以在Spring中使用的一些策略。这些策略通过利用Spring和Apache Kafka中的事务支持来采用轻量级的方法。这些解决方案不能替代outbox模式,而是作为一些提示,以供您的应用程序不需要outbox模式的全部保证时考虑。

值得在此重复的是,在消费-处理-生产模式和生产者启动的事务场景中,如果您想严格遵守outbox模式实现的原始规则,您可以这样做而无需使用上述快捷方式。Spring Cloud Stream和Spring for Apache Kafka允许您这样做。只需按照规定的模式进行操作即可。

鸣谢

在我们结束关于Spring Cloud Stream和Apache Kafka事务的这个系列时,我要感谢一些在整个系列中为我提供宝贵反馈和指导的人。我要特别感谢Spring for Apache Kafka的项目负责人Gary Russell,他指导我了解Spring for Apache Kafka中事务如何在非常低的级别上工作的所有技术细节。Gary回答了我无数关于Spring和事务的问题,尤其是从Spring for Apache Kafka/Spring Cloud Stream的角度来看,我非常感谢他。我还特别感谢Jay Bryant细致地校对所有博客草稿并进行所有必要的更正。还要特别感谢Ilayaperumal GopinathanOleg Zhurakousky提供的指导和支持。

再次,以下是本博客系列中所有其他部分的链接。

第一部分:Spring Cloud Stream Kafka 应用程序中的事务简介

第二部分:Spring Cloud Stream Kafka 应用程序中的生产者启动事务

第三部分:Spring Cloud Stream Kafka 应用程序中与外部事务管理器的同步

第四部分:使用 Spring Cloud Stream 和 Apache Kafka 的事务回滚策略

第五部分:Spring Cloud Stream Kafka 应用程序中 Apache Kafka 的精确一次语义

获取Spring新闻通讯

通过Spring新闻通讯保持联系

订阅

领先一步

VMware提供培训和认证,以加快您的进度。

了解更多

获得支持

Tanzu Spring在一个简单的订阅中提供OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部