分布式事务 Seata 业务系统集成

分布式事务 Seata 业务系统集成

  • 业务系统集成 - Client端(TM,RM) ,使用 seata 0.9 实现分布式事务

    • 先参考seata-samples 项目,完成一个 dubbo项目,如:my demo

    • 添加pom依赖

      <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>1.1.0</version>
      </dependency>
      <!--1. dubbo 依赖请参见上面 Spring boot 2.1.9 + Dubbo 2.7.3 + Nacos 1.1.4 -->
      <!--2. druid 依赖和 mybatis 依赖请参见 MySQL.md-->
      
    • 建表: undo_log ,SQL语句在源码的script --> client -->at–> db目录下

      -- for AT mode you must to init this sql for you business database. the seata server not need it.
      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';
      
    • 配置参数:将file.conf和registry.conf复制到 spring boot工程resource 目录下

      • file.conf和registry.conf 文件位置在源码的script --> client -->config 目录下

      • 如果以file形式注册,默认配置不用改

      • 但是file.conf有一处要根据服务名来修改

        vgroupMapping.xxx_tx_group = "default"
        # xxx表示你的spring.application.name
        
      • 问题:如果忘记复制file.conf和registry.conf 文件,会报错: NotSupportYetException: not support register type: null

      • 注意:pom 依赖 seata-all , 不能改成 seata-spring-boot-starter ,否则无法识别 file.conf和registry.conf 文件

    • 配置数据源代理,支持自动和手动两种配置。

      // 开启自动配置:使用注解 @EnableAutoDataSourceProxy 开启数据源代理
      
      // 手动配置:需要将业务数据源放到代理数据源中,另外mybatis的SqlSessionFactory也需要手动配置,将代理数据源写入mybatis的SqlSessionFactory中
      @Bean("dataSource")
      public DataSourceProxy dataSource(DataSource druidDataSource) {
          return new DataSourceProxy(druidDataSource);
      }
      // 将代理数据源写入mybatis的SqlSessionFactory中
      @Bean
      public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
          SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
          factoryBean.setDataSource(dataSourceProxy);
          factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                                       .getResources("classpath*:/mappers/*.xml"));
          factoryBean.setTransactionFactory(new JdbcTransactionFactory());
          return factoryBean.getObject();
      }
      
    • 初始化 GlobalTransactionScanner - 全局事务扫描器 , 也支持自动和手动两种

      // 自动配置: 引入seata-spring-boot-starter 即可
      
      // 手动配置
      public GlobalTransactionScanner globalTransactionScanner() {
        String applicationName = this.applicationContext.getEnvironment()
          .getProperty("spring.application.name");
        
        String txServiceGroup = this.seataProperties.getTxServiceGroup();
        
        if (StringUtils.isEmpty(txServiceGroup)) {
          txServiceGroup = applicationName + "-fescar-service-group";
          this.seataProperties.setTxServiceGroup(txServiceGroup);
        }
      
        return new GlobalTransactionScanner(applicationName, txServiceGroup);
      }
      
    • 实现xid跨服务传递 , 似乎 1.1.0 版本内部已经实现了xid传递

      • Seata上的注解@Activate(group = {Constants.PROVIDER, Constants.CONSUMER}, order = 100),表示dubbo的服务提供方跟消费方都会触发到这个过滤器,所以我们的Seata发起者会产生一个XID的传递
    • 在需要分布式事务的方法上添加@GlobalTransactional注解,就这么简单。

      import org.springframework.stereotype.Service;
      
      @Service
      public class BusinessServiceImp implements BusinessService {
          @Reference(version = "0.0.1")
          private StorageService storageService;
          @Reference(version = "0.0.1")
          private OrderService orderService;
          
          //通过@GlobalTransactional注解的方法会走拦截器GlobalTransactionalInterceptor的执行逻辑
          @GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example")
          @Override
          public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
              System.out.println("开始全局事务,XID = " + RootContext.getXID());
              ObjectResponse<Object> objectResponse = new ObjectResponse<>();
              //1、扣减库存
              ObjectResponse response1 = storageService.decreaseStorage(commodityDTO);
              //2、创建订单
              ObjectResponse<OrderDTO> response2 = orderService.createOrder(orderDTO);
              return objectResponse;
          }
      }
      
    • 为什么只要添加一个 @GlobalTransactional注解 就可以了呢?请看RM原理

      • Phase 1 ( 1阶段)

        • 当前本地事务是否处于全局事务中(也就判断ConnectionContext中的 xid 是否为空)
        • 如果不处于全局事务中,但是有全局事务锁(即方法标注了@GlobalLock注解),则在全局事务锁中提交本地事务。
        • 以上情况都不是,则调用targetConnection对本地事务进行 commit。
        • 如果处于全局事务中,首先创建分支事务,再将ConnectionContext中的 UndoLog 写入到undo_log表中,然后调用targetConnection对本地事务进行 commit,将 UndoLog 与业务 SQL 一起提交,最后上报分支事务的状态(成功 or 失败),并将ConnectionContext上下文重置。

      • Phase 2 (2阶段)

        • 提交

        • 回滚

  • 在项目使用 seata-spring-boot-starter 依赖

    • 添加 pom 依赖

      <dependency>
          <groupId>io.seata</groupId>
          <artifactId>seata-spring-boot-starter</artifactId>
          <version>1.1.0</version>
      </dependency>
      
    • 删除 file.conf和registry.conf 文件

    • 删除 手动配置数据源代理 和 手动 初始化的 GlobalTransactionScanner 相关代码,这些都自动完成了

    • 在 properties 文件中添加 seata 配置 , spring 相关配置在源码的script --> client -->spring 目录下

      #================seata config=======================
      seata.enabled=true
      seata.application-id=dubbo-account-example
      seata.tx-service-group=my_test_tx_group
      seata.enable-auto-data-source-proxy=true
      seata.use-jdk-proxy=false
      
      # ----- begin: 不在源码的默认配置里面 , 属于 client和Server的公共配置--------- 
      # 使用 nacos 注册类型,通过nacos 发现 seata-server 
      seata.registry.type=nacos
      seata.registry.nacos.server-addr=112.74.179.118:8848
      seata.registry.nacos.cluster=default
      seata.registry.nacos.namespace=9a6af877-1cd4-4450-9eff-7ba6252fca8e
      # 使用 nacos 配置中心
      seata.config.type=nacos
      seata.config.nacos.server-addr=112.74.179.118:8848
      seata.config.nacos.namespace=9a6af877-1cd4-4450-9eff-7ba6252fca8e
      seata.config.nacos.group=SEATA_GROUP
      # ----- end: 不在源码的默认配置里面 --------- 
      
      seata.service.vgroup-mapping.my_test_tx_group=default
      seata.service.enable-degrade=false
      seata.service.disable-global-transaction=false
      # 只会在 register.type = file 时,才会被使用,其他类型不会读取
      seata.service.grouplist.default=127.0.0.1:6091
      
      seata.client.rm.async-commit-buffer-limit=1000
      seata.client.rm.report-retry-count=5
      seata.client.rm.table-meta-check-enable=false
      seata.client.rm.report-success-enable=false
      seata.client.rm.lock.retry-interval=10
      seata.client.rm.lock.retry-times=30
      seata.client.rm.lock.retry-policy-branch-rollback-on-conflict=true
      seata.client.tm.commit-retry-count=5
      seata.client.tm.rollback-retry-count=5
      seata.client.undo.data-validation=true
      seata.client.undo.log-serialization=jackson
      seata.client.undo.log-table=undo_log
      seata.client.log.exceptionRate=100
      
      seata.transport.type=TCP
      seata.transport.server=NIO
      seata.transport.heartbeat=true
      seata.transport.serialization=seata
      seata.transport.compressor=none
      seata.transport.enable-client-batch-send-request=true
      seata.transport.shutdown.wait=3
      seata.transport.thread-factory.boss-thread-prefix=NettyBoss
      seata.transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker
      seata.transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler
      seata.transport.thread-factory.share-boss-worker=false
      seata.transport.thread-factory.client-selector-thread-prefix=NettyClientSelector
      seata.transport.thread-factory.client-selector-thread-size=1
      seata.transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread
      seata.transport.thread-factory.worker-thread-size=default
      seata.transport.thread-factory.boss-thread-size=1
      
    • 注意: 如果要修改 my_test_tx_group ,需要修改两个地方,否则会报错:no available service 'null' found, please make sure registry config correct

      seata.tx-service-group=my_test_tx_group
      seata.service.vgroup-mapping.my_test_tx_group=default
      
    • 上面关于 seata的配置,也可以放到 nacos 配置中心

      • 在nacos 配置中新建一个 nacos-seata-config.properties 配置文件,将上面的那么多配置copy进来

      • 启用 配置文件

        @SpringBootApplication
         // 自动加载配置资源
        @NacosPropertySource(dataId = "dubbo-demo-api", autoRefreshed = true)
        @NacosPropertySource(dataId = "nacos-seata-config.properties", autoRefreshed = true)
        public class NacosConfigApplication {
            public static void main(String[] args) {
                SpringApplication.run(NacosConfigApplication.class, args);
          }
        }
        
  • seata 参数配置介绍

    • 关注的配置, 具体说明参见下面

      server端 client端
      registry.type registry.type
      config.type config.type
      store.mode service.vgroupMapping.my_test_tx_group
      store.db.driverClassName service.default.grouplist
      store.db.url service.disableGlobalTransaction
      store.db.user
      store.db.password
    • 公共部分

      key desc remark
      transport.serialization client和server通信编解码方式 seata(ByteBuf)、protobuf、kryo、hession,默认seata
      transport.compressor client和server通信数据压缩方式 none、gzip,默认none
      transport.heartbeat client和server通信心跳检测开关 默认true开启
      registry.type 注册中心类型 默认file,支持file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom
      config.type 配置中心类型 默认file,支持file、nacos 、apollo、zk、consul、etcd3、custom
    • Server 端

      • store.mode : 事务会话信息存储方式, file本地文件(不支持HA),db数据库(支持HA)
      • store.db.driverClassName: db模式数据库驱动
      • store.db.url : db模式数据库url , 默认jdbc:mysql://127.0.0.1:3306/seata
    • Client 端

      • service.vgroupMapping.my_test_tx_group : my_test_tx_group为分组,配置项值为TC集群名
      • service.default.grouplist : 仅注册中心为file时使用
      • service.disableGlobalTransaction : 全局事务开关,默认false。false为开启,true为关闭
    • 事务分组说明

      1.事务分组是什么?

      事务分组是seata的资源逻辑,类似于服务实例。在file.conf中的my_test_tx_group就是一个事务分组。

      2.通过事务分组如何找到后端集群?

      首先程序中配置了事务分组(GlobalTransactionScanner 构造方法的txServiceGroup参数),程序会通过用户配置的配置中心去寻找service.vgroupMapping .事务分组配置项,取得配置项的值就是TC集群的名称。拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同。拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表。

      3.为什么这么设计,不直接取服务名?

      这里多了一层获取事务分组到映射集群的配置。这样设计后,事务分组可以作为资源的逻辑隔离单位,当发生故障时可以快速failover。

    • grouplist 说明

      1.什么时候会用到file.conf中的default.grouplist?

      当registry.type=file时会用到,其他时候不读。

      2.default.grouplist的值列表是否可以配置多个?

      可以配置多个,配置多个意味着集群,但当store.mode=file时,会报错。原因是在file存储模式下未提供本地文件的同步,所以需要使用store.mode=db,通过db来共享TC集群间数据

      3.是否推荐使用default.grouplist?

      不推荐,如问题1,当registry.type=file时会用到,也就是说这里用的不是真正的注册中心,不具体服务的健康检查机制当tc不可用时无法自动剔除列表,推荐使用nacos 、eureka、redis、zk、consul、etcd3、sofa。registry.type=file或config.type=file 设计的初衷是让用户再不依赖第三方注册中心或配置中心的前提下,通过直连的方式,快速验证seata服务。

      4.seata-spring-boot-starter中的配置为什么是grouplist.default,也就是说和file.conf中的default.grouplist写法刚好颠倒了位置?

      由于spring-boot本身配置文件语法的要求,这个地方需要将file.conf中的default.grouplist写成grouplist.default,效果是一样的.

  • 相关测试和常见问题

    • 测试时发现 undo_log 表中 有很多 log_status=1的记录,这是做什么用的?

      • 场景 : 分支事务a注册TC后,a的本地事务提交前发生了全局事务回滚
      • 说明: log_status=1的是防御性的,是收到全局回滚请求,但是不确定某个事务分支的本地事务是否已经执行完成了,这时事先插入一条branchid相同的数据,插入的假数据成功了,本地事务继续执行就会报主键冲突自动回滚。 假如插入不成功说明表里有数据这个本地事务已经执行完成了,那么取出这条undolog数据做反向回滚操作。
      • 结果 : 全局事务回滚成功后,确保 a资源不会Commit成功,因此是正常现象。
    • 并发测试 http://localhost:9000/business/buy 接口,可能会出现如下异常

      curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ "amount": 20,  "commodityCode": "C201901140001", "count": 2, 
         "name": "水杯", "userId": "1" }' 'http://localhost:9000/business/buy'
      
      // StorageService 报的异常
      2020-02-25 13:27:48.036 ERROR 33029 --- [20883-thread-36] .d.r.f.ExceptionFilter$ExceptionListener :  [DUBBO] Got unchecked and undeclared exception which called by 192.168.1.6. service: com.example.common.service.StorageService, method: decreaseStorage, exception: org.springframework.jdbc.UncategorizedSQLException: 
      ### Error updating database.  Cause: java.sql.SQLException: 
      ....
      ### SQL: update t_storage set count = count-1 where commodity_code = ?
      ...
      Caused by: java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[Could not found global transaction xid = 192.168.1.6:6091:2036330807] ]
      	at io.seata.rm.datasource.ConnectionProxy.recognizeLockKeyConflictException(ConnectionProxy.java:153) ~[seata-all-1.1.0.jar:1.1.0]
      	at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:215) ~[seata-all-1.1.0.jar:1.1.0]
      	at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:192) ~[seata-all-1.1.0.jar:1.1.0]
      	at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:283) ~[seata-all-1.1.0.jar:1.1.0]
      ...
      	at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:293) ~[seata-all-1.1.0.jar:1.1.0]
      ...
      	at io.seata.rm.datasource.PreparedStatementProxy.execute(PreparedStatementProxy.java:54) ~[seata-all-1.1.0.jar:1.1.0]
      	at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:47) ~[mybatis-3.5.3.jar:3.5.3]
      ... org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426) ~[mybatis-spring-2.0.3.jar:2.0.3]
      	... 52 common frames omitted
      
      // seata server 报的异常 
      2020-02-25 13:27:46.229 ERROR[ServerHandlerThread_1_500]io.seata.core.exception.AbstractExceptionHandler.exceptionHandleTemplate:120 -Catch TransactionException while do RPC, request: xid=192.168.1.6:6091:2036330807,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/test,lockKey=t_storage:1
      io.seata.core.exception.BranchTransactionException: Global lock acquire failed xid = 192.168.1.6:6091:2036330807 branchId = 2036330925
      	at io.seata.server.transaction.at.ATCore.branchSessionLock(ATCore.java:48)
      	at io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0(AbstractCore.java:77)
      	at io.seata.server.session.GlobalSession.lockAndExecute(GlobalSession.java:616)
      	at io.seata.server.coordinator.AbstractCore.branchRegister(AbstractCore.java:72)
      	at io.seata.server.coordinator.DefaultCore.branchRegister(DefaultCore.java:95)
      	...
        
      2020-02-25 13:27:46.864 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:86 -Successfully register branch xid = 192.168.1.*:6091:2036330816, branchId = 2036330927
      2020-02-25 13:27:47.014 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:334 -Successfully rollback global, xid = 192.168.1.*:6091:2036330807
      2020-02-25 13:27:47.068 
        ERROR[ServerHandlerThread_1_500]io.seata.core.exception.AbstractExceptionHandler.exceptionHandleTemplate:120 -Catch TransactionException while do RPC, request: xid=192.168.1.*:6091:2036330807,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/test,lockKey=t_storage:1
      io.seata.core.exception.GlobalTransactionException: Could not found global transaction xid = 192.168.1.*:6091:2036330807
      	at ...
      
    • 测试 http://localhost:9000/business/buy 和 http://localhost:8001/account/test_global_lock 接口

      curl -X GET http://localhost:8001/account/test_global_lock 
      
      全局事务,XID = 192.168.1.*:6091:2036330432
      
      2020-02-25 12:58:29.798  INFO 34142 --- [nio-8001-exec-7] c.e.a.controller.AccountController       : testGlobalLock
      Hi, i got lock, i will do some thing with holding this lock.
      
      2020-02-25 12:58:29.883 ERROR 34142 --- [nio-8001-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.TransactionSystemException: Could not commit JDBC transaction; nested exception is io.seata.rm.datasource.exec.LockConflictException] with root cause
      
      io.seata.rm.datasource.exec.LockConflictException: null
      	at io.seata.rm.datasource.ConnectionProxy.checkLock(ConnectionProxy.java:116) ~[seata-all-1.1.0.jar:1.1.0]
      ...
      	at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:283) ~[seata-all-1.1.0.jar:1.1.0]
      	at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:179) ~[seata-all-1.1.0.jar:1.1.0]
      ...
      	at io.seata.spring.annotation.GlobalTransactionalInterceptor.handleGlobalLock(GlobalTransactionalInterceptor.java:92) ~[seata-all-1.1.0.jar:1.1.0]
      	...
      	at com.sun.proxy.$Proxy85.testGlobalLock(Unknown Source) ~[na:na]
      	at com.example.account.controller.AccountController.testGlobalLock(AccountController.java:23) ~[classes/:na]
      	at 
      
      2020-02-25 12:58:30.898  INFO 34142 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=192.168.1.6:6091:2036330432,branchId=2036330436,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/test,applicationData=null
      2020-02-25 12:58:30.916  INFO 34142 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.1.6:6091:2036330432 2036330436 jdbc:mysql://127.0.0.1:3306/test null
      2020-02-25 12:58:30.917  INFO 34142 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
      
  • 每次执行insert 、 update 数据库时,RM 都会抛出异常 Unable to commit against JDBC Connection

    • 网上搜索到类似的问题,说是:“一开始都很正常,可在一次事务更新的条数增多后,开始出现异常

      • 虽然现象和我们的不大一样,但也得到了启发,查看 seata-server 端,是否也抛出异常
    • 查看seata-server日志,发现真正原因是:Caused by: io.seata.common.exception.StoreException: Data truncation: Data too long for column ‘row_key’ at row 1

      • 意思是:数据太长了,超出 row_key 列的范围
    • 解决方法:将 seata 库的 lock_table 表的主键 row_key 字段的长度从128改成250后,问题就解决了

    • 搜索这个问题时,发现作者已经修复这个bug : bugfix: AT mode resourceId(row_key) too long ,但是为什么还抛出异常呢?

    • 原因分析:

      • 根据修复bug的线索,得知 row_key 字段保存的数据有一部分是 jdbcUrl

      • 然后继续查找 seata-server 的源码发现 row_key 字段保存的数据为

        // rowkey 由 jdbcUrl(数据库连接字符串,resourceId) + 表名 + 主键 3部分组成
        lockDO.setRowKey(getRowKey(rowLock.getResourceId(), rowLock.getTableName(), rowLock.getPk()));
        // 中间用 "^^^" 分割
        protected String getRowKey(String resourceId, String tableName, String pk) {
                return new StringBuilder().append(resourceId).append(LOCK_SPLIT).append(tableName).append(LOCK_SPLIT).append(pk)
                    .toString();
            }
        
        LOCK_SPLIT = "^^^";
        
      • 由于项目使用的是阿里云数据库, jdbcUrl 会比较长(70个字符),加上表的主键使用的是UUID也比较长(38个字符),这样留给 表名的长度就很少了(128-70-38=14),也就是说表名不能超过14个字符,如果超过,就会抛出异常:Data too long for column ‘row_key’ at row 1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章