一文带你了解微服务架构中的"发件箱模式"

目录
  • 前言
  • 下订单的例子
  • 发件箱模式
  • 总结

前言

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

下订单的例子

假设我们有一个 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 表里面的记录,通过推送线程完成不同业务的推送
  • 最后删除发送成功的记录
  • 提醒消息消费端要做好幂等处理

总结

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

以上就是一文带你了解微服务架构中的"发件箱模式"的详细内容,更多关于微服务架构发件箱模式的资料请关注我们其它相关文章!

(0)

相关推荐

  • go-micro微服务JWT跨域认证问题

    目录 一 JWT介绍 二 JWT优缺点 三 JWT使用 1. 导包和数据定义 2.生成JWT 3.解析JWT 4.完整代码 四 最后 一 JWT介绍 JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的.URL安全的表述性声明规范,经常用在跨域身份验证. JWT 以 JSON 对象的形式安全传递信息.因为存在数字签名,因此所传递的信息是安全的. 一个JWT Token就像这样: eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.ey

  • SpringCloud微服务中跨域配置的方法详解

    跨域,指的是浏览器不能执行其他网站的脚本.它是由浏览器的同源策略造成的,是浏览器对 javascript 施加的安全限制. 同源策略,指的是协议,域名,端口都要相同,其中有一个不同都会产生跨域. 跨域相关含义: Access-Control-Allow-Origin:服务器允许请求的源: Access-Control-Allow-Headers: 服务器允许使用的头: Access-Control-Allow-Methods: 真实请求允许的方法: Access-Control-Allow-Cr

  • go micro微服务proto开发安装及使用规则

    目录 一 Protobuf介绍 二 安装Protobuf 三 Protobuf语法 1.1 基本规范 1.2 字段规则 1.3 service如何定义 1.4 Message如何定义 四 proto代码编写 五 生成.go文件 六 最后 一 Protobuf介绍 Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件. 他们用于 RP

  • 简单介绍一下什么是microservice微服务

    目录 一.单体软件 二.面向服务架构 三.微服务 References 微服务(microservice)是一种软件架构,正得到越来越多的关注. 但是,它到底是什么意思?什么样的架构可以叫做微服务? 网上的文章虽然很多,但是都太复杂,初学者不容易看懂.我认为,这个概念其实非常简单,可以很通俗地说明白. 一.单体软件 要理解微服务,首先需要理解软件架构的演变. 早期的软件,所有功能都写在一起,这称为单体架构(monolithic software). 整个软件就是单一的整体,彷佛一体化的机器. 可

  • go-micro微服务domain层开发示例详解

    目录 一 domain层介绍说明 二 model层开发 三 repository层开发 四 service层开发 最后 一 domain层介绍说明 domain层专注于数据库数据领域开发,我们把数据库相关操作全部写在domain层. model层:数据表字段定义与开发 repository层:数据库数据CURD操作 service层:数据库相关业务操作 对于复杂的业务操作,这样可以使业务逻辑更加清晰,有利于后期开发与维护. 二 model层开发 在model目录下新建 user.go 文件 pa

  • 微服务 Spring Boot 整合 Redis BitMap 实现 签到与统计功能

    目录 引言 一.Redis BitMap 基本用法 BitMap 基本语法.指令 使用 BitMap 完成功能实现 二.SpringBoot 整合 Redis 实现签到 功能 ️需求介绍 核心源码 三.SpringBoot 整合Redis 实现 签到统计功能 四.关于使用bitmap来解决缓存穿透的方案 小结 引言 在各个项目中,我们都可能需要用到签到和 统计功能. 签到后会给用户一些礼品以此来吸引用户持续在该平台进行活跃. 签到功能,我们可以通过Redis中的 BitMap功能来实现 一.Re

  • go micro微服务框架项目搭建方法

    目录 一 微服务项目介绍 二 go-micro安装 1.拉取micro镜像 2.生成项目目录 三 项目搭建 使用DDD模式开发项目: 四 最后 一 微服务项目介绍 账户功能是每一个系统都绕不开的一部分,所以本次搭建的微服务项目就是账户微服务项目,其中向外暴露的功能有: 登录 注册 查询用户信息 修改信息 发送注册邮件 发送重置密码邮件 重置密码 获取权限 修改权限 退出账号 删除账号 禁用账号 启用账号 提供的功能总共有13个,基本上包含了账户相关的所有功能! 在本次微服务项目中使用到的技术包括

  • Java Feign微服务接口调用方法详细讲解

    目录 Feign说明 引入依赖启动类开启客户端 Feign接口开发 编写容错类 在业务层调用Feign客户端接口 Feign的常用属性如下 Feign说明 Feign是一种声明式.模板化的HTTP客户端.在spring cloud中使用Feign,可以做到类似于普通的接口的请求调用,可以发现对应的服务的接口,进而直接调用对应服务中的接口. 引入依赖启动类开启客户端 首先需要引入依赖 <dependency> <groupId>org.springframework.cloud<

  • 微服务Spring Boot 整合 Redis 实现UV 数据统计的详细过程

    目录 引言 一.HyperLoglog基础用法 HyperLoglog 基本语法.命令 HyperLoglog 命令完成功能实现 二.UV统计 测试百万数据的统计 ️什么是UV统计 使用SpringBoot单元测试进行测试百万数据统计 小结 引言 本文参考黑马 点评项目 在各个项目中,我们都可能需要用到UV数据统计功能,这样可以使我们更加方便.快捷的查看网站的活跃度! 一.HyperLoglog基础用法 HyperLoglog 基本语法.命令 HyperLogLog PFADD :将指定元素添加

  • Java微服务Filter过滤器集成Sentinel实现网关限流过程详解

    目录 Gateway-过滤器Filter 局部路由过滤器 使用局部过滤器 全局过滤器 使用全局过滤器 集成Sentinel实现网关限流 网关限流 API分组限流 Gateway-过滤器Filter 过滤器就是在请求的传递过程中,对请求和响应做一些手脚. 在Gateway中, Filter的生命周期只有两个:“pre”和“post”". .PRE:这种过滤器在请求被路由之前调用.我们可利用这种过滤器实现身份验证.在集群中选择请求的微服务.记录调试信息等. .POST:这种过滤器在路由到微服务以后执

  • 微服务链路追踪Spring Cloud Sleuth整合Zipkin解析

    目录 前言 何为调用链路 Zipkin + Sleuth Zipkin Spring Cloud Sleuth Zipkin启动 引入jar 服务调用测试 总结 前言 如果在开发过程中,你还在靠查看服务器日志来寻找服务与服务之间的报错信息,那么这篇一定要来看下,通常在我们开发环境自测的时候,我们会将代码发布到开发环境,然后无论是通过postMan请求,还是通过页面请求,遇到报错的信息,我们都会去服务器上去看时实的日志,来寻找报错信息: 如果涉及到多个服务调用,这个时候会登陆多个服务器去查看服务的

  • 基于jib-maven-plugin插件快速构建微服务docker镜像的方法

    目录 一.说明 二.插件使用 三.总结 四.样例工程 一.说明 本文介绍基于 Maven 插件 jib-maven-plugin 实现快速构建 Spring Boot 程序镜像,并推送到远程仓库中,且 「无需安装」 Docker 环境 . Jib 是 Google 开发的一个无需 Docker 守护进程,也无需深入掌握 Docker 最佳实践的情况下,为 Java 应用程序构建 Docker 和 OCI 镜像,以 Maven 和 Gradle 插件形式提供. 二.插件使用 下图为插件的配置样例,

  • Mybatis与微服务注册的详细过程

    目录 一.SpringBoot整合MybatisPlus 创建自动生成代码子模块 创建商品服务子模块 二.SpringBoot整合Freeamarker 三.SpringBoot整合微服务&gateway&nginx 整合微服务之商品服务zmall-product 创建并配置网关gateway服务 安装配置SwitchHosts 安装配置Windows版nginx 请求链路测试 一.SpringBoot整合MybatisPlus 创建自动生成代码子模块 1.基于maven方式创建子模块zm

  • 详解go-micro微服务consul配置及注册中心

    目录 一 Consul介绍 1. 注册中心Consul基本介绍 2.注册中心Consul关键功能 3.注册中心Consul两个重要协议 二 Consul安装 1.使用docker拉取镜像 三 Config配置 四 Consul代码编写 1.设置consul配置中心 2.获取consul配置中心的数据 3.consul可视化界面数据编写 4. main.go代码编写 五 最后 一 Consul介绍 Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置. Consu

随机推荐