今天我们要学习的是spring cloud alibaba seata 之2pc实现分布式事务,有时候也叫spring cloud alibaba seata 之XA,基于数据库的XA协议来实现2PC又称为XA方案。
微服务基础项目
项目来源
本次实战是在我们写好一个 spring cloud 项目的基础上去处理的,项目来源:
https://gitee.com/cddofficial/distributed_transaction_project_code/tree/master/microservices-demo
如下图:
拿到项目代码后,项目重命名为:microservices-tranction-seata2pc-demo。用 idea 打开项目,如下图:
业务场景
我们现在有个账户系统,小明想通过该系统把支付宝账户里面的钱提现到他自己工商银行卡账户。那么这个事要分两步进行:
小明的支付宝账户减钱
小明的银行卡账户加钱
要么这两步同时成功,要么同时失败。
这个账户系统 spring cloud 微服务架构,实现支付宝账户出金的是一个微服务,实现银行卡入金的是一个微服务,现在要保证这两个微服务上的操作要么同时成功,要么同时失败,这里就要用到我们的分布式事务了
启动事务协调器
下载 seata 服务器,即事务协调器https://github.com/seata/seata/releases
下载 0.7.1 版本
https://github.com/seata/seata/releases/download/v0.7.1/seata-server-0.7.1.zip
下载后解压,启动
在 dos 窗口,执行命令:seata-server.bat -p 8888 -m file;如下图:
处理支付宝微服务
添加表
在支付宝微服务所连接的库里添加 undo_log 表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在支付宝微服务所连接的库里添加 账户表account
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL,
`account_no` varchar(6) DEFAULT NULL COMMENT '账号',
`account_name` varchar(12) DEFAULT NULL COMMENT '账户名称',
`balance` int(11) DEFAULT NULL COMMENT '账户余额,单位为分',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
添加依赖
父 pom 文件中添加下面依赖,与 springcloud 的依赖放在一起
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
当前项目 pom 文件中添加依赖
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
配置 seata
配置文件
1.把 seata-server-0.7.1.zip 解压后的\seata\conf 目录下的 file.conf、registry.conf 拷贝到项目的 resource/目录下,这里我们采用 mode=file(默认就是)
修改 file.conf,只修改 service 快中配置,如下图:
service {
#vgroup->rgroup #应用名称-fescar-service-group
vgroup_mapping.service-zhifubao-fescar-service-group = "default"
#only support single node; #seata server 的地址
default.grouplist = "127.0.0.1:8888"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
2.application.yml
修改数据库配置,要创建动态代理数据源。Seata 的 RM 通过 DataSourceProxy 才能在业务代码的事务提交时,通过这个切入点,与 TC 进行通信交互、记录 undo_log 等。
spring:
application:
name: service-zhifubao #应用程序名称
datasource:
ds0:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_zhifubao?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
配置类
添加配置类 DataSourceConfig,运行时根据 application.yml 中的 spring.datasource.ds0 的数据库配置创建数据源
@Configuration
public class DataSourceConfig {
private final ApplicationContext applicationContext;
public DataSourceConfig(ApplicationContext applicationContext){
this.applicationContext = applicationContext;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.ds0")
public DruidDataSource ds0(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean
public DataSource dataSource(DruidDataSource ds0){
DataSourceProxy pds0 = new DataSourceProxy(ds0);
return pds0;
}
}
启动时禁止初始化数据源,否则产生循环依赖
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
处理银行卡微服务
步骤和处理支付宝微服务的一样,配置 file.con 时,记得应用名称要变成银行卡微服务的
修改业务
小明要提现,首先请求到支付宝微服务扣减支付宝账户的余额,接着 rpc 调用银行卡微服务增加银行卡账户的余额。(默认更新每个服务对应的数据库 account_id 表 id 为 1 的记录)
在支付宝微服务的业务层进行修改,AccountServiceImpl#updateAccountById()方法
@Override
@Transactional // 本地事务
@GlobalTransactional // 分布式事务
public int updateAccountById(Integer amount) {
AccountInfo accountInfo = getById(1L);
accountInfo.setBalance(accountInfo.getBalance() - amount);
int flag = accountInfoMapper.updateByPrimaryKeySelective(accountInfo); // 支付宝出账
Assert.state(flag == 1, "支付宝账户更新失败");
String rs = bankCardFeign.income(amount); // 银行卡入账
Assert.state(!"fallback".equals(rs), "银行卡微服务调用失败");
return 1;
}
在银行卡微服务的业务层进行修改,AccountServiceImpl#updateAccountById()方法中
@Override
@Transactional // 本地事务
public int updateAccountById(Integer amount) {
AccountInfo accountInfo = getById(1L);
accountInfo.setBalance(accountInfo.getBalance() + amount);
int rs = accountInfoMapper.updateByPrimaryKeySelective(accountInfo);
Assert.state(rs == 1, "更新银行卡账户影响的记录行数不为 1");
return rs;
}
测试
启动服务发现服务,支付宝服务,银行卡服务,进行测试。
支付宝微服务成功,银行卡微服务成功
请求前各账户余额
发起提现请求
请求后各账户余额
支付宝微服务成功,银行卡微服务失败
请求前各账户余额
制造异常并发起请求
- 人为制造异常
要抛出异常,这时候要把银行卡微服务里面的 AccountServiceImpl#updateAccountById()方法修改如下:
@Override
@Transactional // 本地事务
public int updateAccountById(Integer amount) {
AccountInfo accountInfo = getById(1L);
accountInfo.setId(2L); // 人为制造异常:account_info 表中没有 id 为 2 的记录
accountInfo.setBalance(accountInfo.getBalance() + amount);
int rs = accountInfoMapper.updateByPrimaryKeySelective(accountInfo);
Assert.state(rs == 1, "更新银行卡账户影响的记录行数不为 1");
return rs;
}
重启银行卡微服务。
2. 发起请求
请求后各账户余额
修复人造异常
把银行卡微服务里面的 AccountServiceImpl#updateAccountById()方法修改如下:
@Override
@Transactional // 本地事务
public int updateAccountById(Integer amount) {
AccountInfo accountInfo = getById(1L);
accountInfo.setBalance(accountInfo.getBalance() + amount);
int rs = accountInfoMapper.updateByPrimaryKeySelective(accountInfo);
Assert.state(rs == 1, "更新银行卡账户影响的记录行数不为 1");
return rs;
}
重启银行卡微服务。
支付宝微服务失败,银行卡微服务成功
请求前各账户余额
人为制造异常并发起请求
1.要抛出异常,这时候要把支付宝微服务里面的 AccountServiceImpl#updateAccountById()方法修改如下(调整调用顺序,先调用银行卡微服务,后调用支付宝微服务):
@Override
@Transactional // 本地事务
@GlobalTransactional // 分布式事务
public int updateAccountById(Integer amount) {
AccountInfo accountInfo = getById(1L);
accountInfo.setBalance(accountInfo.getBalance() - amount);
String rs = bankCardFeign.income(amount); // 银行卡入账
Assert.state(!"fallback".equals(rs), "银行卡微服务调用失败");
accountInfo.setId(2L); // 人为制造异常:account_info 表中没有 id 为 2 的记录
int flag = accountInfoMapper.updateByPrimaryKeySelective(accountInfo); // 支付宝出账
Assert.state(flag == 1, "支付宝账户更新失败");
return 1;
}
重启支付宝微服务
- 发起请求
请求后各账户余额
修复人造异常
要把支付宝微服务里面的 AccountServiceImpl#updateAccountById()方法修改如下(注意把调用顺序调整回来,先调用支付宝微服务,后调用银行卡微服务):
@Override
@Transactional // 本地事务
@GlobalTransactional // 分布式事务
public int updateAccountById(Integer amount) {
AccountInfo accountInfo = getById(1L);
accountInfo.setBalance(accountInfo.getBalance() - amount);
int flag = accountInfoMapper.updateByPrimaryKeySelective(accountInfo); // 支付宝出账
Assert.state(flag == 1, "支付宝账户更新失败");
String rs = bankCardFeign.income(amount); // 银行卡入账
Assert.state(!"fallback".equals(rs), "银行卡微服务调用失败");
return 1;
}
重启支付宝微服务。
支付宝微服务失败,银行卡微服务超时
请求前各账户余额
打端点并发起请求
1.在银行卡微服务 AccountServiceImpl#updateAccountById()方法里面打上断点(前提是以 debug 模式启动)。如下:
2.发起请求
请求后各账户余额
我们的分布式解决方案seata-2pc的确实现了分布式事务。最后完成后的项目地址:https://gitee.com/cddofficial/distributed_transaction_project_code/tree/master/microservices-tranction-seata2pc-demo