Seata
学习目标
- 能够使用seata解决实际中分布式事务问题
- 了解seata解决分步事务的原理
1. 总述
1. 事务
严格来说事务应该具备原子性、一致性、隔离性和持久性,简称 ACID。
-
原子性(Atomicity):一个事务内的所有操作不可被打断,要么全部完成,要么不做任何操作,如果在执行的过程中发生了错误,要回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过。
-
一致性(Consistency):也叫数据一致性,比如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣。
-
隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其他事务来说是隔离的。
-
持久性(Durability),指的是一个事务完成了之后数据就被永远保存下来
2. 分布式事务
在分布式系统中,很多操作都是跨数据库,由多个本地事务组合而成,这种情况根本无法满足:ACID
比如:

- 从浏览器发送一个下单请求,到聚合服务
- 聚合服务,先调用订单服务,再调用库存服务,这两个服务分别操作不同的库
- 如果下单成功,但是扣减库存失败,但这时订单数据库已经保存,无法回滚,就会产生数据安全问题
3. 解决方案
1. 两阶段提交
也叫2PC,是XA的标准实现。把分布式事务分为2个阶段:prepare和commit/rollback。


事务管理器:也叫事务协调者
- 第一阶段
- 订单服务和库存服务分别执行sql,但是不提交
- 第二阶段
- 事务管理器拿到sql执行结果
- 都成功,发送commit命令,两个服务同时提交事务
- 有失败,发送rollback命令,两个服务同时回滚
- 事务管理器拿到sql执行结果
缺点:
- 性能问题:订单服务成功,但是库存执行时间比较长,这时订单服务一直没有提交事务,会影响其他人的读写
- 数据一致性:
- 事务管理器向订单服务发送commit命令后宕机,库存服务无法提交事务
- 事务的参与者,有的提交成功,有的提交失败
2. 三阶段提交
也叫3PC,把2PC的第一阶段,细分成了CanCommit和PreCommit
新增的CanCommit是一个询问阶段,让参与者执行sql前,根据条件判断该事务是否能顺利完成
比如:扣减库存,先判断库存够不够
优缺点:
- 3PC解决了2PC的单点问题:
- 如果协调者宕机,参与者没有收到commit的消息,3PC默认将事务提交
- 而不是回滚事务或者继续等待
- 性能:多了一个询问阶段,所以性能反而会更差,如果是需要回滚的场景中,3PC的性能通常要好一些
- 没有解决数据一致性问题
3. TCC
补偿事务:针对每个操作,都要有一个与其对应的确认和补偿(撤销)操作,
分为3个阶段
- Try 阶段:对业务系统做检测及资源预留
- Confirm 阶段:对业务系统做确认提交,Try成功,执行 Confirm 时,默认 Confirm阶段是不会出错的。
即:只要Try成功,Confirm一定成功。 - Cancel 阶段:在业务报错后,回滚业务,预留资源释放
比如:订单服务,付款后,会修改订单状态
- try:先把订单状态该改成支付中,然后调用支付服务
- confirm:支付服务完成后,修改订单状态为支付成功
- cancel:支付服务报错,修改订单状态为支付失败
缺点:太麻烦,一个操作需要写3个方法,业务侵入性高,开发成本高
4. 本地消息表
比较流行,基本流程:

- 事务发起方,建一个消息表,在存储业务数据的时,也往消息表中记录,这两个操作是在一个事务里提交,
- 后台会扫描消息表,之后发送MQ消息到消费方。如果消息发送失败,会进行重试发送。
- 消费方,拿到消息后,处理自己的业务逻辑。
- 处理成功,发送一个消息给事务发起方,修改消息表中的消息状态
- 处理失败:
- 重试。
- 如果是业务上面的失败,可以给事务发起方发送一个消息,通知进行回滚操作
5. AT事务
阿里的开源 seata,和商业版的GTS,一般使用这种方式
也是对2PC的优化,基本流程:

- 执行SQL时,自动拦截所有的SQL,保存SQL对数据修改前后的快照,为了回滚使用
- 所有参与者的事务都成功,只需清理每个数据源中对应的日志数据
- 如果有参与者失败,就要根据日志数据进行回滚
好处:分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放资源,相比两阶段提交,极大的提升了系统的吞吐量。
2. Seata 介绍
在微服务架构中,一次业务操作需要跨多个数据库或者多个系统远程调用,就会产生分布式事务问题

Seata是阿里提供给的分布式事务解决方案
官网地址:http://seata.io/zh-cn/
用户文档:http://seata.io/zh-cn/docs/overview/what-is-seata.html
git地址:https://github.com/seata/seata
3. 安装配置
注意:需要先安装JDK8
1. 下载
下载地址:http://seata.io/zh-cn/blog/download.html
另一个地址:https://github.com/seata/seata/releases/tag/v1.5.2

下载后,上传 seata-server-1.5.2.tar.gz 到 /home/soft 目录

执行:tar -zxvf seata-server-1.5.2.tar.gz,解压:

1. 注册nacos
把seata注册到 nacos中,作为一个服务,所以先操作 nacos
- 创建 seata 命名空间
![image.png]()
- 在 seata 空间,增加一个 seata-server 的配置

至于内容,从 seata 的源码包中获取

在源码文件夹的 script\config-center 目录下,有个config.txt

但是需要修改其中几项,比如:

- 修改 seata 中的 application.yml
解压到 /home/soft 中的 seata ,在 conf 目录中有 application.yml

修改其中的部分配置,比如:

seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos # 表示从nacos中获取配置
nacos:
server-addr: 192.168.110.102:8848
namespace: seata
group: SEATA_GROUP
username: nacos
password: nacos
data-id: seata-server
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos # 表示把seata作为一个服务注册到nacos上
nacos:
application: seata-server
server-addr: 192.168.110.102:8848
group: SEATA_GROUP
namespace: seata
cluster: default
username: nacos
password: nacos
# stor#e:
# support: file 、 db 、 redis
# mode: file
解释:
- server-addr:nacos地址
- namespace:命名空间,默认是public,设置为 seata
- group:服务分组,nacos上注册的服务可以分组
- username:nacos用户名
- password:nacos密码
- application:服务名,把seata当做一个服务注册到nacos上
- cluster:设置为default
2. 初始化seata数据库
- 配置了数据库链接,所以自己创建 seata 数据库,选择utf8编码就行

- 执行sql:找到下载的seata源码,在下面路径中可以找到sql

- 结果

4. 启动
注意:需要先启动nacos
在下面目录中找到:seata-server.sh

执行:./seata-server.sh -h 本机IP地址

结果:

nacos中查看

浏览器访问:http://192.168.110.103:7091/

用户名、密码 都是:seata

4. 使用
1. 数据库
创建两个新数据库



每个库都创建下面的表
-- 分布式事务回滚使用, sql在源码文件夹的 script\client\at\db 目录下
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
2. Consumer
1. pom.xml
修改Consumer的pom.xml,增加:
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 连接MySQL数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
2. properties
#数据库
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/seata_user?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
seata.application-id=${spring.application.name}
# 获取配置
seata.config.type=nacos
seata.config.nacos.server-addr=192.168.110.102:8848
seata.config.nacos.namespace=seata
seata.config.nacos.group=SEATA_GROUP
seata.config.nacos.dataId=seata-server
seata.config.nacos.username=nacos
seata.config.nacos.password=nacos
# 获取服务
seata.registry.type=nacos
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=192.168.110.102:8848
seata.registry.nacos.namespace=seata
3. 启动类
@SpringBootApplication
@EnableFeignClients//开启Feign
@EnableAutoDataSourceProxy
@MapperScan("com.javasm.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
4. 其他代码
User实体类
@TableName("user")
public class User {
@TableId
private int id;
private String name;
// get、set省略
}
userMapper
public interface UserMapper extends BaseMapper<User> {
}
UserService
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper;
@Autowired
private ProviderService providerService;
//标记这个方法有分布式事务,遇到任何异常就回滚
@GlobalTransactional(rollbackFor = Exception.class)
public void save(User user) {
userMapper.insert(user);
providerService.saveUser(user.getId(),user.getName());
int i = 2/0;//故意报错
}
}
ProvicerService 中,新增:
@GetMapping("/saveUser")// @RequesetParam注解不可少
String saveUser(@RequestParam int id, @RequestParam String name);
新增 SeataController
@RestController
public class SeataController {
@Autowired
private UserServiceImpl userSerivce;
@GetMapping("/saveUser")
public String saveUser(User user){
userSerivce.save(user);
return "success";
}
}
3. Provider
1. pom.xml
修改 pom.xml,增加
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 连接MySQL数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
2. properties
#数据库
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/seata_user2?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
seata.application-id=${spring.application.name}
# 获取配置
seata.config.type=nacos
seata.config.nacos.server-addr=192.168.110.102:8848
seata.config.nacos.namespace=seata
seata.config.nacos.group=SEATA_GROUP
seata.config.nacos.dataId=seata-server
seata.config.nacos.username=nacos
seata.config.nacos.password=nacos
# 获取服务
seata.registry.type=nacos
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=192.168.110.102:8848
seata.registry.nacos.namespace=seata
3. 启动类
@SpringBootApplication
@EnableAutoDataSourceProxy
@MapperScan("com.javasm.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
4. 其他代码
User实体类
@TableName("user")
public class User {
@TableId
private int id;
private String name;
// get、set省略
}
userMapper
public interface UserMapper extends BaseMapper<User> {
}
UserService
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper;
public void save(User user) {
userMapper.insert(user);
}
}
在ProviderController中,新增 :
@Autowired
private UserServiceImpl userSerivce;
@GetMapping("/saveUser")
public String saveUser(User user){
userSerivce.save(user);
return "success";
}
4. 结果
按照上面配置,我们在Consumer的saveUser方法中抛出一个异常

当没有GlobalTransactional注解时,浏览器上访问:http://localhost:8200/saveUser?id=1&name=feng1
虽然报错,但是存储进去数据

清理数据库,开启GlobalTransactional,重启consumer,然后再次访问
代码仍然报错,但是数据库中没有数据,证明分布式事务生效

注意:运行时,先启动nacos,在启动seata,最后启动我们的项目
5. 原理
debug代码

这时查看consumer对应的数据库,发现已经有数据

同时查看 undo_log 表



放过断点,让代码报错,再次查看数据库发现,数据已经被删除
原理:分布式事务分为两个阶段
- 第一阶段:
- 事务的参与者,直接提交事务,但是维护一张UNDO_LOG表,其中rollback_info字段存储了sql执行前和执行后的数据
- 第二阶段:
- 代码报错:执行回滚命令,从UNDO_LOG表获取数据回滚
- 代码无错:正常结束
- 最后删除UNDO_LOG中关于这次事务的记录


浙公网安备 33010602011771号