springcloud:分布式事务组件seata详解(九)

0. 引言

我们知道在springboot单机架构中,可以使用@Transactional注解来快速的部署事务

但是在分布式架构下,@Transactional就无法发挥作用了。于是我们就需要一个能够支持分布式事务的组件来帮忙。于是乎,就引入了seata

1. 什么是分布式事务

在真正开始讲解seata之前,还是要给大家讲解清楚分布式事务。这样才能让大家真正体会到这个组件的作用。如果你已经掌握了这些概念,可以直接跳过这章。

1.1 什么是事务?

事务原本是用于单机架构,单数据库中的概念。表示要执行的一组操作,同时如果一组操作被申明为事务,那么就具备了四个特性:

  • 原子性(Atomicity):一个事务中的所有操作,要么执行,要么都不执行。通过undo log实现(这里不再拓展什么是undo log,以及实现的细节,后续再单独说明)
  • 一致性(Consistency):保证事务能把一个数据从一个正确的状态转移到另一个正确的状态,其中的中间状态是不会被其他事务所见的。通过锁机制实现(你有100W,我有10W,你打给我10W,对于你少了10W,我还没加上这10W的中间状态是不会被别的事务捕捉到的)
  • 隔离性(Isolability):事务独立执行,不受其他并发操作影响;通过锁和MVCC来实现
  • 持久性(Durability):事务完成后,对于数据的修改是永久的;通过redo log实现

事务中的一组操作,如果某一个操作发生报错,会通过undo log执行回滚,以此实现数据恢复成事务执行前的状态。

1.2 什么是分布式事务?

单机事务为什么不能使用在分布式系统?

如果不清楚这一点,建议大家实际体验一下。在服务A的方法中调用服务B的方法,让服务B的方法抛出一个错误,看看抛出错误后,服务A已经执行了的数据库操作会不会回退。

答案是不会的,因为他们根本不在一个事务中,我们可以在服务A中通过feign来调用服务B的方法,但是服务B的方法并不归属于服务A的事务体系中,因此服务B的报错并不会导致已经执行了的服务A的操作回退。

想要让服务A回退,那么就要让服务B纳入到服务A的事务体系中,也就是两者是同一个事务ID。这就是我们的分布式事务,它可以跨越服务来实现事务。

我在知乎上看到一个有意思的比喻:普通事务是小汽车倒车入库,要么成功,要么退出了重新倒。而分布式事务,是火车倒车入站,必须每一节都成功,不然就要一节一节的退出去重新来。

我们从物理概念上来理解,分布式事务是跨服务的,甚至是跨服务器的。

扫描二维码关注公众号,回复: 13824921 查看本文章

想要实现分布式事务,我们有多种方式,可以通过像redis这样的中间件来实现。但也可以用到我们今天介绍的seata组件来实现。甚至发展至今,一般我们谈到分布式事务,基本上seata已经成了我们的不二选择。

2. seata介绍

seata是一款由阿里巴巴开源的分布式事务组件。提供了AT、TCC、SAGA和XA等几种事务模式
seata官方开发文档。我们默认使用的是AT模式。

关于各种模式的工作原理,这里不做拓展了,先教会大家如何使用,后续我们再来详谈原理。感兴趣的同学也可以参考官方开发文档中的说明

2.1 AT模式工作机制

1、seata首先要求在业务表所在的数据库中创建一个undo_log表,用于记录回滚sql。所谓回滚sql就是与所执行sql相反的sql,比如执行了一个insert语句,那么对应的undo log就是一条delete语句。

2、业务表执行了操作后,seata会将执行的sql进行解析,生成回滚sql并且存储到undo_log表中

3、本地事务提交前,会向seata的服务端注册分支,申请对应的业务表中对应数据行的全局锁,这时其他的事务就无法对这条数据进行更新操作。

4、本地事务提交,业务数据的更新和前面生成的undo log会一起提交

5、将本地事务的执行结果上报给seata服务端。也就是说seata服务端会记录多个服务的本地事务。同时这些本地事务因为在seata服务端的管控之下,所以使用的事务ID和分支ID都是一样的。

6、如果某一个本地事务发生报错,那么seata服务端就会发起对应分支的回滚请求

7、同时开启一个本地事务,然后通过事务ID和分支ID去undo_log表查询到对应的回滚sql。并且执行回滚sql,再将执行结果上报给seata服务端

8、如果没有发生报错,seata服务端也会在对应分支发起请求,然后会异步批量的删除undo_log表中的记录

总结一句话,为什么能实现分布式事务,就是因为seata服务端将这些本地事务都记录下来了,同时也在每个库中记录了undo log。当有一个报错时,就找到这同一组的所有的本地事务,然后统一利用记录的undo log回退数据

大家可以看看mysql undo log表涉及的字段。其中xid就是事务ID,branch_id就是分支ID
在这里插入图片描述

3. seata使用

seata分为服务端(TC)和客户端(TM,RM),客户端通过引入jar包来使用。而服务端就需要我们单独安装了。

  • TC(Transaction Coordinator):事务协调器,维护全局事务的运行状态,协调和落实全局事务的回滚提交
    -TM(Transaction Manager):控制全局事务的边界,负责开启一个全局事务,并最终发起全局事务提交或回滚的决议
  • RM(Resource Manager):控制分支(本地)事务,负责分支注册、状态汇报,并接受TC的指令,驱动本地事务的提交和回滚

seata中涉及到配置中心和注册中心的设置。seata支持的注册中心有:

  • eureka
  • consul
  • nacos
  • etcd
  • zookeeper
  • sofa
  • redis
  • file(文件形式,直连)

支持的配置中心有:

  • nacos
  • consul
  • apollo
  • etcd
  • zookeeper
  • file(本地文件,包含conf,properties,yml配置文件的支持)

为了配置之前学习的知识体系(如不知道的,可查看专栏之前的博客),我们使用nacos作为注册中心,采用file作为配置中心,这里不用nacos作为配置中心,是因为目前seata对于nacos配置中心的配置比较复杂,不太适合初步学习。所以我们采用file形式。

如想使用其他组件作为注册中心、配置中心可参考官方文档

3.1 下载seata服务端

seata github下载地址

这里我们以seata1.4.0版本为例,官方建议安装1.4.0+版本
在这里插入图片描述

3.2 安装seata服务端

1、解压安装包

tar -zxvf seata-server-1.4.0.tar.gz

2、修改配置文件registry.conf,seata安装目录下执行

vim conf/registry.conf 

修改内容

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

配置中心采用默认的file形式,因此无需修改,使用默认的即可

config {
  # file、nacos 、apollo、zk、consul、etcd3
 type = "file"
 file {
    name = "file.conf"
  }
}

3、修改file.conf配置文件,配置持久化方式。默认采用的file形式,更建议使用db形式。这里以mysql举例

 vim conf/file.conf

修改内容

store {
  ## store mode: file、db、redis
  mode = "db"
 ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "mysql" 
    password = "mysql"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

4、导入seata数据库
创建seata数据库
在这里插入图片描述
导入表结构:表结构sql文件下载地址
在这里插入图片描述
5、启动seata-server。seata-server端口是8091,如果是安装在虚拟机上的,记得打开8091端口

./bin/seata-server.sh 

在这里插入图片描述

3.3 seata客户端配置

这里我们结合之前搭建的微服务框架来演示客户端的配置,不知道的可以参考之前的博客
springcloud:保姆式教程-从零搭建微服务

3.3.1 准备工作

该项目的springcloud2中有两个微服务:订单服务和商品服务
1、在两个服务中引入依赖,与服务端保持版本

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>1.4.0</version>
</dependency>

2、在两个服务的配置文件中添加如下配置。

注意
(1)这里的group要与server端配置的保持一致
(2)seata.service.vgroupMapping.xxx中的xxx为事务群组,要部署同一套分布式事务的微服务要求事务群组要一致。其值是在server 端conf/registry.conf配置的nacos集群名称registry.nacos.cluster

seata:
  tx-service-group: my-seata-service-group
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group : "SEATA_GROUP"
      namespace: ""
      username: "nacos"
      password: "nacos"
  service:
    vgroupMapping:
      # 事务群组,其值为seata server端配置的seata集群名,与registry.nacos.cluster保持一致,默认是default
      my-seata-service-group: default

客户端完整配置文件见官方github地址

3、在客户端涉及到的业务数据库中添加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;

3.3.2 测试

1、在product-server中添加保存接口

这里1/0会抛出错误,我们来模拟报错后数据回退

@PostMapping("save")
    public String save(){
    
    
         Product product = new Product();
         product.setId(555L);
         product.setName("商品xxx");
         product.setPrice(300.0);
         product.setNo("SP001");
         product.setCreateTime(new Date());
         int i = 1/0;
         return productService.save(product) ? "保存成功" : "保存失败";
    }

2、在order-server feign中创建调用product-server save接口

@FeignClient(name = "product-server",url = "localhost:9091")
public interface ProductApi {
    
    

    @GetMapping("list")
    public List<Product> list();

    @PostMapping("save")
    public String save();
}

3、在order-server中添加保存接口:保存订单的同时,调用product-server的save接口保存商品

在调用方接口中添加@GlobalTransactional注解

    @GlobalTransactional
	@PostMapping("save")
    public String save(){
    
    
        Order order = new Order();
        order.setId(555L);
        order.setOrderNo("DD001");
        order.setCreateTime(new Date());
        orderService.save(order);
        return productApi.save();
}

4、运行项目,调用order-server save接口

3.3.3 结果分析

1、我们在product-server的save方法执行前打个断点
在这里插入图片描述
2、然后观察order表,发现新增的订单数据已经添加成功
在这里插入图片描述
3、同时观察undo_log表,发现已经产生了一条数据
在这里插入图片描述
4、我们让代码继续执行,抛出错误
在这里插入图片描述
5、同时我们再观察order表,发现数据成功回退了
在这里插入图片描述
6、看看undo_log中的记录,因为回退执行完后也已经删除了。
在这里插入图片描述
7、那么到这里我们的分布式事务就部署成功了。

3.4 注意事项

1、只需要在调用方添加一个@GlobalTransactional注解即可,无需在被调用方添加@Transactional注解
2、客户端中的seata.tx-service-groupseata.service.vgroupMapping.xxx配置不要忘记,且同事务组保持统一

4. 其他API

  • 获取分布式事务ID
RootContext.getXID()
  • 指定事务手动回滚
TransactionManagerHolder.get().rollback(RootContext.getXID());
  • 当前事务手动回滚
// 1. 获取当前全局事务实例或创建新的实例
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
tx.rollback();

更多API介绍可见官方文档

5. 常见报错

can not get cluster name in registry config

报错:

i.s.c.r.netty.NettyClientChannelManager  : can not get cluster name in registry config 'service.vgroupMapping.order-server-seata-service-group', please make sure registry config correct

解决:
在微服务中添加配置

seata:
  tx-service-group: my-seata-service-group
  service:
    vgroupMapping:
      # 事务群组,其值为seata server端配置的seata集群名,与registry.nacos.cluster保持一致,默认是default
      my-seata-service-group: default

项目源码地址

本次演示源码git地址

关注公众号,了解更多新鲜内容

在这里插入图片描述

参考文章

https://www.zhihu.com/question/277356260
https://seata.io/zh-cn/docs/user/quickstart.html

猜你喜欢

转载自blog.csdn.net/qq_24950043/article/details/124083075
今日推荐