什么是事物
事务就是针对数据库的一组操作,它可以由一条或多条SQL语句组成,同一个事务的操作具备同步的特点,事务中的语句要么都执行,要么都不执行。
举个栗子:
你去小卖铺买东西,一手交钱,一手交货就是一个事务的例子,交钱和交货必须全部成功,事务才算成功,任何一个活动失败,事务将撤销所有已成功的活动。
什么是本地事物
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。
解释:
- Business∶我们具体的业务代码
- Storage∶ 库存业务代码;扣库存
- Order∶订单业务代码;保存订单
- Account∶账号业务代码;减账户余额
数据库事务的四大特性ACID
- 原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。- 一致性(Consistency)
事务前后数据的完整性必须保持一致。- 隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。- 持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
总结
数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。
前言
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变。
注意:
分布式系统会把一个应用系统拆分为可独立部署的多个服务, 因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。
假如没有分布式事务
在一系列微服务系统当中,假如不存在分布式事务,会发生什么呢?让我们以互联网中常用的交易业务为例子:
解释:
上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务, 创建订单记录。
正常情况下,两个数据库各自更新成功,两边数据维持着一致性。
但是,在非正常情况下,有可能库存的扣减完成了,随后的订单记录却因为某些原因插入失败。这个时候,两边数据就失去了应有的一致性。
问题:
这种时候需要要保证数据的一致性,单数据源的一致性靠单机事物来保证,多数据源的一致性就要靠分布式事物保证。
什么是分布式事务
指一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。从本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
跨JVM进程
当我们将单体项目拆分为分布式、微服务项目之后,各个服务之间通过远程REST或者RPC调用来协同完成业务操作。
典型的场景:
商城系统中的订单微服务和库存微服务,用户在下单时会访问订单微服务,订单微服务在生成订单记录时,会调用库存微服务来扣减库存。各个微服务是部署在不同的JVM进程中的,此时,就会产生因跨JVM进程而导致的分布式事务问题。
跨数据库实例
单体系统访问多个数据库实例,也就是跨数据源访问时会产生分布式事务。
典型的场景:
例如,我们的系统中的订单数据库和交易数据库是放在不同的数据库实例中,当用户发起退款时,会同时操作用户的订单数据库和交易数据库,在交易数据库中执行退款操作,在订单数据库中将订单的状态变更为已退款。由于数据分布在不同的数据库实例,需要通过不同的数据库连接会话来操作数据库中的数据,此时,就产生了分布式事务。
多个服务一个数据库
多个微服务访问同一个数据库。
典型场景:
例如,订单微服务和库存微服务访问同一个数据库也会产生分布式事务,原因是:多个微服务访问同一个数据库,本质上也是通过不同的数据库会话来操作数据库,此时就会产生分布式事务。
两阶段提交又称2PC,2PC是一个非常经典的强一致、中心化的原子提交协议。
这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点
(coordinator)和 N个参与者节点
(partcipant)。
两个阶段:第一阶段:投票阶段 和第二阶段:提交/执行阶段。
2pc例子
A组织B、C和D三个人去爬山:如果所有人都同意去爬山,那么活动将举行;如果有一人不同意去爬山,那么活动将取消。
首先A将成为该活动的协调者,B、C和D将成为该活动的参与者。
具体流程:
阶段1:
①A发邮件给B、C和D,提出下周三去爬山,问是否同意。 那么此时A需要等待B、C和D的邮件。
②B、C和D分别查看自己的日程安排表。B、C发现自己在当日没有活动安排,则发邮件告诉A它们同意下周三去爬山。由于某种原因, D白天没有查看邮件。那么此时A、B和C均需要等待。到晚上的时候,D发现了A的邮件,然后查看日程安排, 发现周三当天已经有别的安排,那么D回复A说活动取消吧。
阶段2:
①此时A收到了所有活动参与者的邮件,并且A发现D下周三不能去爬山。那么A将发邮件通知B、C和D,下周三爬山活动取消。
②此时B、C回复A“太可惜了”,D回复A“不好意思”。至此该事务终止。
2PC阶段处理流程
举例订单服务A,需要调用支付服务B去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。
第一阶段:投票阶段
第一阶段分三步:
- 事物询问:协调者向所有的参与者发送事务预处理请求,称之为Prepare,并开始等待各参与者的响应。
- 执行本地事物:各个参与者节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向协调者报告说:“我这边可以处理了/我这边不能处理”。.
- 各个参与者向协调者反馈事物询问的响应:如果所有参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果有参与者执行事务失败,那么就反馈给协调者No响应, 表示事务不可以执行。第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。
第二阶段:提交/执行阶段(成功流程)
成功条件
:所有参与者都返回Yes。
异常流程第二阶段也分为两步
发送回滚请求
协调者向所有参与者节点发出 RoollBack 请求
事务回滚
参与者接收到RoollBack请求后,会回滚本地事务。
2PC缺点
性能问题
无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
单节点故障
由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
什么是DTP
2PC的传统方案是在数据库层面实现的,如Oracle、MySQL都支持 2PC协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group定义分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。
DTP模型定义角色
注意:
DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案。
执行流程:
- 应用程序持有用户库和积分库两个数据源。
- 应用程序通过TM通知用户库RM新增用户,同时通知积分库RM为该用户新增积分,RM此时并未提交事物,此时用户和积分资源锁定。
- TM收到回复,只要有一方失败则分别向其他RM发起回滚事物,回滚完毕,资源释放。
- TM收到执行回复,全部成功,此时向所有RM发起提交事物,提交完毕,资源锁释放。
Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata整体框架
全局事务与分支事务的关系图
与传统2PC的模型类似,Seata定义了三个组件来协议分布式事务的处理过程
具体流程:
- Transaction Coordinator(TC):事务协调者,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各个分支事务的提交或回滚。
- Transaction Manager(TM):事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。
- Resource Manager(RM):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器 TC的指令,驱动分支(本地)事务的提交和回滚。
还拿新用户注册送积分举例Seata的分布式事务过程
执行流程 :
- 用户服务的TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID。
- 用户服务的RM向TC注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入 XID对应全局事务的管辖。
- 用户服务执行分支事务,向用户表插入一条记录。
- 逻辑执行到远程调用积分服务时(XID在微服务调用链路的上下文中传播)。积分服务的RM 向TC注册分支事务,该分支事务执行增加积分的逻辑,并将其纳入XID对应全局事务的管辖。
- 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
- 用户服务分支事务执行完毕。
- TM向TC发起针对XID的全局提交或回滚决议。
- TC调度XID下管辖的全部分支事务完成提交或回滚请求。
Seata实现2PC与传统2PC的差别
业务说明
本实例通过Seata中间件实现分布式事务,模拟两个账户的转账交易过程。两个账户在两个不同的银行(张三在bank1、李四在 bank2),bank1和bank2是两个微服务。交易过程中,张三给李四转账制定金额。上述交易步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。
工程环境
创建数据库
Docker安装Mysql
查看镜像
docker search mysql:5.7
下载镜像
docker pull mysql:5.7
启动镜像
docker run --name mysql -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
参数:
-e 配置环境变量 MYSQL_ROOT_PASSWORD 设置容器内mysql root 密码
bank1库,包含张三账户
CREATE DATABASE /*!32312 IF NOT
EXISTS*/
`bank1` /*!40100 DEFAULT CHARACTER
SET utf8 */;
USE `bank1`;
/*Table structure for table `account_info`
*/
DROP TABLE
IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` BIGINT (20) NOT NULL AUTO_INCREMENT,
`account_name` VARCHAR (100) COLLATE utf8_bin DEFAULT NULL COMMENT '户主姓名',
`account_no` VARCHAR (100) COLLATE utf8_bin DEFAULT NULL COMMENT '银行卡号',
`account_password` VARCHAR (100) COLLATE utf8_bin DEFAULT NULL COMMENT '帐户密码',
`account_balance` DOUBLE DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8 COLLATE = utf8_bin ROW_FORMAT = DYNAMIC;
/*Data for the table `account_info` */
INSERT INTO `account_info` (
`id`,
`account_name`,
`account_no`,
`account_password`,
`account_balance`
)
VALUES
(1, '张三', '1', NULL, 1000);
bank2库,包含李四账户
CREATE DATABASE /*!32312 IF NOT
EXISTS*/
`bank2` /*!40100 DEFAULT CHARACTER
SET utf8 */;
USE `bank2`;
/*Table structure for table `account_info`
*/
DROP TABLE
IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` BIGINT (20) NOT NULL AUTO_INCREMENT,
`account_name` VARCHAR (100) COLLATE utf8_bin DEFAULT NULL COMMENT '户主姓名',
`account_no` VARCHAR (100) COLLATE utf8_bin DEFAULT NULL COMMENT '银行卡号',
`account_password` VARCHAR (100) COLLATE utf8_bin DEFAULT NULL COMMENT '帐户密码',
`account_balance` DOUBLE DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 4 DEFAULT CHARSET = utf8 COLLATE = utf8_bin ROW_FORMAT = DYNAMIC;
/*Data for the table `account_info` */
INSERT INTO `account_info` (
`id`,
`account_name`,
`account_no`,
`account_password`,
`account_balance`
)
VALUES
(2, '李四', '2', NULL, 0);
下载seata服务器
下载地址 :https://github.com/seata/seata/releases
解压并启动
tar -zxvf seata-server-1.4.2.tar.gz -C /usr/local/
#后台运行
nohup sh seata-server.sh -p 9999 -h 114.117.183.67 -m file &> seata.log &
注意:
其中9999为服务端口号;file为启动模式,这里指seata服务将采用文件的方式存储信息。
测试
查看启动日志
cat seata.log
实现如下功能
李四账户增加金额。
创建cloud-seata-bank2
pom引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
编写主启动类
//添加对mapper包扫描 Mybatis-plus
@MapperScan("com.lxx.mapper")
//开启发现注册
@EnableDiscoveryClient
@SpringBootApplication
@Slf4j
public class SeataBank2Main6002 {
public static void main(String[] args) {
SpringApplication.run(SeataBank2Main6002.class, args);
log.info("************** SeataBank1Main6002 *************");
}
}
编写YML配置文件
server:
port: 6002
spring:
application:
name: provider-bank2
cloud:
nacos:
discovery:
# Nacos服务地址
server-addr: 114.117.183.67:8848
datasource:
url: jdbc:mysql://114.117.183.67:3306/bank2?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
创建实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName("account_info")
public class AccountInfo {
//id
@TableId
private Long id;
//户主姓名
@TableField("account_name")
private String accountName;
//银行卡号
@TableField("account_no")
private String accountNo;
//账户密码
@TableField("account_password")
private String accountPassword;
//账户余额
@TableField("account_balance")
private Double accountBalance;
}
编写持久层
@Mapper
public interface AccountMapper extends BaseMapper<AccountInfo> {
}
编写转账接口
public interface IAccountInfoService {
//李四增加金额
void transfer(String accountNo, Double amount);
}
编写转账接口实现类
@Service
public class AccountInfoServiceImpl implements IAccountInfoService {
@Autowired
private AccountMapper accountMapper;
@Override
public void transfer(String accountNo, Double amount) {
// 1.获取用户信息
QueryWrapper<AccountInfo> queryWrapper = new QueryWrapper();
//注意:构造时使用的是数据库字段,不是entity属性
queryWrapper.eq("account_no", accountNo);
AccountInfo accountInfo = accountMapper.selectOne(queryWrapper);
// 2.判断accountInfo是否为空
if (accountInfo != null) {
// 3.给李四加钱
accountInfo.setAccountBalance(accountInfo.getAccountBalance() + amount);
accountMapper.updateById(accountInfo);
}
}
}
编写控制层
@RestController
@RequestMapping("/bank2")
public class Bank2Controller {
@Autowired
private IAccountInfoService accountInfoService;
//李四接收张三的转账
@GetMapping("/transfer")
public String transfer(String accountNo, Double amount) {
//李四增加金额
accountInfoService.transfer(accountNo, amount);
return "bank2" + amount;
}
}
实现如下功能
1、张三账户减少金额
2、远程调用bank2向李四转账。
创建cloud-seata-bank1
pom引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>
编写主启动类
//添加对mapper包扫描 Mybatis-plus
@MapperScan("com.lxx.mapper")
//开启OpenFiegn
@EnableFeignClients
//开启发现注册
@EnableDiscoveryClient
@SpringBootApplication
@Slf4j
public class SeataBank1Main6001 {
public static void main(String[] args) {
SpringApplication.run(SeataBank1Main6001.class, args);
log.info("************** SeataBank1Main6001 *************");
}
}
编写YML配置文件
server:
port: 6001
spring:
application:
name: consumer-bank1
cloud:
nacos:
discovery:
# Nacos服务地址
server-addr: 114.117.183.67:8848
datasource:
url: jdbc:mysql://114.117.183.67:3306/bank1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
创建实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName("account_info")
public class AccountInfo {
//id
@TableId
private Long id;
//户主姓名
@TableField("account_name")
private String accountName;
//银行卡号
@TableField("account_no")
private String accountNo;
//账户密码
@TableField("account_password")
private String accountPassword;
//账户余额
@TableField("account_balance")
private Double accountBalance;
}
编写持久层
@Mapper
public interface AccountMapper extends BaseMapper<AccountInfo> {
}
编写转账接口
public interface IAccountInfoService {
//张三扣减金额
void transfer(String accountNo, Double amount);
}
编写远程调用接口
package com.lxx.feign;
@FeignClient(value = "provider-bank2")
public interface Bank2ServiceFeign {
@GetMapping("/bank2/transfer")
String transfer(@RequestParam("accountNo") String accountNo, @RequestParam("amount") Double amount);
}
编写转账接口实现类
@Service
public class AccountInfoServiceImpl implements IAccountInfoService {
@Autowired
private AccountMapper accountMapper;
@Autowired
Bank2ServiceFeign bank2ServiceFeign;
@Override
public void transfer(String accountNo, Double amount) {
// 1.获取用户信息
QueryWrapper<AccountInfo> queryWrapper = new QueryWrapper();
//注意:构造时使用的是数据库字段,不是entity属性
queryWrapper.eq("account_no", accountNo);
AccountInfo accountInfo = accountMapper.selectOne(queryWrapper);
// 2.判断accountInfo是否为空
if (accountInfo != null) {
// 3.给张三减钱
accountInfo.setAccountBalance(accountInfo.getAccountBalance() - amount);
accountMapper.updateById(accountInfo);
// 4.调用李四微服务,转账,李四增加金额
bank2ServiceFeign.transfer("2", amount);
}
}
}
编写控制层
@RestController
@RequestMapping("/bank1")
public class Bank1Controller {
@Autowired
private IAccountInfoService accountInfoService;
//张三给李四转账
@GetMapping("/transfer")
public String transfer(String accountNo, Double amount) {
//张三减少金额
//李四增加金额
accountInfoService.transfer(accountNo, amount);
return "bank1" + amount;
}
}
初始数据库数据
正常情况
发送请求 http://localhost:6001/bank1/transfer?accountNo=1&amount=100
制造异常
在bank2微服务制造异常(比如关闭bank2此微服务)
异常后测试
发送请求 http://localhost:6001/bank1/transfer?accountNo=1&amount=100
Seata实现XA要点
1、全局事务开始使用@GlobalTransactional标识。
2、每个本地事务方案仍然使用@Transactional标识。
3、每个数据库都需要创建undo_log表,此表是Seata保证本地事务一致性的关键。
创建 UNDO_LOG 表
SEATA XA 模式需要 UNDO_LOG 表
-- 注意此处0.3.0+ 增加唯一索引 ux_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;
添加依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
修改配置文件YML
seata:
# 注册中心
registry:
type: file
service:
# seata服务端的地址和端口信息,多个使用英文分号分隔
grouplist:
default: 114.117.183.67:9999
tx-service-group: my_test_tx_group
bank1微服务开启全局事物
@Transactional
@GlobalTransactional//开启全局事务
@Override
public void transfer(String accountNo, Double amount) {
// 1.获取用户信息
QueryWrapper<AccountInfo> queryWrapper = new QueryWrapper();
//注意:构造时使用的是数据库字段,不是entity属性
queryWrapper.eq("account_no", accountNo);
AccountInfo accountInfo = accountMapper.selectOne(queryWrapper);
// 2.判断accountInfo是否为空
if (accountInfo != null) {
// 3.给张三减钱
accountInfo.setAccountBalance(accountInfo.getAccountBalance() - amount);
accountMapper.updateById(accountInfo);
// 4.调用李四微服务,转账,李四增加金额
bank2ServiceFeign.transfer("2", amount);
}
}
}
注意:
将@GlobalTransactional注解标注在全局事务发起的Service实 现方法上,开启全局事务 :GlobalTransactionalInterceptor会 拦截@GlobalTransactional注解的方法,生成全局事务ID (XID),XID会在整个分布式事务中传递。 在远程调用时,spring-cloud-alibaba-seata会拦截Feign调用将 XID传递到下游服务。
bank2微服务开启事物
@Transactional
@Override
public void transfer(String accountNo, Double amount) {
// 1.获取用户信息
QueryWrapper<AccountInfo> queryWrapper = new QueryWrapper();
//注意:构造时使用的是数据库字段,不是entity属性
queryWrapper.eq("account_no", accountNo);
AccountInfo accountInfo = accountMapper.selectOne(queryWrapper);
// 2.判断accountInfo是否为空
if (accountInfo != null) {
// 3.给李四加钱
accountInfo.setAccountBalance(accountInfo.getAccountBalance() + amount);
accountMapper.updateById(accountInfo);
}
}
测试分布式事物
制作bank2异常时,发送请求 http://localhost:6001/bank1/transfer?accountNo=1&amount=100
总结
传统2PC(基于数据库XA协议)和Seata实现2PC的两种2PC方案, 由于Seata的零入侵并且解决了传统2PC长期锁资源的问题,所以推荐采用Seata实现2PC。