天天看點

分布式事務解決方案之TCC一、什麼是TCC事務二、TCC解決方案三、Hmily實作TCC事務 四、總結

一、什麼是TCC事務

TCC是Try、Confirm、Cancel三個詞語的縮寫,

TCC要求每個分支事務實作三個操作:預處理Try、确認Confirm、撤銷Cancel。

Try操作做業務檢查及資源預留,Confirm做業務确認操作,Cancel實作一個與Try相反的操作即復原操作。

TM首先發起所有的分支事務的try操作,任何一個分支事務的try操作執行失敗,TM将會發起所有分支事務的Cancel操作,

若try操作全部成功,TM将會發起所有分支事務的Confirm操作,其中Confirm/Cancel操作若執行失敗,TM會進行重試。

分布式事務解決方案之TCC一、什麼是TCC事務二、TCC解決方案三、Hmily實作TCC事務 四、總結
 分支事務失敗的情況:
分布式事務解決方案之TCC一、什麼是TCC事務二、TCC解決方案三、Hmily實作TCC事務 四、總結

TCC分為三個階段:

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

2. Confirm 階段是做确認送出,Try階段所有分支事務執行成功後開始執行 Confirm。通常情況下,采用TCC則認為 Confirm階段是不會出錯的。

即:隻要Try成功,Confirm一定成功。若Confirm階段真的出錯了,需引入重試機制或人工處理。

3. Cancel 階段是在業務執行錯誤需要復原的狀态下執行分支事務的業務取消,預留資源釋放。

通常情況下,采用TCC則認為Cancel階段也是一定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理。

4. TM事務管理器

TM事務管理器可以實作為獨立的服務,也可以讓全局事務發起方充當TM的角色,TM獨立出來是為了成為公用元件,是為了考慮系統結構和軟體複用。

TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分布式事務調用鍊條,用來記錄事務上下文,追蹤和記錄狀态,

由于 Confirm 和 Cancel 失敗需進行重試,是以需要實作為幂等,幂等性是指同一個操作無論請求多少次,其結果都相同。

二、TCC解決方案

目前市面上的TCC架構衆多比如下面這幾種:

(以下資料采集日為2021年08月04日)

架構名稱 Github位址 star數量
tcc-transaction https://github.com/changmingxie/tcc-transaction 5200
Hmily https://github.com/dromara/hmily 3700
ByteTCC https://github.com/liuyangming/ByteTCC 2700
EasyTransaction https://github.com/QNJR-GROUP/EasyTransaction 2300

上一節所講的Seata也支援TCC,但Seata的TCC模式對Spring Cloud并沒有提供支援。我們的目标是了解TCC的原理以及事務協調運作的過程,

是以更請傾向于輕量級易于了解的架構,是以最終确定了Hmily。

Hmily是一個高性能分布式事務TCC開源架構。基于Java語言來開發(JDK1.8),支援Dubbo,Spring Cloud等RPC架構進行分布式事務。它目前支援以下特性:

1.支援嵌套事務(Nested transaction support)。

2.采用disruptor架構進行事務日志的異步讀寫,與RPC架構的性能毫無差别。

3.支援SpringBoot-starter 項目啟動,使用簡單。

4.RPC架構支援 : dubbo,motan,springcloud。

5.本地事務存儲支援 : redis,mongodb,zookeeper,file,mysql。

6.事務日志序列化支援 :java,hessian,kryo,protostuff。

7.采用Aspect AOP 切面思想與Spring無縫內建,天然支援叢集。

8.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,confirm,cancel方法實作幂等。

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:
        空
           

三、Hmily實作TCC事務

3.1 業務說明

本執行個體通過Hmily實作TCC分布式事務,模拟兩個賬戶的轉賬交易過程。

兩個賬戶分别在不同的銀行(張三在bank1、李四在bank2),bank1、bank2是兩個微服務。

交易過程是,張三給李四轉賬指定金額。

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

分布式事務解決方案之TCC一、什麼是TCC事務二、TCC解決方案三、Hmily實作TCC事務 四、總結

3.2 程式組成部分

資料庫:MySQL-5.7.25

JDK:64位 jdk1.8.0_201

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

Hmily:hmily-springcloud.2.0.4-RELEASE

微服務及資料庫的關系 :

tcc-demo-bank1 銀行1,操作張三賬戶, 連接配接資料庫bank1

tcc-demo-bank2 銀行2,操作李四賬戶,連接配接資料庫bank2

服務注冊中心:eureka-server

3.3 建立資料庫

建立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;
           

3.4 eureka-server

eureka-server是服務注冊中心,測試工程将自己注冊至discover-server。

3.5 建構案例工程tcc-demo-bank

3.5.1 建構tcc-demo-bank

兩個測試工程如下:

tcc-demo-bank1 銀行1,操作張三賬戶,連接配接資料庫bank1

tcc-demo-bank2 銀行2,操作李四賬戶,連接配接資料庫bank2

3.5.2 引入maven依賴

依賴下載下傳不下來

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

3.5.3 配置hmily

server:
  port: 5555

# 注冊到eureka服務端的微服務名稱
spring:
  application:
    name: tcc-demo-bank1
  datasource:
    url: jdbc:mysql://localhost:3306/bank1?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT user()
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    connection-properties: druid.stat.mergeSql:true;druid.stat.slowSqlMillis:5000


eureka:
  instance:
    # 将ip注冊到eureka server上,ip替代hostname
    prefer-ip-address: true
    # 顯示微服務的服務執行個體id
    instance-id: tcc-demo-bank1-${server.port}
  client:
    register-with-eureka: true
    fetchRegistry: true
    service-url:
      # 注冊到eureka服務端的位址
      defaultZone: http://127.0.0.1:2222/eureka

org:
  dromara:
    hmily :
      serializer : kryo
      recoverDelayTime : 128
      retryMax : 30
      scheduledDelay : 128
      scheduledThreadMax : 10
      repositorySupport : db
      started: true
      hmilyDbConfig :
        driverClassName : com.mysql.cj.jdbc.Driver
        url : jdbc:mysql://localhost:3306/hmily?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
        username : root
        password : root

ribbon:
  # 設定連接配接逾時時間 default 2000
  ConnectTimeout: 6000
  # 設定讀取逾時時間  default 5000
  ReadTimeout: 6000
  # 對所有操作請求都進行重試  default false
  OkToRetryOnAllOperations: true
  # 切換執行個體的重試次數  default 1
  MaxAutoRetriesNextServer: 2
  # 對目前執行個體的重試次數 default 0
  MaxAutoRetries: 1
           

3.5.4 配置類

新增配置類接收application.yml中的Hmily配置資訊,并建立HmilyTransactionBootstrap Bean:
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class HmilyConfiguration {

    @Autowired
    private Environment env;

    @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.hmily.recoverDelayTime")));
        hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retryMax")));
        hmilyTransactionBootstrap.setScheduledDelay(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledDelay")));
        hmilyTransactionBootstrap.setScheduledThreadMax(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledThreadMax")));
        hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySupport"));
        hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.started")));
        HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
        hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassName"));
        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;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource ds1() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
}
           
啟動類增加@EnableAspectJAutoProxy并增加org.dromara.hmily的掃描項:
@ComponentScan({"com.best", "org.dromara.hmily"})
@EnableEurekaClient
@EnableFeignClients
@SpringBootApplication(exclude = MongoAutoConfiguration.class) // 取消資料源自動建立的配置
public class EurekaTccDemoBank1Application {
    public static void main(String[] args) {
        SpringApplication.run(EurekaTccDemoBank1Application.class, args);
    }
}
           

3.6 tcc-demo-bank1

tcc-demo-bank1實作try和cancel方法,如下:
try:
    try幂等校驗
    try懸挂處理
    檢查餘額是夠扣減金額
    扣減金額
confirm:
    空
cancel:
    cancel幂等校驗
    cancel空復原處理
    增加可用餘額
           

3.6.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 本地事務編号
     */
    @Insert("insert into local_try_log values(#{txNo}, now());")
    int addTry(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務Confirm執行記錄
     *
     * @param localTradeNo 本地事務編号
     */
    @Insert("insert into local_confirm_log values(#{txNo}, now());")
    int addConfirm(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務Cancel執行記錄
     *
     * @param localTradeNo 本地事務編号
     */
    @Insert("insert into local_cancel_log values(#{txNo}, now());")
    int addCancel(@Param("txNo") String localTradeNo);

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

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

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

3.6.2 try和cancel方法

@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Resource
    private AccountInfoDao accountInfoDao;

    @Resource
    private Bank2Client bank2Client;

    // 張三轉賬,就是tcc的try方法
    @Override
    @Transactional
    // 隻要标記@Hmily就是try方法,在注解中指定confirm、cancel兩個方法的名字
    @Hmily(confirmMethod = "confirm", cancelMethod = "cancel")
    public void updateAccountBalance(String accountNo, Double amount) {

        // 擷取全局事務id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();

        log.info("bank1 try begin 開始執行...xid:{}", transId);

        int existTry = accountInfoDao.isExistTry(transId);

        // 幂等判斷 判斷local_try_log表中是否有try日志記錄,如果有則不再執行
        if (existTry > 0) {
            log.info("bank1 try 已經執行,無需重複執行,xid:{}", transId);
            return;
        }

        // try懸挂處理,如果cancel、confirm有一個已經執行了,try不再執行
        if (accountInfoDao.isExistConfirm(transId) > 0 || accountInfoDao.isExistCancel(transId) > 0) {
            log.info("bank1 try懸挂處理  cancel或confirm已經執行,不允許執行try,xid:{}", transId);
            return;
        }

        // 扣減金額
        if (accountInfoDao.subtractAccountBalance(accountNo, amount * -1) <= 0) {
            //扣減失敗
            throw new RuntimeException("bank1 try 扣減金額失敗,xid:{}" + transId);
        }

        // 增加本地事務try成功記錄,用于幂等判斷
        accountInfoDao.addTry(transId);

        // 遠端調用bank2
        if (!bank2Client.transfer(amount)) {
            throw new RuntimeException("bank1 遠端調用李四微服務失敗,xid:{}" + transId);
        }
        if (amount == 2) {
            throw new RuntimeException("人為制造異常,xid:{}" + transId);
        }
        log.info("bank1 try end 結束執行...xid:{}", transId);
    }

    // confirm方法
    @Override
    @Transactional
    public void confirm(String accountNo, Double amount) {
        // 擷取全局事務id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("bank1 confirm begin 開始執行...xid:{},accountNo:{},amount:{}", transId, accountNo, amount);
    }

    // cancel方法
    @Override
    @Transactional
    public void cancel(String accountNo, Double amount) {

        //擷取全局事務id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();

        log.info("bank1 cancel begin 開始執行...xid:{}", transId);

        //  cancel幂等校驗,已經執行過了,什麼也不用做
        if (accountInfoDao.isExistCancel(transId) > 0) {
            log.info("bank1 cancel 已經執行,無需重複執行,xid:{}", transId);
            return;
        }

        // cancel空復原處理,如果try沒有執行,cancel不允許執行
        if (accountInfoDao.isExistTry(transId) <= 0) {
            log.info("bank1 空復原處理,try沒有執行,不允許cancel執行,xid:{}", transId);
            return;
        }

        // 再将金額加回賬戶
        accountInfoDao.addAccountBalance(accountNo, amount);

        // 添加cancel日志,用于幂等性控制辨別
        accountInfoDao.addCancel(transId);
        log.info("bank1 cancel end 結束執行...xid:{}", transId);
    }

}
           

3.6.4 feignClient

@FeignClient(value = "tcc-demo-bank2", fallback = Bank2ClientFallback.class)
public interface Bank2Client {

    // 遠端調用李四的微服務
    @GetMapping("/bank2/transfer")
    @Hmily
    Boolean transfer(@RequestParam("amount") Double amount);
}
           
@Component
public class Bank2ClientFallback implements Bank2Client {
    @Override
    public Boolean transfer(Double amount) {
        return false;
    }
}
           

3.6.5 Controller

@RestController
@RequestMapping("/bank1")
public class Bank1Controller {

    @Resource
    private AccountInfoService accountInfoService;

    @GetMapping("/transfer")
    public Boolean transfer(@RequestParam("amount") Double amount){
        accountInfoService.updateAccountBalance("1", amount);
        return true;
    }
}
           

3.7 tcc-demo-bank2

dtx-tcc-demo-bank2實作如下功能:
try:
    空
confirm:
    confirm幂等校驗
    正式增加金額
cancel:
    空
           

3.7.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 本地事務編号
     */
    @Insert("insert into local_try_log values(#{txNo}, now());")
    int addTry(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務Confirm執行記錄
     *
     * @param localTradeNo 本地事務編号
     */
    @Insert("insert into local_confirm_log values(#{txNo}, now());")
    int addConfirm(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務Cancel執行記錄
     *
     * @param localTradeNo 本地事務編号
     */
    @Insert("insert into local_cancel_log values(#{txNo}, now());")
    int addCancel(@Param("txNo") String localTradeNo);

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

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

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

3.7.2 實作confirm方法

@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Resource
    private AccountInfoDao accountInfoDao;

    @Override
    @Transactional
    @Hmily(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
    public void updateAccountBalance(String accountNo, Double amount) {
        // 擷取全局事務id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("******** Bank2 Service Begin try ..." + transId);
    }

    @Override
    @Transactional
    public void confirmMethod(String accountNo, Double amount) {
        // 擷取全局事務id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();

        // 幂等性校驗,已經執行過了,什麼也不用做
        log.info("******** Bank2 Service commit... " + transId);
        if (accountInfoDao.isExistConfirm(transId) > 0) {
            log.info("******** Bank2 confirm 已經執行,無需重複執行...xid:{}", transId);
            return;
        }

        // 正式增加金額
        accountInfoDao.addAccountBalance(accountNo, amount);

        // 增加一條confirm日志,用于幂等校驗
        accountInfoDao.addConfirm(transId);
    }

    @Override
    @Transactional
    public void cancelMethod(String accountNo, Double amount) {
        // 擷取全局事務id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("******** Bank2 Service Begin cancel... " + transId);
    }
}
           

3.7.3 Controller

@RestController
@RequestMapping("/bank2")
public class Bank2Controller {

    @Resource
    private AccountInfoService accountInfoService;

    // 接收張三的轉賬
    @GetMapping("/transfer")
    public Boolean transfer(@RequestParam("amount") Double amount) {

        // 李四增加金額
        accountInfoService.updateAccountBalance("2", amount);

        return true;
    }
}
           

3.8 測試場景

張三向李四轉賬成功。

李四事務失敗,張三事務復原成功。

張三事務失敗,李四分支事務復原成功。

分支事務逾時測試。

http://localhost:5555/bank1/transfer?amount=1

http://localhost:5555/bank1/transfer?amount=2

分布式事務解決方案之TCC一、什麼是TCC事務二、TCC解決方案三、Hmily實作TCC事務 四、總結

 四、總結

如果拿TCC事務的處理流程與2PC兩階段送出做比較,2PC通常都是在跨庫的DB層面,而TCC則在應用層面的處理,需要通過業務邏輯來實作。

這種分布式事務的實作方式的優勢在于,可以讓應用自己定義資料操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。

而不足之處則在于對應用的侵入性非常強,業務邏輯的每個分支都需要實作try、confirm、cancel三個操作。

此外,其實作難度也比較大,需要按照網絡狀态、系統故障等不同的失敗原因實作不同的復原政策。

視訊教程、參考部落格、gitt源碼​​​​​​​

繼續閱讀