你知道微服务架构中的“发件箱模式”吗

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 微服务架构如今非常的流行,这个架构下可能经常会遇到“双写”的场景。双写是指您的应用程序需要在两个不同的系统中更改数据的情况,比如它需要将数据存储在数据库中并向消息队列发送事件。您需要保证这两个操作都会成功。如果两个操作之一失败,您的系统可能会变得不一致。那针对这样的情况有什么好的方法或者设计保证呢?本文就和大家分享一个“发件箱模式”, 可以很好的避免此类问题。

前言
微服务架构如今非常的流行,这个架构下可能经常会遇到“双写”的场景。双写是指您的应用程序需要在两个不同的系统中更改数据的情况,比如它需要将数据存储在数据库中并向消息队列发送事件。您需要保证这两个操作都会成功。如果两个操作之一失败,您的系统可能会变得不一致。那针对这样的情况有什么好的方法或者设计保证呢?本文就和大家分享一个“发件箱模式”, 可以很好的避免此类问题。

欢迎关注个人公众号『JAVA旭阳』交流沟通

下订单的例子
假设我们有一个 OrderService 类,它在创建新订单时被调用,此时它应该将订单实体保存在数据库中并向交付微服务发送一个事件,以便交付部门可以开始计划交付。
你的代码可能是下面这样子的:
@Service
public record OrderService(

IDeliveryMessageQueueService deliveryMessageQueueService,
IOrderRepository orderRepository,
TransactionTemplate transactionTemplate) implements IOrderService {

@Override
public void create(int id, String description) {
    String message = buildMessage(id, description);

    transactionTemplate.executeWithoutResult(transactionStatus -> {
        // 保存订单
        orderRepository.save(id, description);
    });

    // 发送消息
    deliveryMessageQueueService.send(message);
}

private String buildMessage(int id, String description) {
    // ...
}

}
复制代码
可以看到我们在事务中将订单保存在数据库中,然后我们使用消息队列将事件发送到交付服务。这是双写的一个场景。
这么写,会遇到什么问题呢?
首先,如果我们保存了订单但是发送消息失败了怎么办?送货服务永远不会收到消息。
那你可能想到把保存订单和发消息放到同一个事务中不就可以了吗,就是是将 deliveryMessageQueueService#send 移动到与 orderRepository#save 相同的事务中,如下图:
transactionTemplate.executeWithoutResult(transactionStatus -> {

        // 保存订单
        orderRepository.save(id, description);
        // 发送消息
        deliveryMessageQueueService.send(message);
    });

复制代码
实际上,在数据库事务内部建立 TCP 连接是一种糟糕的做法,我们不应该这样做。
有没有更好的方法呢?
我们可以订单表所在的同一数据库中有一个表“发件箱”(在最简单的情况下,它可以有一个列“消息”和当前时间戳)。保存订单时,在同一个事务中,我们在“发件箱”表中保存了一条消息。消息一发送,我们就可以将其从发件箱表中删除,代码如下:
@Service
public record OrderService(

IDeliveryMessageQueueService deliveryMessageQueueService,
IOrderRepository orderRepository,
IOutboxRepository outboxRepository,
TransactionTemplate transactionTemplate) implements IOrderService {

@Override
public void create(int id, String description) {
    UUID outboxId = UUID.randomUUID();
    String message = buildMessage(id, description);

    transactionTemplate.executeWithoutResult(transactionStatus -> {
        // 保存订单
        orderRepository.save(id, description);
        // 保存到发件箱
        outboxRepository.save(new OutboxEntity(outboxId, message));
    });

    deliveryMessageQueueService.send(message);
    
    // 删除
    outboxRepository.delete(outboxId);
}

private String buildMessage(int id, String description) {
    // ...
}

}
复制代码
可以看到,我们在一次事务中将订单和发件箱实体保存在我们的数据库中。然后我们发送一条消息,如果成功,我们删除这条消息。
如果 deliveryMessageQueueService#send 失败会怎样?(例如,您的应用程序被终止或消息队列或数据库不可用)。在这种情况下,outboxRepository#delete 将不会运行,我们必须重试发送消息。
它可以使用将在后台运行的计划任务来完成,该任务将尝试发送在表发件箱中显示超过 X 秒(例如 10 秒)的消息,如下面的代码。
@Service
public record OutboxRetryTask(IOutboxRepository outboxRepository,

                          IDeliveryMessageQueueService deliveryMessageQueueService) {

@Scheduled(fixedDelayString = "10000")
public void retry() {
    List<OutboxEntity> outboxEntities = outboxRepository.findAllBefore(Instant.now().minusSeconds(60));
    for (OutboxEntity outbox : outboxEntities) {
        deliveryMessageQueueService.send(outbox.message());
        outboxRepository.delete(outbox.id());
    }
}

}
复制代码
在这里你可以看到,我们每 10 秒运行一个任务,并发送之前没有发送过的消息。如果消息成功发送到消息队列,但发件箱实体没有从数据库中删除(例如因为数据库问题),那么下次该后台任务将尝试再次将此消息发送到消息队列。但这也意味着我们消息的消费者必须做好幂等处理,因为可能会多次接收相同的消息。
发件箱模式
通过上面的例子,我们可以抽象出“发件箱模式”。

在数据库里面额外增加一个outbox表用于存储需要发送的event
把直接发送event的步骤换成先把event存储到数据库outbox表
程序启动一个 job 不断去抓取 outbox 表里面的记录,通过推送线程完成不同业务的推送
最后删除发送成功的记录
提醒消息消费端要做好幂等处理

总结
发件箱模式虽然听上去可能很简单,但是在平时开发中可能会忽略掉。如果还不能理解,我们可以将它类比到生活的场景,寄信人只需要写好信件,放入收件箱,之后就不用管了。送信的人会来收件箱取走信件,根据信件里需要送到的地址,将信件送至目的地。这样做的好处就是,寄信人写好信之后,就不需要等待收信人有空的时候才能寄信,只需要往发件箱里丢就好了。

相关文章
|
10天前
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2
|
10天前
|
负载均衡 Java 应用服务中间件
微服务分布式系统架构之zookeeper与dubbor-1
微服务分布式系统架构之zookeeper与dubbor-1
|
13天前
|
JSON 监控 安全
探索微服务架构中的API网关模式
【9月更文挑战第22天】在微服务架构的海洋中,API网关如同一位智慧的守门人,不仅管理着服务的进出,还维护着整个系统的秩序。本文将带你一探究竟,看看这位守门人是如何工作的,以及它为何成为现代云原生应用不可或缺的一部分。从流量控制到安全防护,再到服务聚合,我们将一起解锁API网关的秘密。
|
23天前
|
运维 Cloud Native Devops
云原生架构的崛起与实践云原生架构是一种通过容器化、微服务和DevOps等技术手段,帮助应用系统实现敏捷部署、弹性扩展和高效运维的技术理念。本文将探讨云原生的概念、核心技术以及其在企业中的应用实践,揭示云原生如何成为现代软件开发和运营的主流方式。##
云原生架构是现代IT领域的一场革命,它依托于容器化、微服务和DevOps等核心技术,旨在解决传统架构在应对复杂业务需求时的不足。通过采用云原生方法,企业可以实现敏捷部署、弹性扩展和高效运维,从而大幅提升开发效率和系统可靠性。本文详细阐述了云原生的核心概念、主要技术和实际应用案例,并探讨了企业在实施云原生过程中的挑战与解决方案。无论是正在转型的传统企业,还是寻求创新的互联网企业,云原生都提供了一条实现高效能、高灵活性和高可靠性的技术路径。 ##
29 3
|
27天前
|
监控 负载均衡 应用服务中间件
探索微服务架构下的API网关设计与实践
在数字化浪潮中,微服务架构以其灵活性和可扩展性成为企业IT架构的宠儿。本文将深入浅出地介绍微服务架构下API网关的关键作用,探讨其设计原则与实践要点,旨在帮助读者更好地理解和应用API网关,优化微服务间的通信效率和安全性,实现服务的高可用性和伸缩性。
38 3
|
1月前
|
存储 Java Maven
从零到微服务专家:用Micronaut框架轻松构建未来架构
【9月更文挑战第5天】在现代软件开发中,微服务架构因提升应用的可伸缩性和灵活性而广受欢迎。Micronaut 是一个轻量级的 Java 框架,适合构建微服务。本文介绍如何从零开始使用 Micronaut 搭建微服务架构,包括设置开发环境、创建 Maven 项目并添加 Micronaut 依赖,编写主类启动应用,以及添加控制器处理 HTTP 请求。通过示例代码展示如何实现简单的 “Hello, World!” 功能,并介绍如何通过添加更多依赖来扩展应用功能,如数据访问、验证和安全性等。Micronaut 的强大和灵活性使你能够快速构建复杂的微服务系统。
61 5
|
9天前
|
Kubernetes Go Docker
掌握微服务架构:从Go到容器化的旅程
摘要,通常简短概述文章内容,要求精炼。在本文中,我们将打破常规,采用一种故事化叙述的摘要,旨在激发读者的好奇心和探究欲: “从宁静的海滨小城出发,我们踏上了一场技术探险之旅,探索微服务架构的奥秘。我们将学习如何用Go编写微服务,以及如何通过Docker和Kubernetes将它们打包进小巧的容器中。在这场旅程中,我们将遇到挑战、收获知识,最终实现应用的快速部署与可扩展性。”
|
11天前
|
Cloud Native Java 对象存储
揭秘微服务架构之争:Spring Cloud与Netflix OSS巅峰对决,谁将称霸弹性云原生时代?
近年来,微服务架构成为企业应用的主流设计模式。本文对比了两大热门框架Spring Cloud和Netflix OSS,探讨其在构建弹性微服务方面的表现。Spring Cloud依托Spring Boot,提供全面的微服务解决方案,包括服务注册、配置管理和负载均衡等。Netflix OSS则由一系列可独立或组合使用的组件构成,如Eureka、Hystrix等。两者相比,Spring Cloud更易集成且功能完善,而Netflix OSS则需自行整合组件,但灵活性更高。实际上,两者也可结合使用以发挥各自优势。通过对两者的对比分析,希望为企业在微服务架构选型上提供参考。
30 0
|
19天前
|
缓存 负载均衡 数据管理
深入探索微服务架构的核心要素与实践策略在当今软件开发领域,微服务架构以其独特的优势和灵活性,已成为众多企业和开发者的首选。本文将深入探讨微服务架构的核心要素,包括服务拆分、通信机制、数据管理等,并结合实际案例分析其在不同场景下的应用策略,旨在为读者提供一套全面、深入的微服务架构实践指南。**
**微服务架构作为软件开发领域的热门话题,正引领着一场技术革新。本文从微服务架构的核心要素出发,详细阐述了服务拆分的原则与方法、通信机制的选择与优化、数据管理的策略与挑战等内容。同时,结合具体案例,分析了微服务架构在不同场景下的应用策略,为读者提供了实用的指导和建议。
|
2月前
|
C# 微服务 Windows
模块化革命:揭秘WPF与微服务架构的完美融合——从单一职责原则到事件聚合器模式,构建高度解耦与可扩展的应用程序
【8月更文挑战第31天】本文探讨了如何在Windows Presentation Foundation(WPF)应用中借鉴微服务架构思想,实现模块化设计。通过将WPF应用分解为独立的功能模块,并利用事件聚合器实现模块间解耦通信,可以有效提升开发效率和系统可维护性。文中还提供了具体示例代码,展示了如何使用事件聚合器进行模块间通信,以及如何利用依赖注入进一步提高模块解耦程度。此方法不仅有助于简化复杂度,还能使应用更加灵活易扩展。
62 0
下一篇
无影云桌面