专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Seata 与 Dubbo 服务整合实践:实现分布式事务管理

一、背景介绍

在上篇文章中,我们对 link-1 的架构设计、部署方式以及使用操作做了一个简单的介绍,相信大家对它已经有了初步的了解。

我们知道,在现有的 Spring Cloud 体系中,有两种技术方式可以实现服务的远程调用。

  • 方式一:通过 Http 工具向目标服务接口发起远程调用,比如 OpenFeignHttp Client 等工具。
  • 方式二:通过 Dubbo 工具向目标服务接口发起远程调用,由于 Dubbo 采用 TCP 协议进行通信,相对 HTTP 方式来说,通信效率会更高一些,应用也更广泛

由于国内很多的项目采用 Dubbo 来实现服务的远程调用,下面我们以此为例,详细的介绍一下如何将 Dubbo 服务接入 Seata 来实现分布式事务操作。

二、方案实践

我们以之前的工程为例,对其进行适度改造,改造后服务之间的交互流程可以用如下图来简要概括。

link-2

具体的实施过程如下。

2.1、创建服务接口

首先,创建一个简单的 Maven 工程,命名为seata-dubbo-api将需要对外暴露的服务接口写入到这里。示例接口如下:

public interface StockApi {
    /**
     * 库存扣减
     * @param productCode
     * @param count
     * @return
     */
    boolean deduct(String productCode, int count);
}

服务接口创建完成之后,接下来我们再来创建库存服务和订单服务。

2.2、创建库存服务

然后,建一个 Spring Boot 工程,命名为seata-dubbo-stock,并在pom.xml中引入相关的依赖内容,示例如下:

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <spring-boot.version>2.2.5.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
    <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencies>
    <!-- SpringBoot web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mysql 驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    <!-- Nacos 服务发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- Dubbo -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-dubbo</artifactId>
    </dependency>
    <!-- seata 分布式事务组件 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <!-- 关联构建的api包 -->
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>seata-dubbo-api</artifactId>
        <version>3.0-SNAPSHOT</version>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <!-- 引入 springBoot 版本号 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- 引入 spring cloud 版本号 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- 引入 spring cloud alibaba 适配的版本号 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

接着,创建一个application.properties文件并配置相关配置项,示例如下:

spring.application.name=seata-dubbo-stock
server.port=9002
# 添加数据源配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata-stock
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置mybatis全局配置文件扫描
mybatis.config-location=classpath:mybatis/mybatis-config.xml
# 配置mybatis的xml配置文件扫描目录
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# 设置Nacos的服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 指定 Dubbo 服务实现类的扫描基准包
dubbo.scan.base-packages=com.example.cloud.nacos.dubbo.seata
# 指定 Dubbo 服务暴露的协议
dubbo.protocol.name=dubbo
# 指定 Dubbo 服务协议端口,-1 表示自增端口,从 20880 开始
dubbo.protocol.port=-1
# 指定 Dubbo 服务注册中心
dubbo.registry.address=nacos://${spring.cloud.nacos.discovery.server-addr}
# 添加Seata 配置项
# Seata 应用编号,默认为spring.application.name
seata.application-id=seata-dubbo-stock
# Seata 事务组编号,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服务配置项,配置对应的虚拟组和分组的映射,其中127.0.0.1:8091为 seata 服务端的监听端口
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091

再然后,创建一个 Dubbo 服务并实现上文创建的服务接口,示例如下:

@com.alibaba.dubbo.config.annotation.Service
publicclass StockApiImpl implements StockApi {
    @Autowired
    private StockService stockService;
    @Override
    public boolean deduct(String productCode, int count) {
        try {
            stockService.deduct(productCode, count);
            // 正常扣除库存,返回 true
            returntrue;
        } catch (Exception e) {
            // 失败扣除库存,返回 false
            returnfalse;
        }
    }
}

其中servicemapper层代码和之前的库存服务工程一样,在此就不再重复粘贴了。

最后,创建一个服务启动类并添加@EnableDiscoveryClient注解,以便将服务注册到 Nacos。

@EnableDiscoveryClient
@MapperScan("com.example.cloud.nacos.dubbo.seata")
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

将服务启动起来,在浏览器中访问http://127.0.0.1:8848/nacos,如果不出意外的话,在 Nacos 服务列表可以看到注册的 dubbo 服务。

link-3

2.3、创建订单服务

订单服务的创建过程与上文类似。

创建一个 Spring Boot 工程,命名为seata-dubbo-order,其pom.xml所需要的依赖内容和服务启动类,与上文完全一致,在此就不重复粘贴了。

其中的webservicemapper层代码和之前的订单服务工程也完全一致,在此就不再重复粘贴了。

下面,我们重点对OrderService服务进行改造,将通过 HTTP 工具调用远程服务接口的逻辑移除,改成用 Dubbo 方式实现服务的远程调用,示例如下:

@Component
publicclass OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @com.alibaba.dubbo.config.annotation.Reference
    private StockApi stockApi;
    @GlobalTransactional
    public void create(String userId, String productCode, int orderCount) throws Exception {
        // 通过dubbo服务,实现远程扣减库存
        stockApi.deduct(productCode, orderCount);
        Order order = new Order();
        order.setUserId(userId);
        order.setProductCode(productCode);
        order.setCount(orderCount);
        order.setMoney(orderCount * 100);
        // 创建订单
        orderMapper.insert(order);
    }
}

与上文类似,在application.properties配置文件中添加相关的配置项,示例如下:

spring.application.name=seata-dubbo-order
server.port=9001
# 添加数据源配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/seata-stock
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置mybatis全局配置文件扫描
mybatis.config-location=classpath:mybatis/mybatis-config.xml
# 配置mybatis的xml配置文件扫描目录
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# 设置Nacos的服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 指定 Dubbo 服务实现类的扫描基准包
dubbo.scan.base-packages=com.example.cloud.nacos.dubbo.seata
# 指定 Dubbo 服务暴露的协议
dubbo.protocol.name=dubbo
# 指定 Dubbo 服务协议端口,-1 表示自增端口,从 20880 开始
dubbo.protocol.port=-1
# 指定 Dubbo 服务注册中心
dubbo.registry.address=nacos://${spring.cloud.nacos.discovery.server-addr}
# 关闭dubbo客户端服务有效性检查
dubbo.consumer.check=false
# 添加Seata 配置项
# Seata 应用编号,默认为spring.application.name
seata.application-id=seata-dubbo-order
# Seata 事务组编号,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服务配置项,配置对应的虚拟组和分组的映射,其中127.0.0.1:8091为 seata 服务端的监听端口
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091

将服务启动,再次访问http://127.0.0.1:8848/nacos的服务列表,可以看到seata-dubbo-order也成功注册到服务中心,界面如下。

link-4

2.4、服务测试

最后,我们还是一起来验证一下如下两种情况,看看是否能如期实现。

  • 1.分布式事务正常提交
  • 2.分布式事务异常回滚

2.4.1、分布式事务正常提交

首先,重新初始化数据库,数据库中原始数据情况如下。

  • seata-stock 库中的库存数据

link-5

  • seata-order 库中的订单数据

link-6

接着,在浏览器中访问http://127.0.0.1:9001/order/create?userId=张三&productCode=wahaha&orderCount=1,它会执行如下两个动作:

  • 第一个:调用库存服务,将产品产品编码为 wahaha 的库存减 1;
  • 第二个:如果库存扣减成功,插入一条产品编码为 wahaha 数量为 1 的订单信息;

发起接口请求后,再次回看数据库,看看目标数据表中的数据情况。

  • seata-stock 库中的库存数据

link-7

  • seata-order 库中的订单数据

link-8

从数据结果来看,与预期一致。

我们还可以通过查看服务的日志信息,来观察分支事务的操作情况。

link-9link-10

其中Branch commit result信息代表分支事务的二阶段操作。

2.4.2、分布式事务异常回滚

测试完正常流程之后,下面我们再来验证一下异常流程。

修改OrderService类中create()方法代码,在创建订单完成之后,试图抛出异常,测试一下扣减的库存数据是否能正常回滚。

link-11

首先,我们还是对数据库中原始数据进行截个图。

  • seata-stock 库中的库存数据

link-12

  • seata-order 库中的订单数据

link-13

然后,再次在浏览器中访问http://127.0.0.1:9001/order/create?userId=张三&productCode=wahaha&orderCount=1

预期的结果是:两个库的数据应该都不会发生变化

再次回看数据库,观察目标数据表中的数据情况。

  • seata-stock 库中的库存数据

link-14

  • seata-order 库中的订单数据

link-15

数据在5秒之内是执行成功的,为了便于观察数据变化,我们在上文抛异常的位置停顿了 5 秒。

过 5 秒后,再次回看数据库表中的数据情况,结果如下。

  • seata-stock 库中的库存数据

link-16

  • seata-order 库中的订单数据

link-17

从数据最终结果来看,与预期是一致的。

在浏览器中访问http://127.0.0.1:7091,登陆 Seata TC Server 服务监控台,还可以看到全局事务的注册信息和状态。

link-18

三、Seata 服务地址配置化

随着 Seata 的集群部署数量的增加,微服务中的Seata 服务地址配置可能会越来越臃肿,此时我们可能希望借助服务注册中心来加载 Seata TC Server 的地址,这个时候如何实现呢?

正如之前我们所介绍的,Seata TC Server 对主流的注册中心也提供了集成,Seata 客户端可以通过注册中心获取 Seata TC Server 所在的服务实例。

引入注册中心之后,Seata 的交互流程可以用如下图来概括。

link-19

下面我们以服务注册中心 Nacos 为例,简单的介绍一下它的配置方式。

3.1、Seata 服务端配置方式

打开 Seata 安装包中conf/application.example.yml文件,找到store.registry相关配置属性。

link-20

将其复制出来,然后拷贝到conf/application.yml文件中。

link-21

最后,重启 Seata TC Server 服务即可。

访问 nacos 的服务控制台,如果看到 Seata 服务,说明服务注册成功了。

link-22

3.2、Seata 客户端端配置方式

seata-dubbo-order服务为例,修改application.properties配置文件中seata相关的配置项,示例如下:

# Seata 应用编号,默认为spring.application.name
seata.application-id=seata-dubbo-stock
# Seata 事务组编号,用于 TC 集群名
seata.tx-service-group=my_test_tx_group
# Seata 服务配置项,配置对应的虚拟组和分组的映射,此处必须填写default
seata.service.vgroup-mapping.my_test_tx_group=default
# 设置 seata 注册中心类型为nacos,默认为 file
seata.registry.type=nacos
# 设置 seata 服务端中配置 nacos 相关信息
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=127.0.0.1:8848
seata.registry.nacos.group=SEATA_GROUP

此处的调整主要是增加 seata 的注册中心配置,客户端通过配置的注册中心来获取 Seata TC Server 服务实例地址。

最后将相关的服务进行重启,再次在浏览器中访问http://127.0.0.1:9001/order/create?userId=张三&productCode=wahaha&orderCount=1

不出意外的话,数据测试结果与上文一致。

3.3、错误排查

如果测试中遇到类似如下异常信息。

link-23

这种情况通常是 seata 客户端版本与服务端版本不兼容导致的,可以尝试升级 seata 客户端版本,以便与 seata 服务端进行适配。

以本文工程为例,Seata TC Server 服务端采用的1.5.2版本,而 Seata 客户端采用的是1.1.0版本,可见两者版本相差太大,当发起接口请求时就出现了上文的错误信息。

通过查阅版本号适配情况,Seata 客户端的1.3.0版本可以与 Seata 服务端进行兼容,因此可以直接升级spring-cloud-alibaba的版本号,示例如下:

<!--原来是 2.2.1.RELEASE版本,将其升级为 2.2.3.RELEASE-->
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>

由于spring-cloud-alibaba2.2.3.RELEASE版本中集成的seata客户端版本号为1.3.0,当重启服务再次发起接口请求时,一切恢复正常。

因此当代码和配置都没有问题时,服务无法启动或者运行错误,通常情况与版本号有很大的关系。可以检查一下工程中的版本号与官方要求的版本号是否出现不兼容现象。

四、小结

最后总结一下,本文主要围绕 dubbo 整合 seata 实现服务分布式事务操作做了一次知识内容的总结和整理,内容比较多,如果有描述不对的地方,欢迎大家留言指出。

如果当前的服务工程采用的是 openFeign 来实现服务远程调用,也可以通过集成spring-cloud-starter-alibaba-seata依赖包实现分布式事务操作,其实现原理也是在远程调用的请求头部中插入全局事务 ID,依次传递到下游服务中,从而保证全局事务的统一提交和回滚操作。

参考

1、https://seata.apache.org/zh-cn/docs/overview/what-is-seata/

未经允许不得转载:搜云库 » Seata 与 Dubbo 服务整合实践:实现分布式事务管理

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们