事务的一个用例: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 的事务回滚策略

第五部分:Apache Kafka 在 Spring Cloud Stream Kafka 应用中的 Exactly-Once 语义

作为本系列博客的最后一部分,我们将深入探讨一个由 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 设计模式通常是消息系统的一个很好的抽象(正如我们在本系列的第 3 部分中讨论的那样),但在使用 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 生态系统中的事务支持时,所有这些都可以在单个原子单元的范围内完成。正如我们在第 3 部分的详细分析中所见,Kafka 事务与数据库事务同步。

在将此视为 outbox 模式的替代策略时,需要记住一些注意事项。这里提出的想法并完全等同于 outbox 模式提供的语义。如果您的用例需要该级别的保证,建议直接使用 outbox 模式。在下面的章节中,我们将指出这些解决方案缺乏 outbox 模式完整保证的情况。

生产者发起应用中的 Outbox 模式语义

在本系列的第 2 部分中,我们讨论了生产者发起事务

@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 模式语义

对于消费-处理-生产类型的应用程序,情况更加复杂,因为 Spring for Apache Kafka 中的消息监听器容器在消费记录后会启动一个 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 将内部的 run 方法包装在一个 JPA 事务中。如果数据库操作成功,我们发布到 Kafka 主题,并且 Kafka 发布操作使用消息监听器容器在此过程开始时创建的相同事务资源。方法退出后,JPA 提交,一旦控制权回到消息监听器容器,它就会提交 Kafka 事务。

以下是其图示

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

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

特别注意事项

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

结论

基于我们在本系列中学习到的事务基础知识,我们在本文中看到了一些在 Spring 中使用的策略,这些策略适用于应用程序需要使用 outbox 模式的情况。这些策略通过利用 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 的事务回滚策略

第五部分:Apache Kafka 在 Spring Cloud Stream Kafka 应用中的 Exactly-Once 语义

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

领先一步

VMware 提供培训和认证,助力您快速进步。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部