一、背景介绍
在上篇文章中,我们对 link-1 的架构设计、部署方式以及使用操作做了一个简单的介绍,相信大家对它已经有了初步的了解。
我们知道,在现有的 Spring Cloud 体系中,有两种技术方式可以实现服务的远程调用。
- 方式一:通过 Http 工具向目标服务接口发起远程调用,比如
OpenFeign
、Http Client
等工具。 - 方式二:通过 Dubbo 工具向目标服务接口发起远程调用,由于 Dubbo 采用 TCP 协议进行通信,相对 HTTP 方式来说,通信效率会更高一些,应用也更广泛
由于国内很多的项目采用 Dubbo 来实现服务的远程调用,下面我们以此为例,详细的介绍一下如何将 Dubbo 服务接入 Seata 来实现分布式事务操作。
二、方案实践
我们以之前的工程为例,对其进行适度改造,改造后服务之间的交互流程可以用如下图来简要概括。
具体的实施过程如下。
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;
}
}
}
其中service
和mapper
层代码和之前的库存服务工程一样,在此就不再重复粘贴了。
最后,创建一个服务启动类并添加@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 服务。
2.3、创建订单服务
订单服务的创建过程与上文类似。
创建一个 Spring Boot 工程,命名为seata-dubbo-order
,其pom.xml
所需要的依赖内容和服务启动类,与上文完全一致,在此就不重复粘贴了。
其中的web
、service
和mapper
层代码和之前的订单服务工程也完全一致,在此就不再重复粘贴了。
下面,我们重点对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
也成功注册到服务中心,界面如下。
2.4、服务测试
最后,我们还是一起来验证一下如下两种情况,看看是否能如期实现。
- 1.分布式事务正常提交
- 2.分布式事务异常回滚
2.4.1、分布式事务正常提交
首先,重新初始化数据库,数据库中原始数据情况如下。
seata-stock
库中的库存数据
seata-order
库中的订单数据
接着,在浏览器中访问http://127.0.0.1:9001/order/create?userId=张三&productCode=wahaha&orderCount=1
,它会执行如下两个动作:
- 第一个:调用库存服务,将产品产品编码为
wahaha
的库存减 1; - 第二个:如果库存扣减成功,插入一条产品编码为
wahaha
数量为 1 的订单信息;
发起接口请求后,再次回看数据库,看看目标数据表中的数据情况。
seata-stock
库中的库存数据
seata-order
库中的订单数据
从数据结果来看,与预期一致。
我们还可以通过查看服务的日志信息,来观察分支事务的操作情况。
其中Branch commit result
信息代表分支事务的二阶段操作。
2.4.2、分布式事务异常回滚
测试完正常流程之后,下面我们再来验证一下异常流程。
修改OrderService
类中create()
方法代码,在创建订单完成之后,试图抛出异常,测试一下扣减的库存数据是否能正常回滚。
首先,我们还是对数据库中原始数据进行截个图。
seata-stock
库中的库存数据
seata-order
库中的订单数据
然后,再次在浏览器中访问http://127.0.0.1:9001/order/create?userId=张三&productCode=wahaha&orderCount=1
。
预期的结果是:两个库的数据应该都不会发生变化!
再次回看数据库,观察目标数据表中的数据情况。
seata-stock
库中的库存数据
seata-order
库中的订单数据
数据在5秒之内是执行成功的,为了便于观察数据变化,我们在上文抛异常的位置停顿了 5 秒。
过 5 秒后,再次回看数据库表中的数据情况,结果如下。
seata-stock
库中的库存数据
seata-order
库中的订单数据
从数据最终结果来看,与预期是一致的。
在浏览器中访问http://127.0.0.1:7091
,登陆 Seata TC Server 服务监控台,还可以看到全局事务的注册信息和状态。
三、Seata 服务地址配置化
随着 Seata 的集群部署数量的增加,微服务中的Seata 服务地址配置可能会越来越臃肿,此时我们可能希望借助服务注册中心来加载 Seata TC Server 的地址,这个时候如何实现呢?
正如之前我们所介绍的,Seata TC Server 对主流的注册中心也提供了集成,Seata 客户端可以通过注册中心获取 Seata TC Server 所在的服务实例。
引入注册中心之后,Seata 的交互流程可以用如下图来概括。
下面我们以服务注册中心 Nacos 为例,简单的介绍一下它的配置方式。
3.1、Seata 服务端配置方式
打开 Seata 安装包中conf/application.example.yml
文件,找到store.registry
相关配置属性。
将其复制出来,然后拷贝到conf/application.yml
文件中。
最后,重启 Seata TC Server 服务即可。
访问 nacos 的服务控制台,如果看到 Seata 服务,说明服务注册成功了。
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、错误排查
如果测试中遇到类似如下异常信息。
这种情况通常是 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-alibaba
的2.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/