Seata TCC 分佈式事務

1. 前言

本文先經過分佈式事務中tcc方案,衍生出seata的tcc模式,主要仍是會經過代碼示例來作介紹。github代碼地址可提早下載,該項目中包括數據庫、seata配置,以及全部分佈式服務的所有代碼。你們若是想練練手,能夠先拉取該項目代碼,再結合本文學習。核心配置環境以下:java

環境類型 版本號
jdk 1.8.0_251
mysql 8.0.22
seata server 1.4.1

1.1. tcc

咱們前面有幾篇文章都有介紹過度布式事務的方案,目前常見的分佈式事務方案有:2pc、tcc和異步確保型。以前講過用jta atomikos實現多數據源的 2pc,用 異步確保型 方案實現支付業務的事務等等,就是沒專門講過 tcc 的應用。node

由於tcc方案的操做難度仍是比較大的。不能單打獨鬥,最好須要依託一個成熟的框架來實現。常見的tcc開源框架有tcc-transaction、Hmily和ByteTCC等,不過他們不像seata背靠大廠,沒法提供持續的維護,所以我更推薦seata的tcc方案。mysql

1.2. seata

先說說seata吧,分佈式事務的解決方案確定不侷限於上面說的三種,實際上五花八門。由於它的確很讓人頭疼,各位大神都想研發出最好用的框架。本文的主角 - seata ,就是阿里的一個開源項目。git

seata提供了AT、TCC、SAGA 和 XA,一共4種事務模式。像AT模式就很受歡迎,咱們在實現多數據源的事務一致性時,一般會選用 2PC的方案,等待全部數據源的事務執行成功,最後再一塊兒提交事務。這個等待全部數據源事務執行的過程就比較耗時,即影響性能,也不安全。github

而seata AT模式的作法就很靈活,它學習數據庫的 undo log,每一個事務執行時當即提交事務,但會把 undo 的回退sql記錄下來。若是全部事務執行成功,清除記錄 undo sql的行記錄,若是某個事務失敗,則執行對應 undo sql 回滾數據。在保證事務的同時,併發量也大了起來。redis

但咱們今天要講的是 seata TCC 模式,若是你對 Seata的其餘模式感興趣,能夠上官網瞭解。spring

2. 業務

先講一下示例的業務吧,咱們仍是拿比較經典的電商支付場景舉例。假設支付成功後,涉及到三個系統事務:sql

  1. 訂單系統(order):建立支付訂單。
  2. 庫存系統(storage):對應商品扣除庫存。
  3. 帳戶系統(account):用戶帳戶扣除響應金額。

2.1. tcc業務

按照tcc(try-confirm-cancel)的思路,這三個事務能夠分別分解成下面的過程。數據庫

訂單系統 order
  1. try: 建立訂單,可是訂單狀態設置一個臨時狀態(如:status=0)。
  2. confirm: try成功,提交事務,將訂單狀態更新爲徹底狀態(如:status=1)。
  3. cancel: 回滾事務,刪除該訂單記錄。
庫存系統 storage
  1. try: 將須要減小的庫存量凍結起來。
  2. confirm: try成功,提交事務,使用凍結的庫存扣除,完成業務數據處理。
  3. cancel: 回滾事務,凍結的庫存解凍,恢復之前的庫存量。
帳戶系統 account
  1. try: 將須要扣除的錢凍結起來。
  2. confirm: try成功,提交事務,使用凍結的錢扣除,完成業務數據處理。
  3. cancel: 回滾事務,凍結的錢解凍,恢復之前的帳戶餘額。

2.2. 數據庫

爲了模擬分佈式事務,上述的不一樣系統業務,咱們經過在不一樣數據庫中建立表結構來模擬。固然tcc的分佈式事務不侷限於數據庫層面,還包括http接口調用和rpc調用等,可是殊途同歸,能夠做爲示例參考。json

下面先列出三張業務表的表結構,具體的sql可見最後附件。

表:order
列名 類型 備註
id int 主鍵
order_no varchar 訂單號
user_id int 用戶id
product_id int 產品id
amount int 數量
money decimal 金額
status int 訂單狀態:0:建立中;1:已完結
表:storage
列名 類型 備註
id int 主鍵
product_id int 產品id
residue int 剩餘庫存
frozen int TCC事務鎖定的庫存
表:account
列名 類型 備註
id int 主鍵
user_id int 用戶id
residue int 剩餘可用額度
frozen int TCC事務鎖定的金額

3. seata server

3.1. 下載

seata server 的安裝包可直接從官方github下載,下載壓縮包後,解壓到本地或服務器上。

3.2. 配置

Seata Server 的配置文件有兩個:

  • seata/conf/registry.conf
  • seata/conf/file.conf
registry.conf

Seata Server 要向註冊中心進行註冊,這樣,其餘服務就能夠經過註冊中心去發現 Seata Server,與 Seata Server 進行通訊。Seata 支持多款註冊中心服務:nacos 、eureka、redis、zk、consul、etcd三、sofa。咱們項目中要使用 eureka 註冊中心,eureka服務的鏈接地址、註冊的服務名,這須要在 registry.conf 文件中對 registry 進行配置。

Seata 須要存儲全局事務信息、分支事務信息、全局鎖信息,這些數據存儲到什麼位置?針對存儲位置的配置,支持放在配置中心,或者也能夠放在本地文件。Seata Server 支持的配置中心服務有:nacos 、apollo、zk、consul、etcd3。這裏咱們選擇最簡單的,使用本地文件,這須要在 registry.conf 文件中對 config 進行配置。

file.conf

file.conf 中對事務信息的存儲位置進行配置,存儲位置支持:file、db、redis。
這裏咱們選擇數據庫做爲存儲位置,這須要在 file.conf 中進行配置。

3.3. 啓動

執行 seata/bin/seata-server.sh(windows 是 seata-server.bat) 腳本便可啓動seata server。還能夠配置下列參數:

-h:註冊到註冊中心的ip
-p:server rpc 監聽端口,默認 8091
-m:全局事務會話信息存儲模式,file、db,優先讀取啓動參數
-n:server node,多個server時,須要區分各自節點,用於生成不一樣區間的transctionId,以避免衝突
-e:多環境配置

3.4. 常見問題

mysql 8

默認啓動後會報mysql-connector-java-x.jar驅動的錯誤,是由於seata server 默認不支持mysql 8。
能夠在seata server的 lib 文件夾下替換 mysql 的驅動 jar 包。lib 文件夾下,已經有一個 jdbc 文件夾,把裏面驅動版本爲 8 的 mysql-connector-java-x.jar 包拷貝到外面 lib 文件夾下便可。

4. 代碼

github示例項目中包括3個業務服務、1個註冊中心,以及resources下的數據庫腳本和seata server配置文件。按照服務的啓動順序,以下分類:

  1. resources/database-sql:初始化數據庫
  2. eureka-server:運行 註冊中心
  3. resources/seata-server:下載、安裝、配置、啓動 seata server服務
  4. account-service:運行 用戶帳戶服務
  5. storage-service:運行 商品庫存服務
  6. order-service:運行 訂單服務
  7. 測試:經過postman等工具,調用 order-server 的下訂單接口

3個業務服務中,order訂單服務 能夠被稱爲「主事務」,當訂單建立成功後,再在訂單服務中調用 account帳號服務storage庫存服務兩個「副事務」。所以從 seata tcc代碼層面上,能夠分紅下面兩類。
下文中不會列舉業務代碼,完整代碼能夠從github上查看,只會列出 seata 的相關代碼和配置。

4.1. 主事務(order)

4.1.1. application 配置文件

配置文件中須要配置 tx-service-group,須要注意的是,3個業務服務中都須要配置一樣的值。

application.yml
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group

4.1.2. seata配置文件

在application.yml同級目錄,即 resources 目錄下,建立兩個seata 的配置文件。還記得在seata server 啓動的時候也有這兩個文件,但內容不同,不要混淆了。

file.conf
transport {
  type = "TCP"
  server = "NIO"
  heartbeat = true
  enableClientBatchSendRequest = true

  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    bossThreadSize = 1
    workerThreadSize = "default"
  }
  shutdown {
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  vgroupMapping.order_tx_group = "seata-server"
  order_tx_group.grouplist = "127.0.0.1:8091"
  enableDegrade = false
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}
registry.conf
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd三、sofa
  type = "eureka"

  eureka {
    serviceUrl = "http://localhost:8761/eureka"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd三、springCloudConfig
  type = "file"
  file {
    name = "file.conf"
  }
}

4.1.3. @LocalTCC tcc服務

這是配置 TCC 子服務的核心代碼,

  • @LocalTCC:

該註解須要添加到上面描述的接口上,表示實現該接口的類被 seata 來管理,seata 根據事務的狀態,自動調用咱們定義的方法,若是沒問題則調用 Commit 方法,不然調用 Rollback 方法。

  • @TwoPhaseBusinessAction:

該註解用在接口的 Try 方法上。

  • @BusinessActionContextParameter:

該註解用來修飾 Try 方法的入參,被修飾的入參能夠在 Commit 方法和 Rollback 方法中經過 BusinessActionContext 獲取。

  • BusinessActionContext:

在接口方法的實現代碼中,能夠經過 BusinessActionContext 來獲取參數, BusinessActionContext 就是 seata tcc 的事務上下文,用於存放 tcc 事務的一些關鍵數據。BusinessActionContext 對象能夠直接做爲 commit 方法和 rollbakc 方法的參數,Seata 會自動注入參數。

OrderTccAction.java

@LocalTCC
public interface OrderTccAction {

    /**
     * try 嘗試
     *
     * BusinessActionContext 上下文對象,用來在兩個階段之間傳遞數據
     * BusinessActionContextParameter 註解的參數數據會被存入 BusinessActionContext
     * TwoPhaseBusinessAction 註解中commitMethod、rollbackMethod 屬性有默認值,能夠不寫
     *
     * @param businessActionContext
     * @param orderNo
     * @param userId
     * @param productId
     * @param amount
     * @param money
     * @return
     */
    @TwoPhaseBusinessAction(name = "orderTccAction")
    boolean prepareCreateOrder(BusinessActionContext businessActionContext,
                               @BusinessActionContextParameter(paramName = "orderNo") String orderNo,
                               @BusinessActionContextParameter(paramName = "userId") Long userId,
                               @BusinessActionContextParameter(paramName = "productId") Long productId,
                               @BusinessActionContextParameter(paramName = "amount") Integer amount,
                               @BusinessActionContextParameter(paramName = "money") BigDecimal money);

    /**
     * commit 提交
     * @param businessActionContext
     * @return
     */
    boolean commit(BusinessActionContext businessActionContext);

    /**
     * cancel 撤銷
     * @param businessActionContext
     * @return
     */
    boolean rollback(BusinessActionContext businessActionContext);
}

OrderTccActionImpl.java

@Slf4j
@Component
public class OrderTccActionImpl implements OrderTccAction {
    private final OrderMapper orderMapper;
    public OrderTccActionImpl(OrderMapper orderMapper){
        this.orderMapper=orderMapper;
    }
    
    /**
     * try 嘗試
     *
     * BusinessActionContext 上下文對象,用來在兩個階段之間傳遞數據
     * BusinessActionContextParameter 註解的參數數據會被存入 BusinessActionContext
     * TwoPhaseBusinessAction 註解中commitMethod、rollbackMethod 屬性有默認值,能夠不寫
     *
     * @param businessActionContext
     * @param orderNo
     * @param userId
     * @param productId
     * @param amount
     * @param money
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean prepareCreateOrder(BusinessActionContext businessActionContext,
                                      String orderNo,
                                      Long userId,
                                      Long productId,
                                      Integer amount,
                                      BigDecimal money) {
        orderMapper.save(new OrderDO(orderNo,userId, productId, amount, money, 0));
        ResultHolder.setResult(OrderTccAction.class, businessActionContext.getXid(), "p");
        return true;
    }

    /**
     * commit 提交
     *
     * @param businessActionContext
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean commit(BusinessActionContext businessActionContext) {
        //檢查標記是否存在,若是標記不存在不重複提交
        String p = ResultHolder.getResult(OrderTccAction.class, businessActionContext.getXid());
        if (p == null){
            return true;
        }

        /**
         * 上下文對象從第一階段向第二階段傳遞時,先轉成了json數據,而後還原成上下文對象
         * 其中的整數比較小的會轉成Integer類型,因此若是須要Long類型,須要先轉換成字符串在用Long.valueOf()解析。
         */
        String orderNo = businessActionContext.getActionContext("orderNo").toString();
        orderMapper.updateStatusByOrderNo(orderNo, 1);
        //提交完成後,刪除標記
        ResultHolder.removeResult(OrderTccAction.class, businessActionContext.getXid());
        return true;
    }

    /**
     * cancel 撤銷
     *
     * 第一階段沒有完成的狀況下,沒必要執行回滾。由於第一階段有本地事務,事務失敗時已經進行了回滾。
     * 若是這裏第一階段成功,而其餘全局事務參與者失敗,這裏會執行回滾
     * 冪等性控制:若是重複執行回滾則直接返回
     *
     * @param businessActionContext
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean rollback(BusinessActionContext businessActionContext) {
        //檢查標記是否存在,若是標記不存在不重複提交
        String p = ResultHolder.getResult(OrderTccAction.class, businessActionContext.getXid());
        if (p == null){
            return true;
        }
        String orderNo = businessActionContext.getActionContext("orderNo").toString();
        orderMapper.deleteByOrderNo(orderNo);
        //提交完成後,刪除標記
        ResultHolder.removeResult(OrderTccAction.class, businessActionContext.getXid());
        return true;
    }

}

4.1.4. @GlobalTransactional 全局服務

@GlobalTransactional 註解是惟一做用到「主事務」的方法。該註解加在「主事務」調用「副事務」的方法上。

OrderServiceImpl.java

@Service
public class OrderServiceImpl implements OrderService {
    private final OrderTccAction orderTccAction;
    private final AccountFeign accountFeign;
    private final StorageFeign storageFeign;

    public OrderServiceImpl(OrderTccAction orderTccAction, AccountFeign accountFeign, StorageFeign storageFeign){
        this.orderTccAction=orderTccAction;
        this.accountFeign=accountFeign;
        this.storageFeign=storageFeign;
    }

    /**
     * 建立訂單
     * @param orderDO
     */
    @GlobalTransactional
    @Override
    public void createOrder(OrderDO orderDO) {
        String orderNo=this.generateOrderNo();
        //建立訂單
        orderTccAction.prepareCreateOrder(null,
                orderNo,
                orderDO.getUserId(),
                orderDO.getProductId(),
                orderDO.getAmount(),
                orderDO.getMoney());
        //扣餘額
        accountFeign.decreaseMoney(orderDO.getUserId(),orderDO.getMoney());
        //扣庫存
        storageFeign.decreaseStorage(orderDO.getProductId(),orderDO.getAmount());
    }

    private String generateOrderNo(){
        return LocalDateTime.now()
                .format(
                        DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")
                );
    }
}

4.2. 副事務(account、storage)

account 和 storage 兩個服務相比較於 order,只少了 「4.1.4. @GlobalTransactional 全局服務」,其餘的配置徹底同樣。所以,這裏就再也不贅言了。

5. 總結

測試

經過調用「主事務」 order-service 的建立訂單接口,來模擬分佈式事務。咱們能夠經過在3個業務服務的不一樣代碼處故意拋出錯誤,看是否可以實現事務的一致回滾。

seata框架表結構

在 /resources/database-sql 的數據庫腳本中,各自還有一些 seata 框架自己的表結構,用於存儲分佈式事務各自的中間狀態。由於這個中間狀態很短,一旦事務一致性達成,表數據就會自動刪除,所以平時咱們沒法查看數據庫。

由於seata tcc模式,會一直阻塞到全部的 try執行完畢,再執行後續的。從而咱們能夠經過在部分業務服務try的代碼中加上Thread.sleep(10000),強制讓事務過程變慢,從而就能夠看到這些 seata 表數據。

冪等性

tcc模式中,CommitCancel 都是有自動重試功能的,處於事務一致性考慮,重試功能頗有必要。但咱們就必定要慎重考慮方法的 冪等性,示例代碼中的ResultHolder類並非個好方案,仍是要在Commit、Cancel業務方法自己作冪等性要求。

相關文章
相關標籤/搜索