Spring Cloud 系列:Seata 中TCC模式具體實現

概述

https://seata.io/zh-cn/docs/dev/mode/tcc-mode

https://seata.io/zh-cn/docs/user/mode/tcc

TCC模式與AT模式非常相似,每階段都是獨立事務,不同的是TCC通過人工編碼來實現數據恢復。需要實現三個方法:

  • Try:資源的檢測和預留;
  • Confirm:完成資源操作業務;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:預留資源釋放,可以理解爲try的反向操作。

Seata的TCC模型

Seata中的TCC模型依然延續之前的事務架構,如圖:

image

優缺點

TCC模式的每個階段是做什麼的?

  • Try:資源檢查和預留
  • Confirm:業務執行和提交
  • Cancel:預留資源的釋放

TCC的優點是什麼?

  • 一階段完成直接提交事務,釋放數據庫資源,性能好
  • 相比AT模型,無需生成快照,無需使用全局鎖,性能最強
  • 不依賴數據庫事務,而是依賴補償操作,可以用於非事務型數據庫

TCC的缺點是什麼?

  • 有代碼侵入,需要人爲編寫try、Confirm和Cancel接口,太麻煩
  • 軟狀態,事務是最終一致
  • 需要考慮Confirm和Cancel的失敗情況,做好冪等處理

事務懸掛和空回滾

1)空回滾

當某分支事務的try階段阻塞時,可能導致全局事務超時而觸發二階段的cancel操作。在未執行try操作時先執行了cancel操作,這時cancel不能做回滾,就是空回滾

如圖:

image

執行cancel操作時,應當判斷try是否已經執行,如果尚未執行,則應該空回滾。

2)業務懸掛

對於已經空回滾的業務,之前被阻塞的try操作恢復,繼續執行try,就永遠不可能confirm或cancel ,事務一直處於中間狀態,這就是業務懸掛

執行try操作時,應當判斷cancel是否已經執行過了,如果已經執行,應當阻止空回滾後的try操作,避免懸掛

實現TCC模式

決空回滾和業務懸掛問題,必須要記錄當前事務狀態,是在try、還是cancel?

1)思路分析

這裏我們定義一張表:

CREATE TABLE `account_freeze_tbl` (
  `xid` varchar(128) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
  `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '凍結金額',
  `state` int(1) DEFAULT NULL COMMENT '事務狀態,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

其中:

  • xid:是全局事務id
  • freeze_money:用來記錄用戶凍結金額
  • state:用來記錄事務狀態

那此時,我們的業務開怎麼做呢?

  • Try業務:
    • 記錄凍結金額和事務狀態到account_freeze表
    • 扣減account表可用金額
  • Confirm業務
    • 根據xid刪除account_freeze表的凍結記錄
  • Cancel業務
    • 修改account_freeze表,凍結金額爲0,state爲2
    • 修改account表,恢復可用金額
  • 如何判斷是否空回滾?
    • cancel業務中,根據xid查詢account_freeze,如果爲null則說明try還沒做,需要空回滾
  • 如何避免業務懸掛?
    • try業務中,根據xid查詢account_freeze ,如果已經存在則證明Cancel已經執行,拒絕執行try業務

接下來,我們改造account-service,利用TCC實現餘額扣減功能。

2)聲明TCC接口

TCC的Try、Confirm、Cancel方法都需要在接口中基於註解來聲明,

我們在account-service項目中的com.mcode.account.service包中新建一個接口,聲明TCC三個接口:

package com.mcode.account.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface AccountTCCService {

    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money")int money);

    boolean confirm(BusinessActionContext ctx);

    boolean cancel(BusinessActionContext ctx);
}

3)編寫實現類

在account-service服務中的com.mcode.account.service.impl包下新建一個類,實現TCC業務:

package com.mcode.account.service.impl;

import com.mcode.account.entity.AccountFreeze;
import com.mcode.account.mapper.AccountFreezeMapper;
import com.mcode.account.mapper.AccountMapper;
import com.mcode.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 0.獲取事務id
        String xid = RootContext.getXID();
        // 1.扣減可用餘額
        accountMapper.deduct(userId, money);
        // 2.記錄凍結金額,事務狀態
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.獲取事務id
        String xid = ctx.getXid();
        // 2.根據id刪除凍結記錄
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查詢凍結記錄
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        // 1.恢復可用餘額
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 2.將凍結金額清零,狀態改爲CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}

四種模式對比

我們從以下幾個方面來對比四種實現:

  • 一致性:能否保證事務的一致性?強一致還是最終一致?
  • 隔離性:事務之間的隔離性如何?
  • 代碼侵入:是否需要對業務代碼改造?
  • 性能:有無性能損耗?
  • 場景:常見的業務場景

如圖:

image

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