分布式事务Seata(nacos集成)

解决方案

1.XA协议

XA 协议是由 X/Open 组织提出的分布式事务处理规范,主要定义了事务管理器 TM 和局部资源管理器 RM 之间的接口。目前主流的数据库,比如 oracle、DB2 、mysql(5.0以后)都是支持 XA 协议的,你可以把 XA 理解为一个强一致性的中心化原子提交协议。
2PC:它就是把一个事务分成了两步来提交。第一步做准备动作,第二步做提交 / 回滚动作,这两步之间的协调是交由一个中心化的 Coordinator 来管理,保证多步操作的原子性。

第一步(Prepare):Coordinator 向各个分布式事务的参与者下达了 Prepare 指令,各个事务分别将 SQL 语句在数据库执行但不提交,并且将就绪状态上报给 Coordinator。

分布式事务Seata(nacos集成)_第1张图片

第二步(Commit/Rollback):如果所有节点都已就绪,那么 Coordinator 就下达 Commit 指令,各个参与者提交本地事务;如果有任何一个节点不能就绪,Coordinator 则下达 Rollback 指令进行本地回滚。

分布式事务Seata(nacos集成)_第2张图片

这种分布式事务有一个天然缺陷,导致 XA 特别不适合用在互联网的高并发场景里面。因为每个本地事务在 Prepare 阶段,都要一直占用一个数据库连接资源,这个资源直到二阶段 Commit 或者 Rollback 之后才会被释放。

使用场景:对事物有强一致要求,对事务执行效率要求不高,并且不希望有太多的代码侵入

2.TCC

TCC模式可以解决2pc中资源锁定和阻塞的问题,减少资源占用问题

它本质是一种补偿的思路,事务运行包括三个方法

  • Try:资源的检测和预留
  • Confirm:执行的业务操作提交;要求Try成功Confirm一定能成功;
  • Cancel:预留资源和释放

执行分两个阶段:

准备阶段(try):资源的检测和预留

执行阶段(confirm/cancel):根据上一步结果,判断下面的执行方法,如果上一步中所有事物参与者都成功,则这里执行confirm.反之,执行cancel
分布式事务Seata(nacos集成)_第3张图片

使用场景:

  • 对事物的一致性有要求(最终一致性)
  • 对性能要求较高
  • 开发人员具备较高的编码能力和幂等处理经验

3.消息可靠性

基本的设计思想就是将远程分布式事务拆分成一系列的本地事务.

基本原理

一般分为事务的发起者A和其他参与者B:

  • 事务发起者A执行本地事务
  • 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B
  • 事务参与者B接收到消息后执行本地事务
    分布式事务Seata(nacos集成)_第4张图片

几个注意事项:

  • 事务发起者A必须确保本地事务成功后,消息一定发送成功
  • MQ必须保证消息正确投递和持久化保存
  • 事务参与者B必须确保消息最终一定能消费,如果失败需要多次重试
  • 事务B执行失败,会重试,但不会导致事务A回滚

4.AT模式

2019年一月份,Seata开源了AT模式,AT模式是一种无侵入的分布式事务解决方案.可以看做是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题.

在AT模式下,用户只需关注自己的业务sql,用户的业务sql作为一阶段,Stata框架会自动生成事务的二阶段提交和回滚操作.

基本原理:

分布式事务Seata(nacos集成)_第5张图片

两个阶段:

  • 一阶段:执行本地事务,并返回执行结果
  • 二阶段:根据一阶段的结果,判断二阶段做法:提交或回滚

但AT模式底层做的事情完全不同,而且二阶段不需要我们编写.全部由Seata自己实现了,也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务.

那么,AT模式如何实现无代码侵入,如何帮我们自动实现二阶段代码?

一阶段

在一阶段,Seata会拦截业务SQL,首先解析SQL预约,找到业务sql要更新的业务数据,在业务数据被更新前,将其保存成"before image",然后执行业务SQL更新业务数据,在业务数据更新之后,再将其保存成after image,最后获取全局行锁,提交事务,以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性.

这里的before image和agter image类似于数据库的undo和redo日志,但其实使用数据库模拟的.

分布式事务Seata(nacos集成)_第6张图片

二阶段

提交:

​ 因为业务sql在一阶段已经提交至数据库了,所以Seata框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可.

回滚:

​ Seata就需要回滚一阶段已经执行的业务sql,还原业务数据,回滚方式便是用before image还原业务数据;但在还原前要首先校验脏写,对比数据库当前业务数据和after image,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏鞋,出现脏鞋就需要转人工处理.

分布式事务Seata(nacos集成)_第7张图片

不过因为有全局锁机制,可以降低出现脏鞋的概率

AT模式的一阶段,二阶段提交和回滚都由Seata框架自动完成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

详细架构和流程

Seata中的几个基本概念:

  • TC(Transaction Coordinator) - 事务协调者

    维护全局和分支事务的状态,驱动全局事务提交或回滚(TM之间的协调者)。

  • TM(Transaction Manager) - 事务管理器

    定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM(Resource Manager) - 资源管理器

    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

我们看下面的一个架构图

分布式事务Seata(nacos集成)_第8张图片

  • TM:业务模块中全局事务的开启者
    • 向TC开启一个全局事务
    • 调用其它微服务
  • RM:业务模块执行者中,包含RM部分,负责向TC汇报事务执行状态
    • 执行本地事务
    • 向TC注册分支事务,并提交本地事务执行结果
  • TM:结束对微服务的调用,通知TC,全局事务执行完毕,事务一阶段结束
  • TC:汇总各个分支事务执行结果,决定分布式事务是提交还是回滚;
  • TC 通知所有 RM 提交/回滚 资源,事务二阶段结束。

一阶段:

  • TM开启全局事务,并向TC声明全局事务,包括全局事务XID信息
  • TM所在服务调用其它微服务
  • 微服务,主要有RM来执行
    • 查询before_image
    • 执行本地事务
    • 查询after_image
    • 生成undo_log并写入数据库
    • 向TC注册分支事务,告知事务执行结果
    • 获取全局锁(阻止其它全局事务并发修改当前数据)
    • 释放本地锁(不影响其它业务对数据的操作)
  • 待所有业务执行完毕,事务发起者(TM)会尝试向TC提交全局事务

二阶段:

  • TC统计分支事务执行情况,根据结果判断下一步行为
    • 分支都成功:通知分支事务,提交事务
    • 有分支执行失败:通知执行成功的分支事务,回滚数据
  • 分支事务的RM
    • 提交事务:直接清空before_imageafter_image信息,释放全局锁
    • 回滚事务:
      • 校验after_image,判断是否有脏写
      • 如果没有脏写,回滚数据到before_image,清除before_imageafter_image
      • 如果有脏写,请求人工介入

5.Saga 模式

Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

分布式事务Seata(nacos集成)_第9张图片

适用场景:

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

优势:

  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现

缺点:

  • 不保证隔离性

集成seata

在之前讲解Seata原理的时候,我们就聊过,其中包含重要的3个角色:

  • TC:事务协调器
  • TM:事务管理器
  • RM:资源管理器

其中,TC是一个独立的服务,负责协调各个分支事务,而TM和RM通过jar包的方式,集成在各个事务参与者中。

因此,首先我们需要搭建一个独立的TC服务。

1.安装

首先去官网下载TC的服务端安装包,GitHub的地址:https://github.com/seata/seata/releases

然后解压即可,其目录结构如下:

分布式事务Seata(nacos集成)_第10张图片

包括:

  • bin:启动脚本
  • conf:配置文件
  • lib:依赖项

2.配置

Seata的核心配置主要是两部分:

  • 注册中心的配置:在${seata_home}/conf/目录中,一般是registry.conf文件
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  #  指定注册中心类型,这里使用nacos类型
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "192.168.10.101:8848"
    group = "SEATA_GROUP"
    namespace = "e3aa94a8-47cf-4737-ab96-c664eeb61f8b"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}
config {
  # file、nacos 、apollo、zk、consul、etcd3
  # 配置文件方式,可以支持 file、nacos 、apollo、zk、consul、etcd3
  type = "file"
  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  file {
    name = "file.conf"
  }
}

file.conf文件:

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""
  ## 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"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://192.168.10.101:3306/dev_seata?rewriteBatchedStatements=true"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

但是如果使用db作为存储介质,还需要在数据库中创建3张表:

CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME,
    `gmt_modified`      DATETIME,
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

3.启动

进入${seata_home}/bin/目录中:

分布式事务Seata(nacos集成)_第11张图片

如果是linux环境(要有JRE),执行seata-server.sh

如果是windows环境,执行seata-server.bat

4.业务模块

undo_log

CREATE TABLE `undo_log` (
  `branch_id` bigint(20) 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`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='AT transaction mode undo table'

1.引入依赖:

<dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-seataartifactId>
            <version>2021.1version>
            <exclusions>
                <exclusion>
                    <groupId>io.seatagroupId>
                    <artifactId>seata-spring-boot-starterartifactId>
                exclusion>
            exclusions>
        dependency>

        <dependency>
            <groupId>io.seatagroupId>
            <artifactId>seata-spring-boot-starterartifactId>
            <version>1.4.2version>
        dependency>

2.添加配置文件

首先在application.yml中添加一行配置:

spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: test_tx_group # 定义事务组的名称

这里是定义事务组的名称,接下来会用到。

然后是在resources目录下放两个配置文件:file.confregistry.conf

其中,registry.conf与TC服务端的一样,此处不再讲解。

我们来看下`file.conf

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  vgroup_mapping.test_tx_group = "seata_tc_server"
  #only support when registry.type=file, please don't set multiple addresses
  seata_tc_server.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
     # 二阶段回滚镜像校验    默认true开启,false关闭。
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}

或者yaml配置:

seata:
  enabled: true
  enable-auto-data-source-proxy: true
  tx-service-group: my_test_tx_group
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.10.101:8848
      username: nacos
      password: nacos
  config:
    type: nacos
    nacos:
      server-addr: 192.168.10.101:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
  service:
    vgroup-mapping:
      test_tx_group: default
    disable-global-transaction: false
  client:
    rm:
      report-success-enable: false     

配置解读:

  • transport:与TC交互的一些配置
    • heartbeat:client和server通信心跳检测开关
    • enableClientBatchSendRequest:客户端事务消息请求是否批量合并发送
  • service:TC的地址配置,用于获取TC的地址
    • vgroup_mapping.test_tx_group = "seata_tc_server"
      • test_tx_group:是事务组名称,要与application.yml中配置一致,
      • seata_tc_server:是TC服务端在注册中心的id,将来通过注册中心获取TC地址
      • enableDegrade:服务降级开关,默认关闭。如果开启,当业务重试多次失败后会放弃全局事务
      • disableGlobalTransaction:全局事务开关,默认false。false为开启,true为关闭
    • default.grouplist:这个当注册中心为file的时候,才用到
  • client:客户端配置
    • rm:资源管理器配
      • asynCommitBufferLimit:二阶段提交默认是异步执行,这里指定异步队列的大小
      • lock:全局锁配置
        • retryInterval:校验或占用全局锁重试间隔,默认10,单位毫秒
        • retryTimes:校验或占用全局锁重试次数,默认30次
        • retryPolicyBranchRollbackOnConflict:分支事务与其它全局回滚事务冲突时锁策略,默认true,优先释放本地锁让回滚成功
      • reportRetryCount:一阶段结果上报TC失败后重试次数,默认5次
    • tm:事务管理器配置
      • commitRetryCount:一阶段全局提交结果上报TC重试次数,默认1
      • rollbackRetryCount:一阶段全局回滚结果上报TC重试次数,默认1
    • undo:undo_log的配置
      • dataValidation:是否开启二阶段回滚镜像校验,默认true
      • logSerialization:undo序列化方式,默认Jackson
      • logTable:自定义undo表名,默认是undo_log
    • log:日志配置
      • exceptionRate:出现回滚异常时的日志记录频率,默认100,百分之一概率。回滚失败基本是脏数据,无需输出堆栈占用硬盘空间

3.代理DataSource

Seata的二阶段执行是通过拦截sql语句,分析语义来指定回滚策略,因此需要对DataSource做代理。我们在项目的cn.itcast.order.config包中,添加一个配置类:

package cn.itcast.order.config;

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class DataSourceProxyConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        // 订单服务中引入了mybatis-plus,所以要使用特殊的SqlSessionFactoryBean
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        // 代理数据源
        sqlSessionFactoryBean.setDataSource(new DataSourceProxy(dataSource));
        // 生成SqlSessionFactory
        return sqlSessionFactoryBean.getObject();
    }
}

注意,这里因为订单服务使用了mybatis-plus这个框架(这是一个mybatis集成框架,自动生成单表Sql),因此我们需要用mybatis-plus的MybatisSqlSessionFactoryBean代替SqlSessionFactoryBean

如果用的是原生的mybatis,请使用SqlSessionFactoryBean

或者

seata:
  enabled: true
  enable-auto-data-source-proxy: true

4.添加事务注解

给事务发起者order_serviceOrderServiceImpl中的createOrder()方法添加@GlobalTransactional注解,开启全局事务:

分布式事务Seata(nacos集成)_第12张图片

重新启动即可。

seata的坑

​ Seata AT模式下,二阶段-回滚。会做数据校验。

​ 数据校验:拿 UNDO LOG 中的数据与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。

这个所谓的文档,在官网找了半天,才找到。大概意思是,可以将数据校验功能关闭(默认是打开的)。关闭了就不报错了。

通过设置client端的一个参数,将校验功能关闭:

client.undo.dataValidation 二阶段回滚镜像校验 默认true开启,false关闭。

(PS:不过把这个关闭了,有点草率了吧。直接回滚?中间乱入的数据不处理了,会出问题的。)

可以看出Seata AT模式一阶段执行本地事务的时候,才会锁表/行。一阶段后本地事务就已经提交了,也就释放了锁,别的操作可以更改数据。如果二阶段发生回滚,数据会有异常,需要额外处理(人工介入等等)。反正官网没给出解决方案,所谓的解决方案只是关闭校验。不可行。

无法回滚之后会seata会带有行锁

在这里插入图片描述

回滚日志也不会清除

在这里插入图片描述

错误信息:Cause: io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout

在这里插入图片描述
解决方案:找出程序中对该表有修改操作的方法,添加@GlobalLock添加全局锁
GlobalLock 注解说明
从执行过程和提交过程可以看出,既然开启全局事务 @GlobalTransactional注解可以在事务提交前,查询全局锁是否存在,那为什么 Seata 还要设计多处一个 @GlobalLock注解呢?

因为并不是所有的数据库操作都需要开启全局事务,而开启全局事务是一个比较重的操作,需要向 TC 发起开启全局事务等 RPC 过程,而@GlobalLock注解只会在执行过程中查询全局锁是否存在,不会去开启全局事务,因此在不需要全局事务,而又需要检查全局锁避免脏读脏写时,使用@GlobalLock注解是一个更加轻量的操作。

你可能感兴趣的:(java,spring,cloud,分布式,中间件,java)