Seata

学习目标

  • 能够使用seata解决实际中分布式事务问题
  • 了解seata解决分步事务的原理

1. 总述

1. 事务

严格来说事务应该具备原子性、一致性、隔离性和持久性,简称 ACID

  • 原子性(Atomicity):一个事务内的所有操作不可被打断,要么全部完成,要么不做任何操作,如果在执行的过程中发生了错误,要回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过。

  • 一致性(Consistency):也叫数据一致性,比如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣。

  • 隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰,即一个事务内部的数据对于其他事务来说是隔离的。

  • 持久性(Durability),指的是一个事务完成了之后数据就被永远保存下来

2. 分布式事务

在分布式系统中,很多操作都是跨数据库,由多个本地事务组合而成,这种情况根本无法满足:ACID
比如:
image.png

  • 从浏览器发送一个下单请求,到聚合服务
  • 聚合服务,先调用订单服务,再调用库存服务,这两个服务分别操作不同的库
  • 如果下单成功,但是扣减库存失败,但这时订单数据库已经保存,无法回滚,就会产生数据安全问题

3. 解决方案

1. 两阶段提交

也叫2PC,是XA的标准实现。把分布式事务分为2个阶段:prepare和commit/rollback。
image.png
image.png
事务管理器:也叫事务协调者

  • 第一阶段
    • 订单服务和库存服务分别执行sql,但是不提交
  • 第二阶段
    • 事务管理器拿到sql执行结果
      • 都成功,发送commit命令,两个服务同时提交事务
      • 有失败,发送rollback命令,两个服务同时回滚

缺点:

  • 性能问题:订单服务成功,但是库存执行时间比较长,这时订单服务一直没有提交事务,会影响其他人的读写
  • 数据一致性:
    • 事务管理器向订单服务发送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的优化,基本流程:
image.png

  • 执行SQL时,自动拦截所有的SQL,保存SQL对数据修改前后的快照,为了回滚使用
  • 所有参与者的事务都成功,只需清理每个数据源中对应的日志数据
  • 如果有参与者失败,就要根据日志数据进行回滚

好处:分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放资源,相比两阶段提交,极大的提升了系统的吞吐量。

2. Seata 介绍

在微服务架构中,一次业务操作需要跨多个数据库或者多个系统远程调用,就会产生分布式事务问题
image.png

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
image.png
下载后,上传 seata-server-1.5.2.tar.gz 到 /home/soft 目录
image.png
执行:tar -zxvf seata-server-1.5.2.tar.gz,解压:
image.png

1. 注册nacos

把seata注册到 nacos中,作为一个服务,所以先操作 nacos

  1. 创建 seata 命名空间
    image.png
  2. 在 seata 空间,增加一个 seata-server 的配置

image.png
至于内容,从 seata 的源码包中获取
image.png
在源码文件夹的 script\config-center 目录下,有个config.txt
image.png
但是需要修改其中几项,比如:
image.png

  1. 修改 seata 中的 application.yml

解压到 /home/soft 中的 seata ,在 conf 目录中有 application.yml
image.png
修改其中的部分配置,比如:
image.png

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数据库

  1. 配置了数据库链接,所以自己创建 seata 数据库,选择utf8编码就行

image.png

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

image.png

  1. 结果

image.png

4. 启动

注意:需要先启动nacos
在下面目录中找到:seata-server.sh
image.png
执行:./seata-server.sh -h 本机IP地址
image.png
结果:
image.png
nacos中查看
image.png
浏览器访问:http://192.168.110.103:7091/
image.png
用户名、密码 都是:seata
image.png

4. 使用

1. 数据库

创建两个新数据库
image.pngimage.png
image.png
每个库都创建下面的表

-- 分布式事务回滚使用, 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方法中抛出一个异常
image.png
当没有GlobalTransactional注解时,浏览器上访问:http://localhost:8200/saveUser?id=1&name=feng1
虽然报错,但是存储进去数据
image.png

清理数据库,开启GlobalTransactional,重启consumer,然后再次访问
代码仍然报错,但是数据库中没有数据,证明分布式事务生效
image.png
注意:运行时,先启动nacos,在启动seata,最后启动我们的项目

5. 原理

debug代码
image.png
这时查看consumer对应的数据库,发现已经有数据
image.png
同时查看 undo_log 表
image.png
image.png
image.png
放过断点,让代码报错,再次查看数据库发现,数据已经被删除

原理:分布式事务分为两个阶段

  • 第一阶段:
    • 事务的参与者,直接提交事务,但是维护一张UNDO_LOG表,其中rollback_info字段存储了sql执行前和执行后的数据
  • 第二阶段:
    • 代码报错:执行回滚命令,从UNDO_LOG表获取数据回滚
    • 代码无错:正常结束
    • 最后删除UNDO_LOG中关于这次事务的记录
posted @ 2023-02-23 16:32  空空大首领  阅读(202)  评论(0)    收藏  举报