Seata分布式事务保姆级教程

Source

初识Seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 为提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案,这四种模式的具体解释可以查看官方文档
下载地址:https://github.com/seata/seata/releases/download
官方文档:http://seata.io/zh-cn/docs/user/quickstart.html
 

Seata基本使用

一、检查环境

因为之后的设置可能要配置注册中心和配置中心,所以如果需要用到要提前准备好,我这里的注册中心用的Nocos,配置中心用的file.conf

二、引入依赖

这里注意版本问题

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.2.0</version>
</dependency>

三、YML配置文件

seata.enabled属于1.0新特性,可以不用引入file.conf 和 registry.conf,但我这里使用的是引入文件的方式

# 其他配置项省略
spring:
    # db相关配置
    datasource:
        # 是否开启seata事务
        seata: true
    alibaba:
      # 分布式事务
      seata:
        # 激活自动配置,使得我们可以在yaml/properties文件中配置
        enabled: true
        # enable-auto-data-source-proxy: true
        # 事务分组名
        tx-service-group: my_test_tx_group

四、创建undo_log表

SEATA AT模式需要undo_log表,所以在你自己的数据库中创建该表保证事务正常运行

-- mysql 建表语句
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT 'increment id',
    `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     NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME     NOT NULL COMMENT 'modify datetime',
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

五、引入文件

resources目录下创建 registry.conf和file.conf 两个文件(classpath路径下)

1. 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"
    namespace = ""
    # seata-server在nacos的集群名
    cluster = "default"
    username = ""
    password = ""
  }
  eureka {
    
      
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    
      
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    
      
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    
      
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    
      
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    
      
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    
      
    name = "file.conf"
  }
}


# 配置中心
config {
    
      
  # file、nacos 、apollo、zk、consul、etcd3
  #type = "nacos"
  type = "file"
  # 使用nacos作为配置中心
  nacos {
    
      
    serverAddr = "127.0.0.1:8848"
    # nacos命名空间id,""为nacos保留public空间控件,用户勿配置namespace = "public"
    namespace = ""
    group = "SEATA_GROUP"
    # group = "DEFAULT_GROUP"
    username = "nacos"
    password = "nacos"
    # cluster = "default"
  }
  consul {
    
      
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    
      
    appId = "seata-server"
    apolloMeta = "http://127.0.0.1:8801"
    namespace = "application"
  }
  zk {
    
      
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    
      
    serverAddr = "http://localhost:2379"
  }

  # 使用file.conf作为客户端配置解析
  file {
    
      
    name = "file.conf"
  }
}
2. file.conf(具体的配置)
# 事务分组配置,TC的地址配置,用于获取TC的地址
service {
    
      
  # 注意 my_test_tx_group 为我们这里定义的事务的分组名
  # 后面为registry.conf中使用的注册中心的application
  # "default"是TC服务端集群的名称,将来通过注册中心获取TC地址
  # 1.1.0版本以下是vgroup_mapping
  vgroupMapping.my_test_tx_group = "default"
  
  default.grouplist = "127.0.0.1:8091"

  # 服务降级,默认关闭,如果开启,当业务重试多次失败后会放弃全局事务
  enableDegrade = false

  # 全局事务开关,默认false。false为开启,true为关闭
  disableGlobalTransaction = false
}


## 事务日志存储,仅用于seata服务器
store {
    
      
  ## 存储方式:file和db,file在文件中存储,db在数据库中存储
  mode = "file"

  ## file store property
  file {
    
      
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    
      
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) 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 = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}


client {
    
      
  # 资源管理器配置
  rm {
    
      
    # 二阶段提交默认是异步执行,这里指定异步队列的大小
    asyncCommitBufferLimit = 10000

    # 全局锁配置
    lock {
    
      
      # 校验或占用全局锁重试间隔,默认10,单位毫秒
      retryInterval = 10
      # 校验或占用全局锁重试次数,默认30次
      retryTimes = 30
      # 分支事务与其它全局回滚事务冲突时锁策略,默认true,优先释放本地锁让回滚成功
      retryPolicyBranchRollbackOnConflict = true
    }
    # 一阶段结果上报TC失败后重试次数,默认5次
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }

  # 事务管理器配置
  tm {
    
      
    # 一阶段全局提交结果上报TC重试次数,默认1
    commitRetryCount = 5
    # 一阶段全局回滚结果上报TC重试次数,默认1
    rollbackRetryCount = 5
  }

  # undo_log的配置
  undo {
    
      
    # 是否开启二阶段回滚镜像校验,默认true
    dataValidation = true
    # undo序列化方式,默认Jackson
    logSerialization = "jackson"
    # 自定义undo表名,默认是undo_log
    logTable = "undo_log"
  }

  # 日志配置
  log {
    
      
    # 出现回滚异常时的日志记录频率,默认100,百分之一概率。回滚失败基本是脏数据,无需输出堆栈占用硬盘空间
    exceptionRate = 100
  }
}

# 与TC交互的配置
transport {
    
      
  # tcp udt unix-domain-socket
  type = "TCP"

  #NIO NATIVE
  server = "NIO"

  # client和server通信心跳检测开关,默认true开启
  heartbeat = true

  # 客户端事务消息请求是否批量合并发送
  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
  }

  # client和server通信编解码方式,seata(ByteBuf)、protobuf、kryo、hession、fst,默认seata
  serialization = "seata"

  # client和server通信数据压缩方式,none、gzip,默认none
  compressor = "none"
}

六、代理数据源

使用Seata对数据源进行代理,需要注意的是,如果是比较高的版本(1.0)不需要我们去手动配置代理数据源

@Configuration
public class Config {
    
      

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource(){
    
      
        return new DruidDataSource();
    }
    @Bean
    @Primary
    public DataSourceProxy dataSourceProxy(DruidDataSource dataSource) {
    
      
        return new DataSourceProxy(dataSource);
    }
    
    /**
     * 使用mybatis
     */
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
    
      
        // 因为使用的是mybatis,这里定义SqlSessionFactoryBean
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 配置数据源代理
        sqlSessionFactoryBean.setDataSource(new DataSourceProxy(dataSource));
        return sqlSessionFactoryBean.getObject();
    }
    
    /**
     * 使用mybatis-plus
     */
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
    
      
        // 业务中引入了mybatis-plus,所以要使用特殊的SqlSessionFactoryBean
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        // 代理数据源
        sqlSessionFactoryBean.setDataSource(new DataSourceProxy(dataSource));
        // 生成SqlSessionFactory
        return sqlSessionFactoryBean.getObject();
    }
    
}

六、测试

简单测试一下,使用注解@GlobalTransactional

1. 在业务实现类的方法上加上 @GlobalTransactional(rollbackFor=Exception.class)
2. 方法内部业务操作之后人为造个异常
3. 运行方法查看业务操作是否回滚

 
 
 

可能的问题:

问题一:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [io.seata.spring.annotation.GlobalTransactionScanner]
Caused by: io.seata.common.exception.NotSupportYetException: not support register type: null

解决: 如果没在classpath下创建两个文件会出现异常

 

问题二:

io.seata.common.exception.FrameworkException: can not register RM,err:can not connect to services-server.
Caused by: io.seata.common.exception.FrameworkException: connect failed, can not connect to services-server.

解决: 启动程序前需要先启动seata服务端

 

问题三:

控制台不停打印 no available service ‘default’ found, please make sure registry config correct

解决: 1. 首先确认文件中事务分组名是否能对应上
       2.看file.conf中使用的是vgroup_mapping还是vgroupMapping,因为1.1.0版本之下是vgroup_mapping,1.1.0版本开始修改为vgroupMapping
       3.修改YML配置文件enabled属性

spring:
    alibaba:
      seata:
        enabled: true

 

问题四:

控制台不停打印 com.alibaba.nacos.client.naming

解决: 设置指定的日志输出级别

logging:
  level:
    com:
      alibaba:
        nacos:
          client:
            naming: warn

 

问题五:

Seata怎样用我们创建的undo_log表的

解决: undo_log表是事务用来回滚的表,在回滚后控制台可以看到的事务完成后undo_log日志被删除掉了(undo_log deleted with GlobalFinished)。如果程序执行完再看,一般就是空表,所以要想查看表中数据,就要在操作完数据库和自定义异常的中间debug查看