领先一步
VMware 提供培训和认证,助您加速进步。
了解更多本系列博客的其他部分
第一部分:Spring Cloud Stream Kafka 应用程序中的事务简介
第二部分:Spring Cloud Stream Kafka 应用程序中的生产者启动事务
第三部分:Spring Cloud Stream 中与外部事务管理器同步
第四部分:Spring Cloud Stream 和 Apache Kafka 的事务回滚策略
第五部分:Spring Cloud Stream Kafka 应用程序中的 Apache Kafka 的精确一次语义
在本系列博客的最后一部分,我们将深入探讨一个由Chris Richardson首先提出但从 Spring Cloud Stream 的角度来看相对较新的设计模式。我们将了解 Outbox 模式是什么,它是如何工作的,以及在使用 Spring Cloud Stream 和 Apache Kafka 时采用该模式的几种策略。有关 Outbox 模式工作原理的介绍,请参阅此处的说明。
简而言之,Outbox 模式通过严格避免两阶段提交 (2PC),在一个原子单元内确保数据库或外部系统接收消息并发布到消息系统。
在 Outbox 模式中,开发人员需要遵循以下步骤
下图直观展示了这一流程

结果是,事件的**端到端**流在语义上是事务性的。我们写“语义上”,因为更新消息系统的过程(在本例中)在数据库事务之外,但实现了事务系统所保证的数据完整性。如果数据库写入成功,下游进程将看到这一点,并将记录从 outbox 表发布到 Kafka 主题。如果数据库事务不成功,则不会写入任何内容到 Kafka。需要注意的是,在 Kafka 发布和删除 outbox 记录时,我们仍然需要使用同步机制。
使用 Outbox 模式的一个重要优点是它避免了复杂的事务策略,例如分布式两阶段提交 (2-PC) 或使用单一共享事务资源协调各种提交等。但通过引入一些额外的进程,例如将事件持久化到 outbox 表,然后由另一个进程基于此将事件发布到消息代理,它仍然能提供分布式事务的语义好处。
Outbox 模式在许多涉及消息代理的不同用例中都有效。如果您的用例特别需要使用此模式,您可以按照规定来实现此模式。然而,在本博客中,我们向使用 Spring 和 Apache Kafka 的用户展示了一些替代策略,如果可以放宽遵循 Outbox 模式的严格规则。
尽管从概念上讲,Outbox 设计模式是消息系统的良好抽象,尤其是在应用程序想要避免 2PC 时,正如我们在本系列第三部分中所讨论的,但对于 Apache Kafka 和 Spring Cloud Stream,如果您不需要 Outbox 模式的完整支持,也有一些选择。首先,实现存在复杂性,例如应用程序需要维护一个额外的数据库表用于 outbox,需要额外的代码来消费它然后发布到 Kafka,消息发布后需要更多的代码来显式删除它,等等。
在编写 Spring Cloud Stream Kafka 应用程序时,我们可以通过利用 Spring Cloud Stream 通过 Spring for Apache Kafka 提供的事务支持来避免这种复杂性。
想象一个与上面相同的 order-service 编写的服务,但重写为一个事务性的 Spring Cloud Stream 应用程序。与原始 Outbox 模式避免 2PC 的前提一样,在此模型中我们也不必使用分布式事务管理器的 2PC。同时,我们还可以避免需要额外的 outbox 表以及用于查询它并将其发布到 Kafka 主题的外部代码。在使用 Spring Cloud Stream Kafka 生态系统中的事务支持时,所有这些都可以在单个原子单元的范围内完成。正如我们在第三部分的详细分析中所见,Kafka 事务与数据库事务同步。
将此作为 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 模式所提出的相同结果。
这是该流程的直观表示

从图中可以看出,端到端流程运行在一个事务上下文中,并且此解决方案不需要额外的 outbox 表以及用于查询它然后仅发布到 Kafka 等的外部进程。**但有一个重要的注意事项。** 如果应用程序在数据库操作后崩溃,则不会将任何数据发送到 Kafka,这会导致应用程序处于不一致状态。如果您的应用程序无法承受这种不一致性,最佳解决方案是依赖 Outbox 模式(或使用适当的 2PC 策略)。
对于**消费-处理-生产**类型的应用程序,情况更为复杂,因为 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 事务。
以下是这张图示

这样,我们就可以保持实现非常精简,而无需额外的数据库设置和外部进程来查询表并将其发布到 Kafka,以及同步、删除 outbox 记录和其他复杂性。
与生产者启动场景一样,这里也有几点需要注意。此解决方案在应用程序中途崩溃(例如,在数据库操作之后)时,不提供任何容错能力。在这种情况下,不会将任何记录发布到 Kafka,这会导致应用程序处于不一致状态。您需要编写应用程序级别的保护措施,例如幂等使用者和其他类似策略,以确保应用程序在不一致期间正常工作,这可能容易出错且不切实际。因此,在这种情况下,您的最佳选择是考虑使用正确的 Outbox 模式或实现一些 2PC 策略。
基于贯穿本系列学习的事务基础,我们在本文中看到了当应用程序需要使用 Outbox 模式时,可以在 Spring 中使用的一些策略。这些策略通过利用 Spring 和 Apache Kafka 中的事务支持,采用了轻量级的方法。这些解决方案不能替代 Outbox 模式,但如果您的应用程序不需要 Outbox 模式的完整保证,可以作为一些参考。
在此再次重申,在**消费-处理-生产**模式和**生产者启动**事务场景中,如果您想严格遵循 Outbox 模式实现的原始规则,可以在不经过上述捷径的情况下做到。Spring Cloud Stream 和 Spring for Apache Kafka 允许您这样做。只需按照规定遵循模式即可。
在结束本系列关于 Spring Cloud Stream 和 Apache Kafka 事务的介绍之际,我想特别感谢几位在整个系列中给予我宝贵反馈和指导的人。我想以一种非常特别的方式感谢 Gary Russell,他是 Spring for Apache Kafka 的项目负责人,他指导我了解了 Spring for Apache Kafka 中事务在非常底层的工作原理。Gary 回答了我无数关于 Spring 和事务的问题,尤其是从 Spring for Apache Kafka/Spring Cloud Stream 的角度,我非常感激他。我还要特别感谢 Jay Bryant 仔细校对了所有博客草稿并进行了必要的更正。还要特别感谢 Ilayaperumal Gopinathan 和 Oleg Zhurakousky 给予的所有指导和支持。
再次,以下是本系列所有其他部分的链接。
第一部分:Spring Cloud Stream Kafka 应用程序中的事务简介
第二部分:Spring Cloud Stream Kafka 应用程序中的生产者启动事务
第三部分:Spring Cloud Stream 中与外部事务管理器同步