Seata概念+使用详解

一、概念

1.在微服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个DML操作,变成了跨进程或者跨数据库的多个事务单元的多个DML操作,而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。

2.分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题,业内常见的解决方法有两种

a. 强一致性,就是所有的事务参与者要么全部成功,要么全部失败,全局事务协调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者回滚!

b. 最终一致性,也叫弱一致性,也就是多个网络节点的数据允许出现不一致的情况,但是在最终的某个时间点会达成数据一致。基于CAP定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以对于数据一致性要求不高的场景,就会采用最终一致性算法。

3.在分布式事务的实现上,对于强一致性,我们可以通过基于XA协议下的二阶段提交来实现,对于弱一致性,可以基于TCC事务模型、可靠性消息模型等方案来实现。

4.市面上有很多针对这些理论模型实现的分布式事务框架,我们可以在应用中集成这些框架来实现分布式事务。而Seata就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易用的分布式事务服务。

二、Seata中封装了四种分布式事务模式

1. XA模式

强一致性,无代码侵入,在一阶段不提交事务,会锁住资源,导致性能低需要依赖数据库的事务特性,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。

RM一阶段:

1)TM开启全局事务

2)TM调用分支RM、RM将分支注册到TC、RM执行SQL(但不提交!)、RM将执行状态报告给TC

TC二阶段:

1)TM提交全局事务

2)TC统计各分支状态,如果都成功,则通知RM提交。如果失败,则通知RM回滚。

2. AT模式

默认模式,弱一致性,是一种基于本地事务+二阶段协议来实现的最终数据一致性方案,基于全局锁隔离,无代码侵入,一阶段提交事务,在提交事务前,会记录undolog日志,性能比XA模式好,二阶段TC通知回滚,则根据undolog回滚,通知提交,则删除undolog日志。

一阶段:TM开启全局事务、TM调用分支、RM注册分支事务、RM记录undolog日志、RM提交事务、TCC记录各分支状态

二阶段:TM通知提交/回滚全局事务、TC检查各分支事务状态,成功,则删除undolog日志,失败,则根据undolog日志回滚。

脏写问题:

如果一个A事务执行sql并提交,另一个B事务也执行提交,此时A事务进行回滚,则会回滚为A记录的undolog日志,而B事务的更新修改记录会被忽略,出现了脏写问题。

解决:引入全局锁,在A事务提价事务释放DB锁之前,申请全局锁,而此时如果B事务进行操作修改,在执行更新数据库操作前会获取全局锁,获取失败,则无法更新,不断重试,但不能一直让其重试,否则A尝试获取B占用的DB锁则会造成死锁,一般让其重试30秒,然后失败则放弃其占有的DB锁,执行失败。A锁此时就能获取DB锁,执行回滚,然后再释放全局锁。

又引来新问题:如果是另一不归seata管理的事务的?全局锁失败!

XA也自动带来了解决的方案:

1)首先记录更新前的记录

2)记录更新后的记录。

完整正确的执行流程如下:

1)原数据假设为100,undolog记录100这个数值。

2)A事务获取DB锁将数据修改为90,此时undolog记录这条90。

3)A事务获取全局锁,并提交事务释放DB数据库锁

4)A事务回滚,在回滚为100前,会比较此时数据是否是90。假设,不归Seata管理的B事务,不需要获取全局锁,然后成功获取DB锁并修改了数据为80,则此时A事务将90(A修改后)与80(B修改)比较,则回滚失败。

3. TCC模式

性能最好,不需要依赖关系型数据库,但代码入侵读高。Try:冻结可用数据,Confirm:确认提交数据,删除冻结数据  Canel:恢复数据,将冻结数据恢复。简单理解就是把一个完整的业务逻辑拆分成三个阶段,然后通过事务管理器在业务逻辑层面根据每个分支事务的执行情况分别调用该业务的Confirm或者Cacel方法。

Try: 判断是否有可用数据,足够则冻结可用数据。

Confirm:  完成资源的操作业务;要求try成功,confirm一定要成功。

Cancel: 预留资源释放,可以理解为try方向操作。

阶段1:检查资源是否足够,足够则冻结资源,执行try方法

阶段2:执行成功,则执行Confirm方法删除冻结资源。执行失败,执行Canel逻辑,恢复冻结资源

4、Saga模式

用于长事务,例如A项目调另外一个公司的项目接口,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者。 


从这四种模型中不难看出,在不同的业务场景中,我们可以使用Seata的不同事务模型来解决不同业务场景中的分布式事务问题,因此我们可以认为Seata是一个一站式的分布式事务解决方案。 

三、四种事务优缺点

XA:强一致性,无代码侵入、但一阶段事务不提交、会锁住资源,导致性能低。需要依赖数据库的事务特性。

AT:默认,弱一致性,无代码侵入,一阶段事务直接提交,失败则根据undolog日志回滚,隔离性引入全局锁,但并发几率低,所以性能会比XA好。

TCC:无需依赖关系型数据库,基于资源预留隔离。try、confirm、canel需要人工手写,而且需要考虑空悬挂、空回滚、幂等性判断,较为复杂、性能最好,但成本太高。

Seaga:适用于长事务类型,无太多应用场景。 

四、部署(SpringCloud + Nacos + Docker + Seata)

(一)Docker配置

1. docker-compose

seata:
  image: seataio/seata-server:1.5.2
  hostname: seata-server
  container_name: seata-server
  restart: always
  ports:
    - "8091:8091"
    - "7091:7091"
  environment:
    - SEATA_PORT=8091
    - SEATA_IP=192.168.52.10
    - SEATA_CONFIG_NAME=file:/root/seata-config/registry
  volumes:
    - /var/docker/server/seata/config:/root/seata-config
    - /var/docker/server/seata/logs:/root/logs
    - /var/docker/server/seata/application.yml:/seata-server/resources/application.yml

2. 新建配置nacos服务端配置文件application.yml

#Seata 服务器配置
server:
  #Seata 服务器的监听端口为 7091。
  port: 7091
#Spring Boot 配置
spring:
  #Spring 应用配置
  application:
    #设置应用名称为 "seata-server"
    name: seata-server
#日志配置
logging:
  #设置日志级别
  level:
    #设置 Nacos 相关包的日志级别为 WARN
    com.alibaba.nacos.client.config.*: WARN
  #指定日志配置文件为 classpath 下的 logback-spring.xml
  config: classpath:logback-spring.xml
  #指定日志文件路径
  file:
    #设置日志文件路径为用户主目录下的 logs/seata 目录
    path: ${user.home}/logs/seata
  #扩展的日志设置
  extend:
    #Logstash 的日志设置
    logstash-appender:
      #Logstash 服务器的地址和端口
      destination: 127.0.0.1:4560
    #Kafka 的日志设置
    kafka-appender:
      #Kafka 服务器的地址和端口
      bootstrap-servers: 127.0.0.1:9092
      #要发送日志的 Kafka 主题
      topic: logback_to_logstash
#Seata 控制台配置
console:
  #控制台用户认证配置
  user:
    username: seata
    password: seata
#Seata 相关配置
seata:
  #配置 Seata 事务分组为 "test_tx_group"
  tx-service-group: test_tx_group
  #Seata 配置中心的配置
  config:
    type: nacos
    nacos:
      server-addr: 192.168.52.10:8848
      namespace: 5d2b98a6-2bf4-4d89-a2a6-a30fb281c2f5
      group: SEATA_GROUP
      data-id: seataServer.properties
  #Seata 注册中心的配置
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.52.10:8848
      group: SEATA_GROUP
      namespace: 5d2b98a6-2bf4-4d89-a2a6-a30fb281c2f5
      cluster: default
  #Seata 控制台安全配置
  security:
    #控制台的密钥
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    #Token 的有效期,单位为毫秒
    tokenValidityInMilliseconds: 1800000
    #需要忽略的 URL 配置
    ignore:
      #指定不需要认证的 URL 列表
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

(二)nacos配置

1. 新建命名空间,id: 5d2b98a6-2bf4-4d89-a2a6-a30fb281c2f5,name: seata,group为:SEATA_GROUP

2. 新建配置seataServer.properties,在seata命名空间下

store.mode=db

store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.52.10:3306/ball_seata?useUnicode=true&rewriteBatchedStatements=true&useSSL=false
store.db.user=root
store.db.password=Yifan123.
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
 
#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
 
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

service.vgroupMapping.test_tx_group=default

(三)MySQL

1. 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=5 DEFAULT CHARSET=utf8;

2. 新建ball_seata数据库,执行下列语句

CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(32) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime(6) DEFAULT NULL,
  `gmt_modified` datetime(6) DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `distributed_lock` ( `lock_key` char(20) NOT NULL, `lock_value` varchar(20) NOT NULL, `expire` bigint(20) DEFAULT NULL, PRIMARY KEY (`lock_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `global_table` ( `xid` varchar(128) NOT NULL, `transaction_id` bigint(20) DEFAULT NULL, `status` tinyint(4) NOT NULL, `application_id` varchar(32) DEFAULT NULL, `transaction_service_group` varchar(32) DEFAULT NULL, `transaction_name` varchar(128) DEFAULT NULL, `timeout` int(11) DEFAULT NULL, `begin_time` bigint(20) DEFAULT NULL, `application_data` varchar(2000) DEFAULT NULL, `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`xid`), KEY `idx_status_gmt_modified` (`status`,`gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `lock_table` ( `row_key` varchar(128) NOT NULL, `xid` varchar(128) DEFAULT NULL, `transaction_id` bigint(20) DEFAULT NULL, `branch_id` bigint(20) NOT NULL, `resource_id` varchar(256) DEFAULT NULL, `table_name` varchar(32) DEFAULT NULL, `pk` varchar(36) DEFAULT NULL, `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` datetime DEFAULT NULL, `gmt_modified` datetime DEFAULT NULL, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid_and_branch_id` (`xid`,`branch_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

(四)启动seata

docker-compose -f docker-compose.yml build seata
docker-compose -f docker-compose.yml up -d seata

1. 启动完成后可以去nacos中查看seata服务是否已注册成功

注意:seata启动会占用两个端口,8091 和 7091 。 其中 8091 是注册到nacos中的服务端端口,7091是其客户端端口。7091对应页面如下图:

访问地址:xx.xx.x.xxx:7091    账号:   seata/seata

四、Java集成(SpringCloud + Nacos + Docker + Seata)

1. pom,所有相关业务模块均引入

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-commons</artifactId>
    <version>2021.0.4.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2021.0.4.0</version>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-commons</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2. yml,所有相关业务模块均引入

#分布式事务
seata:
  enabled: true
  #启用自动代理数据源,它会自动对数据源进行代理以实现分布式事务的管理。
  enableAutoDataSourceProxy: true
  #数据源代理模式使用AT模式(默认使用AT模式)
  data-source-proxy-mode: AT
  #配置事务分组,用于标识事务所属的分组。要与客户端配置的事务分组一致。
  tx-service-group: 'test_tx_group'
  #配置 Seata 服务的相关信息,包括事务分组映射。
  service:
    #配置事务分组映射表,将事务分组映射到实际的 Seata 服务集群。
    vgroup-mapping:
      #将名为 "test_tx_group" 的事务分组映射到名为 "default" 的 Seata 服务集群。
      test_tx_group: 'default'
      #配置 Seata 服务集群的地址。
      grouplist:
        #将名为 "seata-server" 的服务映射到地址 "192.168.52.10:8091"。
        seata-server: 192.168.52.10:8091
  #配置 Seata 使用的注册中心,这里是 Nacos。
  registry:
    type: nacos
    nacos:
      #Nacos 服务器地址
      server-addr: 192.168.52.10:8848
      #Nacos 分组名称
      group: SEATA_GROUP
      #Nacos 命名空间 ID
      namespace: 5d2b98a6-2bf4-4d89-a2a6-a30fb281c2f5
      #Seata 在 Nacos 注册的应用名
      application: seata-server
      #集群名称
      cluster: 'default'
  #配置 Seata 使用的配置中心,这里同样是 Nacos。
  config:
    type: nacos
    nacos:
      #Nacos 服务器地址
      server-addr: 192.168.52.10:8848
      #Nacos 分组名称
      group: SEATA_GROUP
      #Nacos 命名空间 ID
      namespace: 5d2b98a6-2bf4-4d89-a2a6-a30fb281c2f5
      #要读取的配置文件在 Nacos 上的 ID,这里是 seataServer.properties。
      data-id: seataServer.properties

3. java

//开启全局事务注解
@GlobalTransactional(rollbackFor = Exception.class)
public boolean saveTrading(Trading trading) throws Exception {
    //输出全局事务ID
    System.out.println("xid_order1:" + RootContext.getXID());
    //1. 保存数据
    super.save(trading);
    //2. Feign更新数据(调用方法会报错,然后全局回滚)
    RoleDTO roleDTO = new RoleDTO();
    roleDTO.setName("dddddddddddddddddddddddd");
    Boolean a = ruleFeign.create(roleDTO);
    return true;
}

feign

@Component
@FeignClient(value = "sys")
public interface RuleFeign {
    @RequestMapping(value = "/role/addDTO", method = RequestMethod.POST)
    Boolean create(RoleDTO roleDTO);
}

(四)调用

启动服务,调用接口,报错即全局回滚

注意:各业务库中 undo_log 表中的数据只能在程序断点的时候看的到,因为seata执行完一次全局事务(提交或回滚)之后,会删掉undo_log表中对应XID的数据(回滚脚本)。 

五、可能出现的问题

1、实例场景

A、B、C 三个服务。A 服务通过两个feign接口请求调用 B 和 C 的服务往数据库中插入数据。具体代码不做演示,A服务测试代码如下(方法上添加分布式事务注解:@GlobalTransactional ):

由于在服务中设置了feign接口异常回调(服务降级策略),吃掉了抛出的异常,导致seata全局事务回滚失效。

2、解决方法

a. 在异常回调中手动添加事务回滚

b. AOP手动回滚事务

@Aspect
@Component
public class GlobalTransactionalAspect {
    private final static Logger logger = LoggerFactory.getLogger(GlobalTransactionalAspect.class);

    /**
     * 用于处理feign降级后无法触发seata的全局事务的回滚
     * 过程:拦截*FallbackFactory降级方法,只要进了这个方法就手动结束seata全局事务
     */
    @After("execution(* com.chain.feign..*FallbackFactory.*(..))")
    public void before(JoinPoint joinPoint) throws TransactionException {
        if (!StringUtils.isBlank(RootContext.getXID())) {
            String className = joinPoint.getTarget().getClass().getSimpleName();
            String methodName = joinPoint.getSignature().getName();
            logger.info("\n===========>降级后进行全局事务手动回滚,class:" + className + ",method:" + methodName + ",全局事务XID:" + RootContext.getXID());
            GlobalTransactionContext.reload(RootContext.getXID()).rollback();
        }
    }
}

 

posted @ 2023-08-12 18:59  yifanSJ  阅读(350)  评论(0编辑  收藏  举报