Seata框架
1.Seata简介
官网: http://seata.io/zh-cn/
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
2. 什么是事务
事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元,也就是说事务是具有原子性的
2.1 事务的特性(ACID)
原子性(Atomicity)
事务的原子性保证事务中包含的一组更新操作是原子的,不可分割的,不可分割是事务最小的工作单位,所包含的操作被视为一个整体,执行过程中遵循“要么全部执行,要不都不执行”,不存在一半执行,一半未执行的情况。
一致性(Consistency)
事务的一致性要求事务必须满足数据库的完整性约束,且事务执行完毕后会将数据库由一个一致性的状态变为另一个一致性的状态。事务的一致性与原子性是密不可分的
隔离性(Isolation)
事务的隔离性要求事务之间是彼此独立的,隔离的。即一个事务的执行不可以被其他事务干扰。具体到操作是指一个事务的操作必须在一个事务commit之后才可以进行操作。
持续性(Durability)
事物的持续性也称持久性,是指一个事务一旦提交,它对数据库的改变将是永久性的,因为数据刷进了物理磁盘了,其他操作将不会对它产生任何影响。
3. 什么是分布式事务问题
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
换句话说就是事务的参与者不在同一个JVM下面,也就是不在同一个事务管理器下
3.1 分布式事务概念
随着互联化的蔓延,各中的种项目都逐渐向分布式服务做转换。如今微服务已经普遍存在,本地事务已经无法满足分布式的要求,由此分布式事务问题诞生。 分布式事务被称为世界性的难题,目前分布式事务存在两大理论依据:CAP定律 BASE理论。
分布式事务没有谁说100% ,99.99% 99.9999% 提高成功率
3.1.1 CAP定律
这个定理的内容是指的是在一个分布式系统中、Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性©
在分布式系统中的所有数据备份,在同一时刻是否同样的值。
例如3 个zk 构成的集群,它的3个节点,在每一个时刻,数据都是相同的
可用性(A)
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求
例如在zk ,主机挂了,就没有可用性了(重新选举)
在eureka 里面,任何一个挂了,都不影响
分区容错性(P)
以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
在分布式里面,机器的数据复制,选举,一般都是通过网络的通信来完成的。
若在一个时间内,你无法达到数据的一致性,就必须在C 和 A 直接选择一个
CAP 让我们知道,我们的系统最多保持CAP 三个里面的(CP,AP)
若没有P的特性,就没有集群可言!
3.1.2 BASE理论(分布式系统的解决方案的理论)
BASE是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。
BASE理论的核心思想是:
即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
我们的系统无法做到数据的强一致性,但是我们能做到最终的一致性!就是有延迟的一致性!
最终一致性 (mq)-- 延迟一致性
实时一致性 (LCN,seata TCC) – 及时一致性
基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性----注意,这绝不等价于系统不可用。比如:
(1)响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
(2)系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
软状态
软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。数据的变化是逐渐变化的状态(中间的状态),不是一个塌陷态!
最终一致性
最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
4.分布式事务场景复现
我们先搭建业务架构,演示分布式事务问题,因为我们要使用远程调用,所以我们直接nacos做注册中心,openfeign做远程调用,本次业务为经典的转账问题
4.1创建数据库
新建数据库banka和bankb,分别新建一张账户金额表acount
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(11) NOT NULL,
`money` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES (1, 1000);
INSERT INTO `account` VALUES (2, 0);
SET FOREIGN_KEY_CHECKS = 1;
4.2创建服务account-service-a
4.2.1pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>account-service-a</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>account-service-a</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2021.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2.2完善配置文件application.yml
server:
port: 8081
spring:
application:
name: account-service-a
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/banka?useSSL=false&serverTimezone=UTC
druid:
max-active: 20
min-idle: 2
initial-size: 2
cloud:
nacos: # Nacos的配置
server-addr: localhost:8848
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4.2.3启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan(basePackages = {"com.powernode.mapper"})
public class AccountServiceAApplication {
public static void main(String[] args) {
SpringApplication.run(AccountServiceAApplication.class, args);
}
}
4.2.4生成代码
使用插件生成实体类,数据访问类,业务类
4.2.5 创建AccountAController
@RestController
public class AccountAController {
@Autowired
private AccountService accountService;
/**
* 用户转账的接口
*
* @param decrUserId 减钱的用户id,发起转账的用户id
* @param incrUserId 加钱的用户id
* @param money 金额
* @return
*/
@GetMapping("changeAccount")
public String changeAccount(Integer decrUserId, Integer incrUserId, Integer money) {
accountService.doAccount(decrUserId, incrUserId, money);
return "转账成功";
}
}
4.2.6 修改AccountService
/**
* 转账的接口
*
* @param decrUserId
* @param incrUserId
* @param money
*/
void doAccount(Integer decrUserId, Integer incrUserId, Integer money);
4.2.7 创建AccountFeign远程调用
@FeignClient(value = "account-service-b")
public interface AccountFeign {
/**
* 远程调用加钱的服务
*
* @param userId
* @param money
*/
@PostMapping("incrMoney")
void incrMoney(@RequestParam("userId") Integer userId, @RequestParam("money") Integer money);
}
4.2.8修改AccountServiceImpl
@Autowired
private AccountFeign accountFeign;
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void doAccount(Integer decrUserId, Integer incrUserId, Integer money) {
Account account = accountMapper.selectByPrimaryKey(decrUserId);
int finalMoney = account.getMoney() - money;
if (finalMoney < 0) {
throw new IllegalArgumentException("金额不足");
}
// 更新自身账户的余额
account.setMoney(finalMoney);
int i = accountMapper.updateByPrimaryKey(account);
if (i > 0) {
// --远程账户增加余额
accountFeign.incrMoney(incrUserId, money);
}
}
4.3 搭建account-service-b
4.3.1pom文件
和account-service-a一样,少了openfeign的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.powernode</groupId>
<artifactId>account-service-b</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>account-service-b</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.3.2 完善配置文件application.yml
server:
port: 8082
spring:
application:
name: account-service-b
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/bankb?useSSL=false&serverTimezone=UTC
druid:
max-active: 20
min-idle: 2
initial-size: 2
cloud:
nacos: # Nacos的配置
server-addr: localhost:8848
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4.3.2启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = {"com.powernode.mapper"})
public class AccountServiceBApplication {
public static void main(String[] args) {
SpringApplication.run(AccountServiceBApplication.class, args);
}
}
4.3.3 创建AccountBController
@RestController
public class AccountBController {
@Autowired
private AccountService accountService;
/**
* 远程调用加钱的服务
*
* @param userId
* @param money
*/
@PostMapping("incrMoney")
void incrMoney(@RequestParam("userId") Integer userId, @RequestParam("money") Integer money) {
accountService.incrMoney(userId,money);
}
}
4.3.4 修改AccountService
/**
* 加钱的接口
*
* @param userId
* @param money
*/
void incrMoney(Integer userId, Integer money);
4.3.5 修改AccountServiceImpl
/**
* 加钱的接口
*
* @param userId
* @param money
*/
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void incrMoney(Integer userId, Integer money) {
// 查询账户详情
Account account = accountMapper.selectByPrimaryKey(userId);
//增加自生账户余额
account.setMoney(account.getMoney() + money);
// 更新余额
accountMapper.updateByPrimaryKey(account);
}
4.4启动测试
先启动nacos服务,在启动account-service-b,在启动account-service-a,查看nacos
访问测试转账
http://localhost:8081/changeAccount?decrUserId=1&incrUserId=2&money=500
转账成功,查看数据库,正常更改数据
4.5制造异常
我们在account-service-b里面制造异常,远程调用失败了,那么A服务里面也会报错,数据会回滚,这样测试不出来,所以我们在远程调用以后,在A服务里面制造一个异常
4.5.1还原数据
4.5.2修改account-service-a的实现类
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void doAccount(Integer decrUserId, Integer incrUserId, Integer money) {
Account account = accountMapper.selectByPrimaryKey(decrUserId);
int finalMoney = account.getMoney() - money;
if (finalMoney < 0) {
throw new IllegalArgumentException("金额不足");
}
// 更新
account.setMoney(finalMoney);
int i = accountMapper.updateByPrimaryKey(account);
if (i > 0) {
// 远程调用加钱服务
accountFeign.incrMoney(incrUserId, money);
int a = 10 / 0;
}
}
4.5.3 重启测试分布式事务问题
重启后访问
http://localhost:8081/changeAccount?decrUserId=1&incrUserId=2&money=500
我们去看数据库,发现用户1的钱没有减少,用户2的钱却增加了,这就出现了分布式事务问题
因为两次数据库操作没有在同一个事务管理器下
5. Seata-Server的安装
5.1版本选择
在使用seata之前,我们要先确认版本,因为在前面的章节我们确认了,springboot版本为2.6.8,alibaba的版本为2021.0.1.0,所以我们去看依赖管理中seata的版本1.4.2
5.2 下载Seata-Server
地址:https://github.com/seata/seata/tags找到V1.4.2版本下载
5.3 修改数据库连接
修改config目录下的file.conf配置文件,修改模式为数据库,mode=“db”,修改驱动,连接字符串,账号密码。
5.4 新建数据库seata创建以下表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '全局事务ID',
`transaction_id` bigint(20) NULL DEFAULT NULL COMMENT '事务ID',
`status` tinyint(4) NOT NULL COMMENT '状态',
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '应用ID',
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '事务分组名',
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '执行事务的方法',
`timeout` int(11) NULL DEFAULT NULL COMMENT '超时时间',
`begin_time` bigint(20) NULL DEFAULT NULL COMMENT '开始时间',
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '应用数据',
`gmt_create` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL COMMENT '分支事务ID',
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '全局事务ID',
`transaction_id` bigint(20) NULL DEFAULT NULL COMMENT '全局事务ID,不带TC地址',
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源分组ID',
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源ID',
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '事务模式,AT、XA等',
`status` tinyint(4) NULL DEFAULT NULL COMMENT '状态',
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端ID',
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '应用数据',
`gmt_create` datetime(6) NULL DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime(6) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '行键',
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '全局事务ID',
`transaction_id` bigint(20) NULL DEFAULT NULL COMMENT '全局事务ID,不带TC 地址',
`branch_id` bigint(20) NOT NULL COMMENT '分支ID',
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源ID',
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '表名',
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '主键对应的值',
`gmt_create` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
#创建在banka和bankb数据库中
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=2 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
5.5Seata-Server启动
进入bin目录下启动,windows上启动bat文件
6. 使用Seata解决分布式事务问题
使用seata之前,先还原数据库数据。
6.1 修改两个项目的依赖,都增加seata的依赖
<!-- seata的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
6.2 修改两个项目的配置文件
修改account-service-a的配置文件
seata:
enabled: true # 开启seata功能
tx-service-group: ${spring.application.name}
service:
grouplist: # 事务组通讯端口和地址
default: localhost:8091
vgroup-mapping: # 使用默认的组映射,相当于在一个组别下
account-service-a: default
修改account-service-b的配置文件
seata:
enabled: true # 开启seata功能
tx-service-group: ${spring.application.name} # 事务组名称
service:
grouplist: # 事务组通讯端口和地址
default: localhost:8091
vgroup-mapping: # 使用默认的组映射,相当于在一个组别下
account-service-b: default
6.3 在account-service-a的实现类添加注解@GlobalTransactional
去掉两边方法的本事事务@Transactional,方便查看日志信息等
6.4重启测试
访问 http://localhost:8081/changeAccount?decrUserId=1&incrUserId=2&money=500
查看数据库,发现已经解决了分布式事务问题了,在控制台中也看到回滚的日志信息
注意:调试启动,查看几个日志表数据,可能会超时,报下列错误:
Could not found global transaction xid 2486286608909156711
由于feign远程调用超时了,可以修改以下配置测试:
feign:
client:
config:
default:
connectionTimeout: 3000
readTimeout: 3000
7. Seata总结
在seata处理分布式事务中出现三个角色
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
我们的本次事务管理图为:
Seata默认使用AT模式(2pc的方式)
一阶段:
业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
global_table记录了全局事务的信息
branch_table记录了分支事务的信息,当一个服务调用另一个服务进行全局事务时,在该表中插入了当前两个服务分支事务的相关信息,其中重要的有ID、事务模式、客户端地址、数据库连接地址等。
lock_table记录了锁相关的信息 (数据库行锁,业务加锁)
在全局事务的一阶段中,分支事务在获取到全局锁提交事务时,会释放本地锁和连接资源,并在undo_log表中插入一条数据。
例如在减钱以后,查看undo_log表
在第二阶段中,如果全局事务成功,会收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
在第二阶段中,如果全局事务失败,会收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作:
通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较。根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句。提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。反向补偿
1.TM端使用@GolableTranscation进行全局事务开启、提交、回滚
2.TM开始RPC调用远程服务--记录操作前的数据
3.RM端seata-client通过扩展DataSourceProxy,实现自动生成undo-log与TC上报
4.TM告知TC提交/回滚全局事务
5.TC通知RM各自执行commit/rollback操作,同时清除undo-log
8、Seat的四种模式
(1)AT
在 AT 模式下,用户只需关心自己的 “业务SQL”
1)执行过程
一阶段
在一阶段,Seata 会拦截“业务 SQL”,首先解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
二阶段提交
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
2)优点
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
(2)TCC
TCC分为三个阶段:
Try:做业务检查和资源预留
Confirm:确认提交
Cancel:业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放
优点
相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。
(3)Sage
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现
适用场景
业务流程长/多,参与者包含其他公司或遗留系统服务,无法提供 TCC 模式要求的三个接口。典型业务系统:如金融网络(与外部金融机构对接)、互联网微贷、渠道整合、分布式架构服务集成等业务系统,银行业金融机构使用广泛。
(4)XA
XA满足事务的强一致性,满足ACID原则,常用数据库都支持,实现简单,没有代码入侵。但是因为第一阶段需要锁定数据库资源,等待第二阶结束才释放,性能较差。
如何选择模式?
四种分布式事务模式,分别在不同的时间被提出,每种模式都有它的适用场景
AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。
TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。
用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
(2)TCC
TCC分为三个阶段:
Try:做业务检查和资源预留
Confirm:确认提交
Cancel:业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放
[外链图片转存中…(img-AnlsfFnu-1672026485025)]
优点
相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。
(3)Sage
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现
适用场景
业务流程长/多,参与者包含其他公司或遗留系统服务,无法提供 TCC 模式要求的三个接口。典型业务系统:如金融网络(与外部金融机构对接)、互联网微贷、渠道整合、分布式架构服务集成等业务系统,银行业金融机构使用广泛。
(4)XA
XA满足事务的强一致性,满足ACID原则,常用数据库都支持,实现简单,没有代码入侵。但是因为第一阶段需要锁定数据库资源,等待第二阶结束才释放,性能较差。
如何选择模式?
四种分布式事务模式,分别在不同的时间被提出,每种模式都有它的适用场景
AT 模式是无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本。
TCC 模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。
Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。
XA模式是分布式强一致性的解决方案,但性能低而使用较少。