分佈式事務二TCC

分佈式事務解決方案之TCC

4.1.什麼是TCC事務

  TCC是Try、Confirm、Cancel三個詞語的縮寫,TCC要求每一個分支事務實現三個操做:預處理Try、確認Confirm、撤銷Cancel。Try操做作業務檢查及資源預留,Confirm作業務確認操做,Cancel實現一個與Try相反的操做即回滾操做。TM首先發起全部的分支事務的try操做,任何一個分支事務的try操做執行失敗,TM將會發起全部分支事務的Cancel操做,若try操做所有成功,TM將會發起全部分支事務的Confirm操做,其中Confirm/Cancel操做若執行失敗,TM會進行重試。html

分支事務失敗的狀況:java

TCC分爲三個階段:mysql

  1. Try 階段是作業務檢查(一致性)及資源預留(隔離),此階段僅是一個初步操做,它和後續的Confirm 一塊兒才能真正構成一個完整的業務邏輯。web

  2. Confirm 階段是作確認提交,Try階段全部分支事務執行成功後開始執行 Confirm。一般狀況下,採用TCC則認爲 Confirm階段是不會出錯的。即:只要Try成功,Confirm必定成功。若Confirm階段真的出錯了,需引入重試機制或人工處理。redis

  3. Cancel 階段是在業務執行錯誤須要回滾的狀態下執行分支事務的業務取消,預留資源釋放。一般狀況下,採用TCC則認爲Cancel階段也是必定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理。spring

4. TM事務管理器

  TM事務管理器能夠實現爲獨立的服務,也可讓全局事務發起方充當TM的角色,TM獨立出來是爲了成爲公用組件,是爲了考慮系統結構和軟件複用。sql

  TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分佈式事務調用鏈條,用來記錄事務上下文,追蹤和記錄狀態,因爲Confirm 和cancel失敗需進行重試,所以須要實現爲冪等,冪等性是指同一個操做不管請求多少次,其結果都相同。mongodb

4.2.TCC 解決方案

目前市面上的TCC框架衆多好比下面這幾種:數據庫

(如下數據採集日爲2019年07月11日)網絡

  

  上一節所講的Seata也支持TCC,但Seata的TCC模式對Spring Cloud並無提供支持。咱們的目標是理解TCC的原理以及事務協調運做的過程,所以更請傾向於輕量級易於理解的框架,所以最終肯定了Hmily。

  Hmily是一個高性能分佈式事務TCC開源框架。基於Java語言來開發(JDK1.8),支持Dubbo,Spring Cloud等

  RPC框架進行分佈式事務。它目前支持如下特性:

  ①支持嵌套事務(Nested transaction support).

  ②採用disruptor框架進行事務日誌的異步讀寫,與RPC框架的性能毫無差異。

  ③支持SpringBoot-starter 項目啓動,使用簡單。

  ④RPC框架支持 : dubbo,motan,springcloud。

  ⑤本地事務存儲支持 : redis,mongodb,zookeeper,file,mysql。

  ⑥事務日誌序列化支持 :java,hessian,kryo,protostuff。

  ⑦採用Aspect AOP 切面思想與Spring無縫集成,自然支持集羣。

  ⑧RPC事務恢復,超時異常恢復等。

Hmily利用AOP對參與分佈式事務的本地方法與遠程方法進行攔截處理,經過多方攔截,事務參與者能透明的調用到另外一方的Try、Confirm、Cancel方法;傳遞事務上下文;並記錄事務日誌,酌情進行補償,重試等。

  Hmily不須要事務協調服務,但須要提供一個數據庫(mysql/mongodb/zookeeper/redis/file)來進行日誌存儲。

Hmily實現的TCC服務與普通的服務同樣,只須要暴露一個接口,也就是它的Try業務。Confirm/Cancel業務邏輯,只是由於全局事務提交/回滾的須要才提供的,所以Confirm/Cancel業務只須要被Hmily TCC事務框架發現便可,不須要被調用它的其餘業務服務所感知。

官網介紹:https://dromara.org/website/zh-cn/docs/hmily/index.html

TCC須要注意三種異常處理分別是空回滾、冪等、懸掛:

空回滾:

  在沒有調用 TCC 資源 Try 方法的狀況下,調用了二階段的 Cancel 方法,Cancel 方法須要識別出這是一個空回滾,而後直接返回成功。

  出現緣由是當一個分支事務所在服務宕機或網絡異常,分支事務調用記錄爲失敗,這個時候實際上是沒有執行Try階段,當故障恢復後,分佈式事務進行回滾則會調用二階段的Cancel方法,從而造成空回滾。

  解決思路是關鍵就是要識別出這個空回滾。思路很簡單就是須要知道一階段是否執行,若是執行了,那就是正常回滾;若是沒執行,那就是空回滾。前面已經說過TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分佈式事務調用鏈條。再額外增長一張分支事務記錄表,其中有全局事務 ID 和分支事務 ID,第一階段 Try 方法裏會插入一條記錄,表示一階段執行了。Cancel 接口裏讀取該記錄,若是該記錄存在,則正常回滾;若是該記錄不存在,則是空回滾。

冪等:

  經過前面介紹已經瞭解到,爲了保證TCC二階段提交重試機制不會引起數據不一致,要求 TCC 的二階段 Try、 Confirm 和 Cancel 接口保證冪等,這樣不會重複使用或者釋放資源。若是冪等控制沒有作好,頗有可能致使數據不一致等嚴重問題。

  解決思路在上述「分支事務記錄」中增長執行狀態,每次執行前都查詢該狀態。

懸掛:

  懸掛就是對於一個分佈式事務,其二階段 Cancel 接口比 Try 接口先執行。

  出現緣由是在 RPC 調用分支事務try時,先註冊分支事務,再執行RPC調用,若是此時 RPC 調用的網絡發生擁堵,一般 RPC 調用是有超時時間的,RPC 超時之後,TM就會通知RM回滾該分佈式事務,可能回滾完成後,RPC 請求才到達參與者真正執行,而一個 Try 方法預留的業務資源,只有該分佈式事務才能使用,該分佈式事務第一階段預留的業務資源就再也沒有人可以處理了,對於這種狀況,咱們就稱爲懸掛,即業務資源預留後無法繼續處理。

  解決思路是若是二階段執行完成,那一階段就不能再繼續執行。在執行一階段事務時判斷在該全局事務下,「分支事務記錄」表中是否已經有二階段事務記錄,若是有則不執行Try。

舉例,場景爲 A 轉帳 30 元給 B,A和B帳戶在不一樣的服務。

  方案1:

帳戶A

try:
  檢查餘額是否夠30元
  扣減30元
confirm:
  空
cancel:
  增長30元

帳戶B 

try:
  增長30元
confirm:
  空
cancel:
  減小30元

方案1說明:

  1)帳戶A,這裏的餘額就是所謂的業務資源,按照前面提到的原則,在第一階段須要檢查並預留業務資源,所以,咱們在扣錢 TCC 資源的 Try 接口裏先檢查 A 帳戶餘額是否足夠,若是足夠則扣除 30 元。 Confirm 接口表示正式提交,因爲業務資源已經在 Try 接口裏扣除掉了,那麼在第二階段的 Confirm 接口裏能夠什麼都不用作。Cancel接口的執行表示整個事務回滾,帳戶A回滾則須要把 Try 接口裏扣除掉的 30 元還給帳戶。

  2)帳號B,在第一階段 Try 接口裏實現給帳戶B加錢,Cancel 接口的執行表示整個事務回滾,帳戶B回滾則須要把 Try 接口裏加的 30 元再減去。

方案1的問題分析:

  1)若是帳戶A的try沒有執行在cancel則就多加了30元。

  2)因爲try,cancel、confirm都是由單獨的線程去調用,且會出現重複調用,因此都須要實現冪等。

  3)帳號B在try中增長30元,當try執行完成後可能會其它線程給消費了。

  4)若是帳戶B的try沒有執行在cancel則就多減了30元。

問題解決:

  1)帳戶A的cancel方法須要判斷try方法是否執行,正常執行try後方可執行cancel。

  2)try,cancel、confirm方法實現冪等。

  3)帳號B在try方法中不容許更新帳戶金額,在confirm中更新帳戶金額。

  4)帳戶B的cancel方法須要判斷try方法是否執行,正常執行try後方可執行cancel。

優化方案:

帳戶A

try: 
  try冪等校驗
  try懸掛處理
  檢查餘額是否夠30元
  扣減30元

confirm:
  空

cancel:
  cancel冪等校驗
  cancel空回滾處理
  增長可用餘額30元

帳戶B

try:
    空
confirm:
    confirm冪等校驗
    正式增長30元
cancel:
    空

4.3.Hmily實現TCC事務

4.3.1.業務說明

  本實例經過Hmily實現TCC分佈式事務,模擬兩個帳戶的轉帳交易過程。

兩個帳戶分別在不一樣的銀行(張三在bank一、李四在bank2),bank一、bank2是兩個微服務。交易過程是,張三給李四轉帳指定金額。

上述交易步驟,要麼一塊兒成功,要麼一塊兒失敗,必須是一個總體性的事務。

4.3.2.程序組成部分

  數據庫:MySQL-5.7.25

  JDK:64位 jdk1.8.0_201

  微服務:spring-boot-2.1.三、spring-cloud-Greenwich.RELEASE

  Hmily:hmily-springcloud.2.0.4-RELEASE

微服務及數據庫的關係 :

  dtx/dtx-tcc-demo/dtx-tcc-demo-bank1 銀行1,操做張三帳戶, 鏈接數據庫

  bank1 dtx/dtx-tcc-demo/dtx-tcc-demo-bank2 銀行2,操做李四帳戶,鏈接數據庫

  bank2服務註冊中心:dtx/discover-server

4.3.3.建立數據庫

  導入數據庫腳本:資料\sql\bank1.sql、資料\sql\bank2.sql、已經導過不用重複導入。

建立hmily數據庫,用於存儲hmily框架記錄的數據。

CREATE DATABASE `hmily` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

建立bank1庫,並導入如下表結構和數據(包含張三帳戶)

CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info`   (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '戶主姓名',
    `account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '銀行卡號',
    `account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '賬戶密碼',
    `account_balance` double NULL DEFAULT NULL COMMENT '賬戶餘額', PRIMARY KEY (`id`) USING BTREE
)  ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
INSERT INTO `account_info` VALUES (2, '張三的帳戶', '1', '', 10000);

建立bank2庫,並導入如下表結構和數據(包含李四帳戶)

CREATE DATABASE `bank2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
CREATE TABLE `account_info`   (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '戶主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '銀行卡號',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '賬戶密碼',
`account_balance` double NULL DEFAULT NULL COMMENT '賬戶餘額', PRIMARY KEY (`id`) USING BTREE
)  ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
INSERT INTO `account_info` VALUES (3, '李四的帳戶', '2', NULL, 0);

每一個數據庫都建立try、confirm、cancel三張日誌表:

CREATE TABLE `local_try_log` (
    `tx_no` varchar(64) NOT NULL COMMENT '事務id',
    `create_time` datetime DEFAULT NULL,
    PRIMARY KEY (`tx_no`)
)  ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `local_confirm_log` ( `tx_no` varchar(
64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `local_cancel_log` ( `tx_no` varchar(
64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8

4.3.5 discover-server

  discover-server是服務註冊中心,測試工程將本身註冊至discover-server。

導入:資料\基礎代碼\dtx 父工程,此工程自帶了discover-server,discover-server基於Eureka實現。

已經導過不用重複導入。

4.3.6 導入案例工程dtx-tcc-demo

  dtx-tcc-demo是tcc的測試工程,根據業務需求須要建立兩個dtx-tcc-demo工程。

(1)導入dtx-tcc-demo

  導入:資料\基礎代碼\dtx-tcc-demo到父工程dtx下。

兩個測試工程以下:

  dtx/dtx-tcc-demo/dtx-tcc-demo-bank1 銀行1,操做張三帳戶,鏈接數據庫bank1

  dtx/dtx-tcc-demo/dtx-tcc-demo-bank2 銀行2,操做李四帳戶,鏈接數據庫bank2

(2)引入maven依賴

<dependency>
    <groupId>org.dromara</groupId>
    <artifactId>hmily‐springcloud</artifactId>
    <version>2.0.4‐RELEASE</version>
</dependency>

(3)配置hmily

application.yml:

org:
    dromara:
        hmily :
            serializer : kryo
            recoverDelayTime : 128
            retryMax : 30
            scheduledDelay : 128
            scheduledThreadMax :    10
            repositorySupport : db
            started: true
            hmilyDbConfig :
                driverClassName : com.mysql.jdbc.Driver
                url :   jdbc:mysql://localhost:3306/bank?useUnicode=true
                username : root
                password : root

 

新增配置類接收application.yml中的Hmily配置信息,並建立HmilyTransactionBootstrap Bean:

@Bean
public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){ 
    HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
    hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
    hmilyTransactionBootstrap.setRecoverDelayTime(Integer.parseInt(env.getProperty("org.dromara.hmi ly.recoverDelayTime")));
    hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retry Max")));    
    hmilyTransactionBootstrap.setScheduledDelay(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledDelay")));
    hmilyTransactionBootstrap.setScheduledThreadMax(Integer.parseInt(env.getProperty("org.dromara.h mily.scheduledThreadMax")));
    hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySup port"));
    hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.st arted")));
    HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
    hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassNa me"));
    hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url")); 
    hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username")); 
    hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password")); 
    hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
    return hmilyTransactionBootstrap;
}

啓動類增長@EnableAspectJAutoProxy並增長org.dromara.hmily的掃描項:

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages = {"cn.itcast.dtx.tccdemo.bank1.spring"}) 
@ComponentScan({"cn.itcast.dtx.tccdemo.bank1","org.dromara.hmily"}) 
public class Bank1HmilyServer {
    public static void main(String[] args) { 
        SpringApplication.run(Bank1HmilyServer.class, args);
    }
} 
4.3.7 dtx-tcc-demo-bank1

dtx-tcc-demo-bank1實現try和cancel方法,以下:

try:
    try冪等校驗
    try懸掛處理
    檢查餘額是夠扣減金額
    扣減金額
confirm:
    空
cancel:
    cancel冪等校驗
    cancel空回滾處理
    增長可用餘額

1)Dao

@Mapper
@Component
public interface AccountInfoDao {
    @Update("update account_info set account_balance=account_balance ‐ #{amount} where account_balance>#{amount} and account_no=#{accountNo} ")
    int subtractAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);

    @Update("update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo} ")
    int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);

    /**
    *  增長某分支事務try執行記錄
    *  @param localTradeNo 本地事務編號
    *  @return
    */
    @Insert("insert into local_try_log values(#{txNo},now());")
    int addTry(String localTradeNo);
    @Insert("insert into local_confirm_log values(#{txNo},now());")
    int addConfirm(String localTradeNo);
    @Insert("insert into local_cancel_log values(#{txNo},now());")
    int addCancel(String localTradeNo);

    /**
    *  查詢分支事務try是否已執行
    *  @param localTradeNo 本地事務編號
    *  @return
    */
    @Select("select count(1) from local_try_log where tx_no = #{txNo} ")
    int isExistTry(String localTradeNo);

    /**
    *  查詢分支事務confirm是否已執行
    *  @param localTradeNo 本地事務編號
    *  @return
    */
    @Select("select count(1) from local_confirm_log where tx_no = #{txNo} ")
    int isExistConfirm(String localTradeNo);

    /**
    *  查詢分支事務cancel是否已執行
    *  @param localTradeNo 本地事務編號
    *  @return
    */
    @Select("select count(1) from local_cancel_log where tx_no = #{txNo} ")
    int isExistCancel(String localTradeNo);
}

2)try和cancel方法

@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    private Logger logger = LoggerFactory.getLogger(AccountInfoServiceImpl.class);

    @Autowired
    private AccountInfoDao accountInfoDao;

    @Autowired
    private Bank2Client bank2Client;

    @Override
    @Transactional
    @Hmily(confirmMethod = "commit", cancelMethod = "rollback")
    public void updateAccountBalance(String accountNo, Double amount) { 
        //事務id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId(); log.info("******** Bank1 Service begin try... "+transId );
        int existTry = accountInfoDao.isExistTry(transId); //try冪等校驗
        if(existTry>0){
            log.info("******** Bank1 Service 已經執行try,無需重複執行,事務id:{} "+transId ); return ;
        }    
        //try懸掛處理
        if(accountInfoDao.isExistCancel(transId)>0 || accountInfoDao.isExistConfirm(transId)>0){ 
            log.info("******** Bank1 Service 已經執行confirm或cancel,懸掛處理,事務id:{}  "+transId);
            return ;
        }

        //從帳戶扣減
        if(accountInfoDao.subtractAccountBalance(accountNo ,amount )<=0){ 
            //扣減失敗
            throw new HmilyRuntimeException("bank1 exception,扣減失敗,事務id:{}"+transId);
        }
        //增長本地事務try成功記錄,用於冪等性控制標識
        accountInfoDao.addTry(transId);
        //遠程調用bank2
        if(!bank2Client.test2(amount,transId)){
            throw new HmilyRuntimeException("bank2Client exception,事務id:{}"+transId);
        }
        if(amount==10){
            //異常必定要拋在Hmily裏面
            throw new RuntimeException("bank1 make exception    10");
        }
        log.info("******** Bank1 Service    end try...    "+transId );
    }

    @Transactional
    public    void commit( String accountNo, double amount) {
        String localTradeNo = HmilyTransactionContextLocal.getInstance().get().getTransId();
        logger.info("******** Bank1 Service begin commit..."+localTradeNo );
    }

    @Transactional
    public void rollback( String accountNo, double amount) {
        String localTradeNo = HmilyTransactionContextLocal.getInstance().get().getTransId(); 
        log.info("******** Bank1 Service begin rollback... " +localTradeNo); 
        if(accountInfoDao.isExistTry(localTradeNo) == 0){ 
            //空回滾處理,try階段沒有執行什麼也不用作
            log.info("******** Bank1 try階段失敗... 無需rollback "+localTradeNo ); return;
        }
        if(accountInfoDao.isExistCancel(localTradeNo) > 0){ 
            //冪等性校驗,已經執行過了,什麼也不用作 
            log.info("******** Bank1 已經執行過rollback... 無需再次rollback " +localTradeNo); 
            return;
        }
        //再將金額加回帳戶
        accountInfoDao.addAccountBalance(accountNo,amount); 
        //添加cancel日誌,用於冪等性控制標識 
        accountInfoDao.addCancel(localTradeNo);
        log.info("******** Bank1 Service end rollback...    " +localTradeNo);
    }
}

3)feignClient

@FeignClient(value = "seata‐demo‐bank2", fallback = Bank2Fallback.class)
public interface Bank2Client {
    @GetMapping("/bank2/transfer")
    @Hmily
    Boolean transfer(@RequestParam("amount") Double amount);
}

4) Controller

@RestController
public class Bank1Controller {

    @Autowired
    AccountInfoService accountInfoService;

    @RequestMapping("/transfer")
    public String test(@RequestParam("amount") Double amount) { 
        this.accountInfoService.updateAccountBalance("1", amount);
        return "cn/itcast/dtx/tccdemo/bank1" + amount;
    }
}
4.3.8 dtx-tcc-demo-bank2

dtx-tcc-demo-bank2實現以下功能:

try:
    空
confirm:
    confirm冪等校驗
    正式增長金額
cancel:
    空

1)Dao

@Component
@Mapper
public interface AccountInfoDao {

    @Update("update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo} ")
    int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);

    /**
    *    增長某分支事務try執行記錄
    *    @param localTradeNo 本地事務編號
    *    @return
    */
    @Insert("insert into local_try_log values(#{txNo},now());")
    int addTry(String localTradeNo);

    @Insert("insert into local_confirm_log values(#{txNo},now());")
    int addConfirm(String localTradeNo);

    @Insert("insert into local_cancel_log values(#{txNo},now());")
    int addCancel(String localTradeNo);

    /**
    *    查詢分支事務try是否已執行
    *    @param localTradeNo 本地事務編號
    *    @return
    */
    @Select("select count(1) from local_try_log where tx_no = #{txNo} ")
    int isExistTry(String localTradeNo);

    /**
    *    查詢分支事務confirm是否已執行
    *    @param localTradeNo 本地事務編號
    *    @return
    */
    @Select("select count(1) from local_confirm_log where tx_no = #{txNo} ")
    int isExistConfirm(String localTradeNo);

    /**
    *    查詢分支事務cancel是否已執行
    *    @param localTradeNo 本地事務編號
    *    @return
    */
    @Select("select count(1) from local_cancel_log where tx_no = #{txNo} ")
    int isExistCancel(String localTradeNo);
}

2)實現confirm方法

@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Autowired
    private AccountInfoDao accountInfoDao;

    @Override
    @Transactional
    @Hmily(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
    public void updateAccountBalance(String accountNo, Double amount) {
        String localTradeNo = HmilyTransactionContextLocal.getInstance().get().getTransId(); 
        log.info("******** Bank2 Service Begin try ..."+localTradeNo);
    }

    @Transactional
    public    void confirmMethod(String accountNo, Double amount) {
        String localTradeNo = HmilyTransactionContextLocal.getInstance().get().getTransId(); 
        log.info("******** Bank2 Service commit... " +localTradeNo); 
        if(accountInfoDao.isExistConfirm(localTradeNo) > 0){ 
            //冪等性校驗,已經執行過了,什麼也不用作
            log.info("******** Bank2 已經執行過confirm... 無需再次confirm "+localTradeNo ); 
            return ;
        }
    
        //正式增長金額
        accountInfoDao.addAccountBalance(accountNo,amount);
        //添加confirm日誌
        accountInfoDao.addConfirm(localTradeNo);
    }

    @Transactional
    public    void cancelMethod(String accountNo, Double amount) {
        String localTradeNo = HmilyTransactionContextLocal.getInstance().get().getTransId(); 
        log.info("******** Bank2 Service begin cancel... "+localTradeNo );
    }
}

3)Controller

@RestController
public class Bank2Controller {

    @Autowired
    AccountInfoService accountInfoService;
    
    @RequestMapping("/transfer")
    public Boolean test2(@RequestParam("amount") Double amount) { 
        this.accountInfoService.updateAccountBalance("2", amount);
        return true;
    }
}

3.3.9 測試場景

   張三向李四轉帳成功。

   李四事務失敗,張三事務回滾成功。

   張三事務失敗,李四分支事務回滾成功。

   分支事務超時測試。

4.4.小結

  若是拿TCC事務的處理流程與2PC兩階段提交作比較,2PC一般都是在跨庫的DB層面,而TCC則在應用層面的處理,須要經過業務邏輯來實現。這種分佈式事務的實現方式的優點在於,可讓應用本身定義數據操做的粒度,使得下降鎖衝突、提升吞吐量成爲可能。

  而不足之處則在於對應用的侵入性很是強,業務邏輯的每一個分支都須要實現try、confirm、cancel三個操做。此外,其實現難度也比較大,須要按照網絡狀態、系統故障等不一樣的失敗緣由實現不一樣的回滾策略。

相關文章
相關標籤/搜索