分散式事務解決方案(五)【TCC型方案】

風靈使發表於2018-07-31

5-TCC型方案

5.1 介紹

TCC方案屬於兩階段型/補償型

5.1.1 實現

image

  • 一個完整的業務活動由一個主業務服務與若干從業務服務組成
  • 主業務服務負責發起並完成整個業務活動
  • 從業務服務提供TCC型業務操作
  • 業務活動管理器控制業務活動的一致性,它登記業務活動中的操作,並在業務活動提交時確認所有的TCC型操作的confirm操作,在業務活動取消時呼叫所有TCC型操作的cancel操作.

5.1.2 成本

  • 業務活動結束時confirm或cancel操作的執行成本
  • 業務活動日誌成本

5.1.3 適用範圍

  • 強隔離性,嚴格一致性要求的業務活動
  • 適用於執行時間較短的業務,如處理賬戶,收費等業務

5.1.4 用到的服務模式

  • TCC操作
  • 冪等操作
  • 可補償操作
  • 可查詢操作

5.1.5 方案特點

  • 不與具體的服務框架耦合(RPC架構通用)
  • 位於業務服務層,而非資源層
  • 可以靈活選擇業務資源的鎖定粒度
  • TCC裡對每個服務資源操作的是本地事務,資料被lock的時間短,可擴充套件性好(可以說是為獨立部署的SOA服務而設計的)

5.1.6 行業應用案例

  • 支付寶XTS(螞蟻金融雲的分散式事務服務DTS)

5.2 TCC框架

GitHub上一個開源的TCC框架實現,以此專案為基礎講解TCC框架的實現(主要是在專案原始碼上新增一些註釋)

5.3 TCC應用例項

5.3.1 業務流程

以訂單處理流程為例:賬戶扣款->使用紅包優惠券->訂單狀態改變為例

image

一個主服務呼叫從業務服務,被TCC控制的方法都應該具有三種方法:主方法(try方法)/確認方法(confirm方法)/取消方法(cancel方法)

  1. 修改支付記錄狀態
  2. 修改訂單狀態
  3. 遠端呼叫點券服務
  4. 遠端呼叫積分服務
  5. 若遠端呼叫存在異常

5.2.2 使用TCC-TRASACTION示例

@Service("pointAccountService")
public class pointAccountServiceImpl implements pointAccountService{

    private static final Logger LOG = LoggerFactory.getLogger(pointAccountServiceImpl.class);

    @Autowired
    private pointAccountDao pointAccountDao;

    @Autowired
    private pointAccountHistoryDao pointAccountHistoryDao;

    @Override
    public void saveData(pointAccount pointAccount) {
        pointAccountDao.insert(pointAccount);
    }

    @Override
    public void updateData(pointAccount pointAccount) {
        pointAccountDao.update(pointAccount);
    }


    /**
     * 積分賬戶加款 Trying
     * @param transactionContext
     * @param userNo
     * @param pointAmount
     * @param requestNo
     * @param bankTrxNo
     * @param trxType
     * @param remark
     * @throws BizException
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    @Compensable(confirmMethod = "confirmCreditToPointAccountTcc",cancelMethod = "cancelCreditToPointAccountTcc")
    public void creditToPointAccountTcc(TransactionContext transactionContext, String userNo, Integer pointAmount, String requestNo, String bankTrxNo, String trxType, String remark) throws BizException {

        LOG.info("===>creditToPointAccountTcc TRYING begin");

        //根據商戶編號獲取商戶積分賬戶
        pointAccount pointAccount = pointAccountDao.getByUserNo(userNo);
        if (pointAccount == null){//如果不存在商戶積分賬戶,建立一條新的積分賬戶
            pointAccount = new pointAccount();
            pointAccount.setBalance(0);
            pointAccount.setUserNo(userNo);
            pointAccount.setStatus(PublicEnum.YES.name());
            pointAccount.setCreateTime(new Date());
            pointAccount.setId(StringUtil.get32UUID());
            pointAccountDao.insert(pointAccount);
        }

        pointAccountHistory pointAccountHistory = pointAccountHistoryDao.getByRequestNo(requestNo);
        // 冪等判斷
        if ( pointAccountHistory == null ){//防止多次提交
            pointAccountHistory = new pointAccountHistory();
            pointAccountHistory.setId(StringUtil.get32UUID());
            pointAccountHistory.setCreateTime(new Date());
            pointAccountHistory.setStatus(PointAccountHistoryStatusEnum.TRYING.name());//訊息不可用
            pointAccountHistory.setAmount(pointAmount);///積分賬戶變動額
            pointAccountHistory.setBalance(pointAccount.getBalance() + pointAmount);
            pointAccountHistory.setBankTrxNo(bankTrxNo);//銀行流水號
            pointAccountHistory.setRequestNo(requestNo);//請求號
            pointAccountHistory.setFundDirection(PointAccountFundDirectionEnum.ADD.name());
            pointAccountHistory.setTrxType(trxType);
            pointAccountHistory.setRemark(remark);
            pointAccountHistory.setUserNo(userNo);
            pointAccountHistoryDao.insert(pointAccountHistory);
        }else if (PointAccountHistoryStatusEnum.CANCEL.name().equals(pointAccountHistory.getStatus())){
            //如果是取消的,有可能是之前的業務出現異常問題而取消,那麼重試階段,再將狀態更新為TYING狀態,而不是重新建立一條
            LOG.info("之前因為業務問題取消後,又重試的{}" , pointAccountHistory.getBankTrxNo());
            pointAccountHistory.setStatus(PointAccountHistoryStatusEnum.TRYING.name());
            this.pointAccountHistoryDao.update(pointAccountHistory);
        }
        //新增一條不可用的積分賬戶流水
        LOG.info("===>creditToPointAccountTcc TRYING end");
    }

    /**
     * 積分賬戶增加確認
     * @param transactionContext
     * @param userNo
     * @param pointAmount
     * @param requestNo
     * @param bankTrxNo
     * @param trxType
     * @param remark
     * @return
     * @throws BizException
     */

    @Transactional(rollbackFor = Exception.class)
    public void confirmCreditToPointAccountTcc(TransactionContext transactionContext, String userNo, Integer pointAmount, String requestNo, String bankTrxNo, String trxType, String remark) throws BizException {

        LOG.info("===>confirmCreditToPointAccountTcc begin");
        //根據請求號獲取賬戶基本流水
        pointAccountHistory pointAccountHistory = pointAccountHistoryDao.getByRequestNo(requestNo);
        // 冪等判斷
        if ( pointAccountHistory == null  || PointAccountHistoryStatusEnum.CONFORM.name().equals(pointAccountHistory.getStatus())){//該筆交易流水已處理過,不需再處理
            return;
        }

        pointAccountHistory.setStatus(PointAccountHistoryStatusEnum.CONFORM.name());
        pointAccountHistoryDao.update(pointAccountHistory);

        pointAccount pointAccount = pointAccountDao.getByUserNo(userNo);//獲取使用者積分賬戶
        pointAccount.setBalance(pointAccount.getBalance() + pointAmount);//增加賬戶餘額
        pointAccountDao.update(pointAccount);

        LOG.info("===>confirmCreditToPointAccountTcc end");

    }
    /**
     *積分賬戶增加回滾
     * @param transactionContext
     * @param userNo
     * @param pointAmount
     * @param requestNo
     * @param bankTrxNo
     * @param trxType
     * @param remark
     * @throws BizException
     */
    @Transactional(rollbackFor = Exception.class)
    public void cancelCreditToPointAccountTcc(TransactionContext transactionContext, String userNo, Integer pointAmount, String requestNo, String bankTrxNo, String trxType, String remark) throws BizException {
        LOG.info("===>cancelCreditToPointAccountTcc begin");
        pointAccountHistory pointAccountHistory = pointAccountHistoryDao.getByRequestNo(requestNo);
        // 冪等判斷
        if ( pointAccountHistory == null  || !PointAccountHistoryStatusEnum.TRYING.name().equals(pointAccountHistory.getStatus())){//該筆交易流水已處理過,不需再處理
            return;
        }

        pointAccountHistory.setStatus(PointAccountHistoryStatusEnum.CANCEL.name());
        pointAccountHistoryDao.update(pointAccountHistory);
        LOG.info("===>cancelCreditToPointAccountTcc end");
    }

    /**
     * 積分賬戶加款 Trying
     * @param userNo
     * @param pointAmount
     * @param requestNo
     * @param bankTrxNo
     * @param trxType
     * @param remark
     * @throws BizException
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void creditToPointAccount(String userNo, Integer pointAmount, String requestNo, String bankTrxNo, String trxType, String remark) throws BizException {

        //根據商戶編號獲取商戶積分賬戶
        pointAccount pointAccount = pointAccountDao.getByUserNo(userNo);
        if (pointAccount == null){//如果不存在商戶積分賬戶,建立一條新的積分賬戶
            pointAccount = new pointAccount();
            pointAccount.setBalance(0);
            pointAccount.setUserNo(userNo);
            pointAccount.setStatus(PublicEnum.YES.name());
            pointAccount.setCreateTime(new Date());
            pointAccount.setId(StringUtil.get32UUID());
            pointAccountDao.insert(pointAccount);
        }


    //新增一條積分歷史
    pointAccountHistory pointAccountHistory = pointAccountHistoryDao.getByRequestNo(requestNo);
    if ( pointAccountHistory == null ){//防止多次提交
            pointAccountHistory = new pointAccountHistory();
            pointAccountHistory.setId(StringUtil.get32UUID());
            pointAccountHistory.setCreateTime(new Date());
            pointAccountHistory.setStatus(PointAccountHistoryStatusEnum.CONFORM.name());//可用
            pointAccountHistory.setAmount(pointAmount);///積分賬戶變動額
            pointAccountHistory.setBalance(pointAccount.getBalance() + pointAmount);
            pointAccountHistory.setBankTrxNo(bankTrxNo);//銀行流水號
            pointAccountHistory.setRequestNo(requestNo);//請求號
            pointAccountHistory.setFundDirection(PointAccountFundDirectionEnum.ADD.name());
            pointAccountHistory.setTrxType(trxType);
            pointAccountHistory.setRemark(remark);
            pointAccountHistory.setUserNo(userNo);
            pointAccountHistoryDao.insert(pointAccountHistory);
        }

        //增加積分賬戶
        pointAccount.setBalance(pointAccount.getBalance() + pointAmount);//增加賬戶餘額
        pointAccountDao.update(pointAccount);
    }

    @Override
    public pointAccount getDataById(String id) {
        return pointAccountDao.getById(id);
    }

    @Override
    public PageBean listPage(PageParam pageParam, pointAccount pointAccount) {
        Map<String, Object> paramMap = new HashMap<String, Object>();
        return pointAccountDao.listPage(pageParam, paramMap);
    }
}

相關文章