<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          阿里 Seata 新版本終于解決了 TCC 模式的冪等、懸掛和空回滾問題

          共 16653字,需瀏覽 34分鐘

           ·

          2022-06-27 20:51

          Hollis的新書限時折扣中,一本深入講解Java基礎的干貨筆記!

          今天來聊一聊阿里巴巴 Seata 新版本(1.5.0)是怎么解決 TCC 模式下的冪等、懸掛和空回滾問題的。

          1 TCC 回顧

          TCC 模式是最經典的分布式事務解決方案,它將分布式事務分為兩個階段來執(zhí)行,try 階段對每個分支事務進行預留資源,如果所有分支事務都預留資源成功,則進入 commit 階段提交全局事務,如果有一個節(jié)點預留資源失敗則進入 cancel 階段回滾全局事務。

          以傳統(tǒng)的訂單、庫存、賬戶服務為例,在 try 階段嘗試預留資源,插入訂單、扣減庫存、扣減金額,這三個服務都是要提交本地事務的,這里可以把資源轉入中間表。在 commit 階段,再把 try 階段預留的資源轉入最終表。而在 cancel 階段,把 try 階段預留的資源進行釋放,比如把賬戶金額返回給客戶的賬戶。

          注意:try 階段必須是要提交本地事務的,比如扣減訂單金額,必須把錢從客戶賬戶扣掉,如果不扣掉,在 commit 階段客戶賬戶錢不夠了,就會出問題。

          1.1 try-commit

          try 階段首先進行預留資源,然后在 commit 階段扣除資源。如下圖:

          1.2 try-cancel

          try 階段首先進行預留資源,預留資源時扣減庫存失敗導致全局事務回滾,在 cancel 階段釋放資源。如下圖:

          2 TCC 優(yōu)勢

          TCC 模式最大的優(yōu)勢是效率高。TCC 模式在 try 階段的鎖定資源并不是真正意義上的鎖定,而是真實提交了本地事務,將資源預留到中間態(tài),并不需要阻塞等待,因此效率比其他模式要高。

          同時 TCC 模式還可以進行如下優(yōu)化:

          2.1 異步提交

          try 階段成功后,不立即進入 confirm/cancel 階段,而是認為全局事務已經結束了,啟動定時任務來異步執(zhí)行 confirm/cancel,扣減或釋放資源,這樣會有很大的性能提升。

          2.2 同庫模式

          TCC 模式中有三個角色:

          • TM:管理全局事務,包括開啟全局事務,提交/回滾全局事務;
          • RM:管理分支事務;
          • TC: 管理全局事務和分支事務的狀態(tài)。

          下圖來自 Seata 官網:

          TM 開啟全局事務時,RM 需要向 TC 發(fā)送注冊消息,TC 保存分支事務的狀態(tài)。TM 請求提交或回滾時,TC 需要向 RM 發(fā)送提交或回滾消息。這樣包含兩個個分支事務的分布式事務中,TC 和 RM 之間有四次 RPC。

          優(yōu)化后的流程如下圖:

          TC 保存全局事務的狀態(tài)。TM 開啟全局事務時,RM 不再需要向 TC 發(fā)送注冊消息,而是把分支事務狀態(tài)保存在了本地。TM 向 TC 發(fā)送提交或回滾消息后,RM 異步線程首先查出本地保存的未提交分支事務,然后向 TC 發(fā)送消息獲?。ū镜胤种聞?span style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;color: rgb(0, 0, 0);font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;font-size: 16px;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: left;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;text-decoration-style: initial;text-decoration-color: initial;float: none;display: inline !important;">所在的)全局事務狀態(tài),以決定是提交還是回滾本地事務。

          這樣優(yōu)化后,RPC 次數減少了 50%,性能大幅提升。

          3 RM 代碼示例

          以庫存服務為例,RM 庫存服務接口代碼如下:

          @LocalTCC
          public interface StorageService {

              /**
               * 扣減庫存
               * @param xid 全局xid
               * @param productId 產品id
               * @param count 數量
               * @return
               */

              @TwoPhaseBusinessAction(name = "storageApi", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
              boolean decrease(String xid, Long productId, Integer count);

              /**
               * 提交事務
               * @param actionContext
               * @return
               */

              boolean commit(BusinessActionContext actionContext);

              /**
               * 回滾事務
               * @param actionContext
               * @return
               */

              boolean rollback(BusinessActionContext actionContext);
          }

          通過 @LocalTCC 這個注解,RM 初始化的時候會向 TC 注冊一個分支事務。在 try 階段的方法(decrease方法)上有一個 @TwoPhaseBusinessAction 注解,這里定義了分支事務的 resourceId,commit 方法和 cancel 方法,useTCCFence 這個屬性下一節(jié)再講。

          4 TCC 存在問題

          TCC 模式中存在的三大問題是冪等、懸掛和空回滾。在 Seata1.5.0 版本中,增加了一張事務控制表,表名是 tcc_fence_log 來解決這個問題。而在上一節(jié) @TwoPhaseBusinessAction 注解中提到的屬性 useTCCFence 就是來指定是否開啟這個機制,這個屬性值默認是 false。

          tcc_fence_log 建表語句如下(MySQL 語法):

          CREATE TABLE IF NOT EXISTS `tcc_fence_log`
          (
              `xid`           VARCHAR(128)  NOT NULL COMMENT 'global id',
              `branch_id`     BIGINT        NOT NULL COMMENT 'branch id',
              `action_name`   VARCHAR(64)   NOT NULL COMMENT 'action name',
              `status`        TINYINT       NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
              `gmt_create`    DATETIME(3)   NOT NULL COMMENT 'create time',
              `gmt_modified`  DATETIME(3)   NOT NULL COMMENT 'update time',
              PRIMARY KEY (`xid``branch_id`),
              KEY `idx_gmt_modified` (`gmt_modified`),
              KEY `idx_status` (`status`)
          ENGINE = InnoDB
          DEFAULT CHARSET = utf8mb4;

          4.1 冪等

          在 commit/cancel 階段,因為 TC 沒有收到分支事務的響應,需要進行重試,這就要分支事務支持冪等。

          我們看一下新版本是怎么解決的。下面的代碼在 TCCResourceManager 類:

          @Override
          public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
                   String applicationData)
           throws TransactionException 
          {
           TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
           //省略判斷
           Object targetTCCBean = tccResource.getTargetBean();
           Method commitMethod = tccResource.getCommitMethod();
           //省略判斷
           try {
            //BusinessActionContext
            BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,
             applicationData);
            Object[] args = this.getTwoPhaseCommitArgs(tccResource, businessActionContext);
            Object ret;
            boolean result;
            //注解 useTCCFence 屬性是否設置為 true
            if (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) {
             try {
              result = TCCFenceHandler.commitFence(commitMethod, targetTCCBean, xid, branchId, args);
             } catch (SkipCallbackWrapperException | UndeclaredThrowableException e) {
              throw e.getCause();
             }
            } else {
             //省略邏輯
            }
            LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId);
            return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
           } catch (Throwable t) {
            //省略
            return BranchStatus.PhaseTwo_CommitFailed_Retryable;
           }
          }

          上面的代碼可以看到,執(zhí)行分支事務提交方法時,首先判斷 useTCCFence 屬性是否為 true,如果為 true,則走 TCCFenceHandler 類中的 commitFence 邏輯,否則走普通提交邏輯。

          TCCFenceHandler 類中的 commitFence 方法調用了 TCCFenceHandler 類的 commitFence 方法,代碼如下:

          public static boolean commitFence(Method commitMethod, Object targetTCCBean,
                    String xid, Long branchId, Object[] args)
           
          {
           return transactionTemplate.execute(status -> {
            try {
             Connection conn = DataSourceUtils.getConnection(dataSource);
             TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
             if (tccFenceDO == null) {
              throw new TCCFenceException(String.format("TCC fence record not exists, commit fence method failed. xid= %s, branchId= %s", xid, branchId),
                FrameworkErrorCode.RecordAlreadyExists);
             }
             if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
              LOGGER.info("Branch transaction has already committed before. idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
              return true;
             }
             if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
              if (LOGGER.isWarnEnabled()) {
               LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
              }
              return false;
             }
             return updateStatusAndInvokeTargetMethod(conn, commitMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_COMMITTED, status, args);
            } catch (Throwable t) {
             status.setRollbackOnly();
             throw new SkipCallbackWrapperException(t);
            }
           });
          }

          從代碼中可以看到,提交事務時首先會判斷 tcc_fence_log 表中是否已經有記錄,如果有記錄,則判斷事務執(zhí)行狀態(tài)并返回。這樣如果判斷到事務的狀態(tài)已經是 STATUS_COMMITTED,就不會再次提交,保證了冪等。如果 tcc_fence_log 表中沒有記錄,則插入一條記錄,供后面重試時判斷。

          Rollback 的邏輯跟 commit 類似,邏輯在類 TCCFenceHandler 的 rollbackFence 方法。

          4.2 空回滾

          如下圖,賬戶服務是兩個節(jié)點的集群,在 try 階段賬戶服務 1 這個節(jié)點發(fā)生了故障,try 階段在不考慮重試的情況下,全局事務必須要走向結束狀態(tài),這樣就需要在賬戶服務上執(zhí)行一次 cancel 操作,這樣就空跑了一次回滾操作。

          Seata 的解決方案是在 try 階段 往 tcc_fence_log  表插入一條記錄,status 字段值是 STATUS_TRIED,在 Rollback 階段判斷記錄是否存在,如果不存在,則不執(zhí)行回滾操作。代碼如下:

          //TCCFenceHandler 類
          public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {
           return transactionTemplate.execute(status -> {
            try {
             Connection conn = DataSourceUtils.getConnection(dataSource);
             boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
             LOGGER.info("TCC fence prepare result: {}. xid: {}, branchId: {}", result, xid, branchId);
             if (result) {
              return targetCallback.execute();
             } else {
              throw new TCCFenceException(String.format("Insert tcc fence record error, prepare fence failed. xid= %s, branchId= %s", xid, branchId),
                FrameworkErrorCode.InsertRecordError);
             }
            } catch (TCCFenceException e) {
             //省略
            } catch (Throwable t) {
             //省略
            }
           });
          }

          在 Rollback 階段的處理邏輯如下:

          //TCCFenceHandler 類
          public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
                   String xid, Long branchId, Object[] args, String actionName)
           
          {
           return transactionTemplate.execute(status -> {
            try {
             Connection conn = DataSourceUtils.getConnection(dataSource);
             TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
             // non_rollback
             if (tccFenceDO == null) {
              //不執(zhí)行回滾邏輯
              return true;
             } else {
              if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
               LOGGER.info("Branch transaction had already rollbacked before, idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
               return true;
              }
              if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
               if (LOGGER.isWarnEnabled()) {
                LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
               }
               return false;
              }
             }
             return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
            } catch (Throwable t) {
             status.setRollbackOnly();
             throw new SkipCallbackWrapperException(t);
            }
           });
          }

          updateStatusAndInvokeTargetMethod 方法執(zhí)行的 sql 如下:

          update tcc_fence_log set status = ?, gmt_modified = ?
              where xid = ? and  branch_id = ? and status = ? ;

          可見就是把 tcc_fence_log 表記錄的  status  字段值從 STATUS_TRIED 改為 STATUS_ROLLBACKED,如果更新成功,就執(zhí)行回滾邏輯。

          4.3 懸掛

          懸掛是指因為網絡問題,RM 開始沒有收到 try 指令,但是執(zhí)行了 Rollback 后 RM 又收到了 try 指令并且預留資源成功,這時全局事務已經結束,最終導致預留的資源不能釋放。如下圖:

          Seata 解決這個問題的方法是執(zhí)行 Rollback 方法時先判斷 tcc_fence_log 是否存在當前 xid 的記錄,如果沒有則向 tcc_fence_log 表插入一條記錄,狀態(tài)是 STATUS_SUSPENDED,并且不再執(zhí)行回滾操作。代碼如下:

          public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
                   String xid, Long branchId, Object[] args, String actionName)
           
          {
           return transactionTemplate.execute(status -> {
            try {
             Connection conn = DataSourceUtils.getConnection(dataSource);
             TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
             // non_rollback
             if (tccFenceDO == null) {
                 //插入防懸掛記錄
              boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);
              //省略邏輯
              return true;
             } else {
              //省略邏輯
             }
             return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
            } catch (Throwable t) {
             //省略邏輯
            }
           });
          }

          而后面執(zhí)行 try 階段方法時首先會向 tcc_fence_log 表插入一條當前 xid 的記錄,這樣就造成了主鍵沖突。代碼如下:

          //TCCFenceHandler 類
          public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {
           return transactionTemplate.execute(status -> {
            try {
             Connection conn = DataSourceUtils.getConnection(dataSource);
             boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
             //省略邏輯
            } catch (TCCFenceException e) {
             if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {
              LOGGER.error("Branch transaction has already rollbacked before,prepare fence failed. xid= {},branchId = {}", xid, branchId);
              addToLogCleanQueue(xid, branchId);
             }
             status.setRollbackOnly();
             throw new SkipCallbackWrapperException(e);
            } catch (Throwable t) {
             //省略
            }
           });
          }

          注意:queryTCCFenceDO 方法 sql 中使用了 for update,這樣就不用擔心 Rollback 方法中獲取不到 tcc_fence_log 表記錄而無法判斷 try 階段本地事務的執(zhí)行結果了。

          5 總結

          TCC 模式是分布式事務使用最多的模式,但是冪等、懸掛和空回滾一直是 TCC 模式需要考慮的問題,Seata 框架在 1.5.0 版本完美解決了這些問題。

          對 tcc_fence_log 表的操作也需要考慮事務的控制,Seata 使用了代理數據源,使 tcc_fence_log 表操作和 RM 業(yè)務操作在同一個本地事務中執(zhí)行,這樣就能保證本地操作和對 tcc_fence_log 的操作同時成功或失敗。


          我的新書《深入理解Java核心技術》已經上市了,上市后一直蟬聯京東暢銷榜中,目前正在6折優(yōu)惠中,想要入手的朋友千萬不要錯過哦~長按二維碼即可購買~


          長按掃碼享受6折優(yōu)惠


          往期推薦

          知乎熱議:月薪 2~3W 的碼農,怎樣度過一天?


          從實現原理講,Nacos 為什么這么強


          我是一個Dubbo數據包...




          有道無術,術可成;有術無道,止于術

          歡迎大家關注Java之道公眾號


          好文章,我在看??

          瀏覽 29
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  五月天色色婷婷 | 成年人免费看视频 | 影音先锋一区二区成人三级视频 | 亚洲操逼黄色网 | 大香蕉女视频 |