分佈式事務 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

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