Springboot微服务整合Seata分布式事务二:客户端接入

Seata分布式事务微服务接入

一、初始化SQL

涉及到业务库都要新建:undo_log表

CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) 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 = utf8 COMMENT ='AT transaction mode undo table';

二、项目配置

依赖 ,目前和Seata Server服务端保持版本一致:1.3.0


<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-alibaba-seataartifactId>
    <version>2.2.0.RELEASEversion>
    <exclusions>
        <exclusion>
            <groupId>io.seatagroupId>
            <artifactId>seata-spring-boot-starterartifactId>
        exclusion>
        <exclusion>
            <groupId>io.seatagroupId>
            <artifactId>seata-allartifactId>
        exclusion>
    exclusions>
dependency>
<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.3.0version>
dependency>

    
    
    

application配置

seata:
  # 开启Springboot自动装配 默认true
  enabled: true
  # 开启数据源自动代理 默认true
  enable-auto-data-source-proxy: true
  # app id
  application-id: ${
     spring.application.name}
  # 事务组(可以每个应用独立取名,也可以使用相同的名字,注意跟配置文件保持一致)
  tx-service-group: ${
     spring.application.name}
  # 配置中心
  config:
    type: apollo
    apollo:
      app-id: ${
     spring.application.name}
      #apollo-meta: http://10.0.17.92:9083
      apollo-meta: ${
     apollo.meta}
      namespace: seata
  # 注册中心
  registry:
    type: eureka
    eureka:
      weight: 1
      application: seata-server
      #service-url: http://10.0.17.92:9001/eureka/
      # eureka.service.ip-address 通过配置注入
      service-url: ${
     eureka.service.ip-address}

Apollo seata命名空间配置

# transport配置
transport.type = TCP
transport.server = NIO
transport.heartbeat = true
# 客户端事务消息请求合并发送 默认true
transport.enableClientBatchSendRequest = true
transport.compressor = none
transport.shutdown.wait = 3
transport.serialization = seata
transport.threadFactory.bossThreadPrefix = NettyBoss
transport.threadFactory.workerThreadPrefix = NettyServerNIOWorker
transport.threadFactory.serverExecutorThread-prefix = NettyServerBizHandler
transport.threadFactory.shareBossWorker = false
transport.threadFactory.clientSelectorThreadPrefix = NettyClientSelector
transport.threadFactory.clientSelectorThreadSize = 1
transport.threadFactory.clientWorkerThreadPrefix = NettyClientWorkerThread
transport.threadFactory.bossThreadSize = 1
transport.threadFactory.workerThreadSize = default

# client配置
# 异步提交缓存队列长度(默认10000)
client.rm.asyncCommitBufferLimit = 10000
# 一阶段结果上报TC重试次数(默认5)
client.rm.reportRetryCount = 5
# 自动刷新缓存中的表结构(默认false)
client.rm.tableMetaCheckEnable = false
# 是否上报一阶段成功(默认false),true用于保持分支事务生命周期完整
client.rm.reportSuccessEnable = false
# 校验或占用全局锁重试间隔(默认10ms)
client.rm.lock.retryInterval = 10
# 校验或占用全局锁重试次数(默认30)
client.rm.lock.retryTimes = 30
# 分支事务与其它全局事务回滚事务冲突时策略(优先释放本地锁让回滚)
client.rm.lock.retryPolicyBranchRollbackOnConflict = true
# 一阶段全局回滚上报TC重试次数(默认1次,建议大于1)
client.tm.rollbackRetryCount = 3
# 一阶段全局提交上报TC重试次数(默认1次,建议大于1)
client.tm.commitRetryCount = 3
# undo序列化方式(默认jackson)
client.undo.logSerialization = jackson
# 自定义undo_log表明(默认undo_log)
client.undo.logTable = undo_log
# 二阶段回滚镜像校验(默认true开启)
client.undo.dataValidation = false
# 日志异常输出概率(默认100)
client.log.exceptionRate = 100

# service配置
# seata-server服务端地址 仅在store.mode=file时生效
service.default.grouplist = 10.0.17.92:8091
# 各自项目需配置自己的 service.vgroupMapping.事务组 , seata-server为服务端事务组
service.vgroupMapping.claim-workflow = seata-server
# 开启降级,业务连续出错则不使用事务。默认false
service.enableDegrade = false
# 禁用全局事务(默认false)
service.disableGlobalTransaction = false

三、手动注入数据源

Seata 1.3.0版本,已经实现自动数据代理注入。

  • 参数:seata.enable-auto-data-source-proxy=true ,默认为true
  • Seata 1.3.0版本后,不需要手动注入数据源(即以下代码)。
@Configuration
public class DataSourceProxyConfig {
     

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
     
        return new DruidDataSource();
    }
}
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class DataSourceProxyConfig {
     

    //@Value("${mybatis-plus.mapper-locations}")
    //private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
     
        return new DruidDataSource();
    }

    //@Primary
    //@Bean
    //public DataSourceProxy dataSourceProxy(DataSource dataSource) {
     
    //    return new DataSourceProxy(dataSource);
    //}

    //代理mybaits-plus
    //@Bean
    //public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSourceProxy) throws Exception {
     
    //    MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
    //    sqlSessionFactoryBean.setDataSource(dataSourceProxy);
    //    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
    //    sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
    //    return sqlSessionFactoryBean;
    //}

    //代理mybatis / mybatis-config
    //@Bean
    //public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
     
    //    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    //    sqlSessionFactoryBean.setDataSource(dataSourceProxy);
    //    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
    //    sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
    //    return sqlSessionFactoryBean.getObject();
    //}

}

四、事务使用

@GlobalTransactional可以自定义名称和超时时间,默认超时时间:60秒

@GlobalTransactional(timeoutMills = 60000,rollbackFor = Exception.class)
public Result submitById(ReportDispatchingCommand command, UserLoginDTO userLoginDTO){
     
    //业务逻辑
}
  • 注意事项: 业务流程中如果涉及到先插入修改插入数据的流程,要保证所有的改库动作都加@GlobalTransactional注解

像这样,test2的接口加了@GlobalTransactionaltest3的接口没有加@GlobalTransactional
如果执行顺序是: test2执行更新 -> test3执行更新 -> test2回滚
这样就会由于test3执行了更新,导致回滚test的时候,seata认为有脏数据,就不回滚了。
要避免这种情况就需要给test3的方法也加上 @GlobalTransactional的注解

// service A
@GetMapping("test2")
@GlobalTransactional
public void test2() {
     
    bService.doInsertAndUpdate();
}

// service A
// 注意这里没有加@GlobalTransactional
@GetMapping("test3")
public void test3() {
     
    bService.doInsertAndUpdate();
}

五、注意事项

1. pom.xml版本冲突问题

spring-cloud-alibaba-seata中自动依赖了seata-spring-boot-starter。但是版本对应不上,所以要处理版本不兼容问题

Springboot微服务整合Seata分布式事务二:客户端接入_第1张图片

2. Could not register branch into global session异常

  • 异步调用不不加入全局事务,异步改为MQ调用
这里很重要
异常:Could not register branch into global session xid = status = Rollbacked(还有              Rollbacking、AsyncCommitting等等二阶段状态) while expecting Begin

描述:分支事务注册时,全局事务状态需是一阶段状态begin,非begin不允许注册。
     属于seata框架层面正常的处理,用户可以从自身业务层面解决。

出现场景(可继续补充)
1. 分支事务是异步,全局事务无法感知它的执行进度,全局事务已进入二阶段,该异步分支才来注册
2. 服务a rpc 服务b超时(dubbo、feign等默认1秒超时),a上抛异常给tm,tm通知tc回滚,但是b还是收到了请求   (网络延迟或rpc框架重试),然后去tc注册时发现全局事务已在回滚
3. tc感知全局事务超时(@GlobalTransactional(timeoutMills = 默认60秒)),主动变更状态并通知各分支事务    回滚,此时有新的分支事务来注册
# 设置RPC调用的超时时间
ribbon:
  # 连接超时
  ConnectTimeout: 5000
  # 响应超时  设置超时时间设置长一点。应为回滚需要时间
  ReadTimeout: 10000

3. @GlobalTransactional 全局事务不生效

事务是基于接口或基于类的代理被创建的。事务注解,没有加在方法入口,事务无法生效

@ Service
public class Service1 {
     
  @ Autowired
  private Service1 self;

  public void m1() {
     
      // 类内部直接调用,不会触发AOP
      //this.m2(); 
      //通过维护一个自己的bean来调用,从而使事务生效
      self.m2();
  }

  @ GlobalTransactional
  publiv void m2() {
     
  }
}

4. 引入 Seata后,spring本地事务注解@Transactional无效

  • 是否所有sql都走DataSourceProxy代理;而DataSourceProxy代理是直接commit代码;所以只能使用@GlobalTransactional才能执行undo 回滚
  • 所以Seata就不支持 spring @Transactional的本地事务了?

​ 待验证 。。。

六、原理分析

Spring事务特性

  • 事务的隔离级别:是指若干个并发的事务之间的隔离程度
1. @Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读, 不可重复读) 基本不使用
2. @Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读)
3. @Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读)
4. @Transactional(isolation = Isolation.SERIALIZABLE):串行化
  • 事务传播行为:如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为
1. TransactionDefinition.PROPAGATION_REQUIRED:   如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:   创建一个新的事务,如果当前存在事务,则把当前事务挂起。
3. TransactionDefinition.PROPAGATION_SUPPORTS:   如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:   以非事务方式运行,如果当前存在事务,则把当前事务挂起。
5. TransactionDefinition.PROPAGATION_NEVER:   以非事务方式运行,如果当前存在事务,则抛出异常。
6. TransactionDefinition.PROPAGATION_MANDATORY:   如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
7. TransactionDefinition.PROPAGATION_NESTED:   如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;   如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

Seata框架设计

Springboot微服务整合Seata分布式事务二:客户端接入_第2张图片

Seata框架的主要角色

  • (Transaction ID)XID 全局唯一事务ID

  • (Transaction Manager) TM 事务管理器

  • (Resource Manager ) RM 资源管理器

  • (Transaction Coordinator) TC 事务协调器

事务管理器:核心模块为全局事务管理器,主要负责:全局事务的开启、提交、回滚。

资源管理器:执行具体的分支事务,由对应的执行器去执行。包括重做日志,SQL的执行;

​ 实际的SQL由连接代理ConnectionProxy执行,连接代理ConnectProxy会注册事务分支到事务 协调器TC,执行完实际SQL后,上报分支事务的状态给事务协调器TC。

事务协调器:根据事务管理器TM的全局事务的开启、提交、回滚请求,创建全局事务,驱动分支事务的提交 和回滚,并维护全局事务和分支事务的状态。

  • 执行流程

Springboot微服务整合Seata分布式事务二:客户端接入_第3张图片

全局事务与分支事务

  • 全局事务: 通过XID(全局唯一事务ID) 来标识

xid = 10.0.18.202:8905:138030304412573696

  • 分支事务:通过分支事务ID和资源ID来标识,每个分支事务都有XID来标识属于哪个全局事务

branchId = 138030306228707329

Register branch successfully, xid = 10.0.18.202:8905:138030304412573696, branchId = 138030306228707329, resourceId = jdbc:mysql://10.0.18.200:3306/claim_auprocess ,lockKeys = c_au_image_detail_info:340489328012288
[2m2021-05-18 13:23:09.581[0;39m [32m INFO[0;39m [2m---[0;39m [2m[LoggerPrint_1_1][0;39m [36mi.s.c.r.p.server.BatchLogHandler        [0;39m [2m:[0;39m SeataMergeMessage xid=10.0.18.202:8905:138030304412573696,branchType=AT,resourceId=jdbc:mysql://10.0.18.200:3306/claim_auprocess,lockKey=c_au_image_detail_info:340489328012289

1. Seata-clientSeata-server 是如何通信的

Springboot微服务整合Seata分布式事务二:客户端接入_第4张图片

1. seata-servaer 向注册中心注册
2. seata-client 向注册中心注册
3. seata-client 通过注册中心返回的seata-server服务列表,找到seata-server
  • 注册中心的分类
1. file 
  文件类型:用于做概念验证,目的是通过配置文件,让client快速找到seata-server,从而免去依赖第三方注册中心

Springboot微服务整合Seata分布式事务二:客户端接入_第5张图片

2. 非file类型
   nacos
   apollo
   zookeeper
   redis
   etcd 
   ....

2. Seata分布式事务的基础 DataSource

  • 一切的核心都在 io.seata.rm.datasource.DataSourceProxy

  • 从 0.9 版本后,已经实现自动数据代理注入,默认开启。

     ```yaml
    

seata:

开启Springboot自动装配 默认true

enabled: true

开启数据源自动代理 默认true

enable-auto-data-source-proxy: true
```

我们只要把DataSourceProxy注册成默认的 java.sql.DataSource实现,并提供给其它框架(mybatis\jdbctemplate)装配,就达到目的。所以client端的一切配置都围绕着把DataSourceProxy注册成默认的  java.sql.DataSource,或者把数据库访问框架的Datasource配置成DataSourceProxy来进行的。

3. 锁管理器 LockManager

锁管理器的顶级接口:LockManager, 锁管理器必须实现该接口。

Lock_key格式 :数据库资源^^表名^^主键ID

示例:jdbc:mysql://10.0.17.xxx:3306/xxxx^^^act_ge_bytearray^^^341269355654272

七、参考文档

  • 参数配置文档

spring cloud seata 参数配置

Seata解析-TC端file.conf文件各配置作用总结

  • 回滚失败

被调用方存在两个本地事务,在调用方抛异常时,被调用方重复出现Branch Rollbacked faild

压力测试一段时间后,tm端rpc调用server端超时 RPC timeout:RPC timeout

  • 源码分析

Seata源码解析

  • seata springboot 整合

微服务分布式事务解决方案-springboot整合分布式seata1.3.0

分布式事务两阶段提交 Eureka+Seata方案

springcloud-nacos-seata 实现分布式事务

  • 项目异常问题

Could not commit JDBC transaction; nested exception is io.seata.rm.datasource.exec.LockConflictException: get global lock fail

  • 怀疑是竞争锁导致的。所以修改seata服务的参数
    • client.rm.lock.retryInterval 20 校验或占用全局锁重试间隔 默认10,单位毫秒
    • client.rm.lock.retryTimes 60 校验或占用全局锁重试次数 默认30

seata 提交全局事务失败 Unable to commit against JDBC Connection

  • 博客分享

又见分布式事务之阿里开源Seata

  • 事务不生效

seata 分布式事务不生效

你可能感兴趣的:(分布式事务,分布式事务,Springboot,apollo)