分布式事務(wù)TCC方案——Hmily方案

分布式事務(wù)理論:分布式事務(wù)

分布式事務(wù)解決方案之TCC

TCC是一種比較成熟的分布式事務(wù)解決方案,可用于解決跨庫操作的數(shù)據(jù)一致性問題;

TCC是服務(wù)化的兩階段編程模型,其Try、Confirm、Cancel 3個(gè)方法均由業(yè)務(wù)編碼實(shí)現(xiàn);其中Try操作作為一階段,負(fù)責(zé)資源的檢查和預(yù)留,Confirm操作作為二階段提交操作,執(zhí)行真正的業(yè)務(wù),Cancel是預(yù)留資源的取消;

如下圖所示,業(yè)務(wù)實(shí)現(xiàn)TCC服務(wù)之后,該TCC服務(wù)將作為分布式事務(wù)的其中一個(gè)資源,參與到整個(gè)分布式事務(wù)中;事務(wù)管理器分2階段協(xié)調(diào)TCC服務(wù),在第一階段調(diào)用所有TCC服務(wù)的Try方法,在第二階段執(zhí)行所有TCC服務(wù)的Confirm或者Cancel方法;

什么是TCC事務(wù)

TCC是Try、Confirm、Cancel三個(gè)詞語的縮寫,TCC要求每個(gè)分支事務(wù)實(shí)現(xiàn)三個(gè)操作:預(yù)處理Try、確認(rèn)Confirm、撤銷Cancel。

Try操作做業(yè)務(wù)檢查及資源預(yù)留,Confirm做業(yè)務(wù)確認(rèn)操作,Cancel實(shí)現(xiàn)一個(gè)與Try相反的操作即回滾操作。TM首先發(fā)起所有的分支事務(wù)的try操作,任何一個(gè)分支事務(wù)的try操作執(zhí)行失敗,TM將會(huì)發(fā)起所有分支事務(wù)的Cancel操作,若try操作全部成功,TM將會(huì)發(fā)起所有分支事務(wù)的Confirm操作,其中Confirm/Cancel操作若執(zhí)行失敗,TM會(huì)進(jìn)行重試。

分支事務(wù)失敗的情況:


TCC分為三個(gè)階段:

  • 1、Try 階段是做業(yè)務(wù)檢查(一致性)及資源預(yù)留(隔離),此階段僅是一個(gè)初步操作,它和后續(xù)的Confirm 一起才能真正構(gòu)成一個(gè)完整的業(yè)務(wù)邏輯。

  • 2、Confirm 階段是做確認(rèn)提交,Try階段所有分支事務(wù)執(zhí)行成功后開始執(zhí)行 Confirm。通常情況下,采用TCC則認(rèn)為 Confirm階段是不會(huì)出錯(cuò)的。即:只要Try成功,Confirm一定成功。若Confirm階段真的出錯(cuò)了,需引入重試機(jī)制或人工處理。

  • 3、Cancel 階段是在業(yè)務(wù)執(zhí)行錯(cuò)誤需要回滾的狀態(tài)下執(zhí)行分支事務(wù)的業(yè)務(wù)取消,預(yù)留資源釋放。通常情況下,采用TCC則認(rèn)為Cancel階段也是一定成功的。若Cancel階段真的出錯(cuò)了,需引入重試機(jī)制或人工處理。

    1. TM事務(wù)管理器
      TM事務(wù)管理器可以實(shí)現(xiàn)為獨(dú)立的服務(wù),也可以讓全局事務(wù)發(fā)起方充當(dāng)TM的角色,TM獨(dú)立出來是為了成為公用組件,是為了考慮系統(tǒng)結(jié)構(gòu)和軟件復(fù)用。

TM在發(fā)起全局事務(wù)時(shí)生成全局事務(wù)記錄,全局事務(wù)ID貫穿整個(gè)分布式事務(wù)調(diào)用鏈條,用來記錄事務(wù)上下文,追蹤和記錄狀態(tài),由于Confirm 和cancel失敗需進(jìn)行重試,因此需要實(shí)現(xiàn)為冪等,冪等性是指同一個(gè)操作無論請求多少次,其結(jié)果都相同。

TCC 解決方案

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


Seata也支持TCC,但Seata的TCC模式對Spring Cloud并沒有提供支持。我們的目標(biāo)是理解TCC原理以及事務(wù)協(xié)調(diào)運(yùn)作的過程,因此更傾向于輕量級易于理解的框架。

Hmily是一個(gè)高性能分布式事務(wù)TCC開源框架?;贘ava語言來開發(fā)(JDK1.8),支持Dubbo,Spring Cloud等RPC框架進(jìn)行分布式事務(wù)。它目前支持以下特性 :

  • 支持嵌套事務(wù)(Nested transaction support).
  • 采用disruptor框架進(jìn)行事務(wù)日志的異步讀寫,與RPC框架的性能毫無差別。
  • 支持SpringBoot-starter 項(xiàng)目啟動(dòng),使用簡單。
  • RPC框架支持 : dubbo,motan,springcloud。
  • 本地事務(wù)存儲(chǔ)支持 : redis,mongodb,zookeeper,file,mysql。
  • 事務(wù)日志序列化支持:java,hessian,kryo,protostuff。
  • 采用Aspect AOP 切面思想與Spring無縫集成,天然支持集群。
  • RPC事務(wù)恢復(fù),超時(shí)異?;謴?fù)等。

Hmily利用AOP對參與分布式事務(wù)的本地方法與遠(yuǎn)程方法進(jìn)行攔截處理,通過多方攔截,事務(wù)參與者能透明的調(diào)用到另一方的Try、Confirm、Cancel方法;傳遞事務(wù)上下文;并記錄事務(wù)日志,酌情進(jìn)行補(bǔ)償,重試等。

Hmily不需要事務(wù)協(xié)調(diào)服務(wù),但需要提供一個(gè)數(shù)據(jù)庫(mysql/mongodb/zookeeper/redis/file)來進(jìn)行日志存
儲(chǔ)。

Hmily實(shí)現(xiàn)的TCC服務(wù)與普通的服務(wù)一樣,只需要暴露一個(gè)接口,也就是它的Try業(yè)務(wù)。Confirm/Cancel業(yè)務(wù)邏輯,只是因?yàn)槿质聞?wù)提交/回滾的需要才提供的,因此Confirm/Cancel業(yè)務(wù)只需要被Hmily TCC事務(wù)框架發(fā)現(xiàn)即可,不需要被調(diào)用它的其他業(yè)務(wù)服務(wù)所感知。

官網(wǎng)介紹:https://dromara.org/website/zh-cn/docs/hmily/index.html

GitHub:https://github.com/yu199195/hmily

Gitee:https://gitee.com/shuaiqiyu/hmily

用戶在實(shí)現(xiàn)TCC服務(wù)時(shí),有以下注意事項(xiàng)

1、業(yè)務(wù)操作分兩階段完成:

如下圖所示,接入TCC前,業(yè)務(wù)操作只需要一步就能完成,但是在接入TCC之后,需要考慮如何將其分成2階段完成,把資源的檢查和預(yù)留放在一階段的Try操作中進(jìn)行,把真正的業(yè)務(wù)操作的執(zhí)行放在二階段的Confirm操作中進(jìn)行;


TCC服務(wù)要保證第一階段Try操作成功之后,二階段Confirm操作一定能成功;

2、允許空回滾;

如下圖所示,事務(wù)協(xié)調(diào)器在調(diào)用TCC服務(wù)的一階段Try操作時(shí),可能會(huì)出現(xiàn)因?yàn)閬G包而導(dǎo)致的網(wǎng)絡(luò)超時(shí),此時(shí)事務(wù)協(xié)調(diào)器會(huì)觸發(fā)二階段回滾,調(diào)用TCC服務(wù)的Cancel操作;

TCC服務(wù)在未收到Try請求的情況下收到Cancel請求,這種場景被稱為空回滾;TCC服務(wù)在實(shí)現(xiàn)時(shí)應(yīng)當(dāng)允許空回滾的執(zhí)行;

在沒有調(diào)用 TCC 資源 Try 方法的情況下,調(diào)用了二階段的 Cancel 方法,Cancel 方法需要識(shí)別出這是一個(gè)空回滾,然后直接返回成功。

出現(xiàn)原因是當(dāng)一個(gè)分支事務(wù)所在服務(wù)宕機(jī)或網(wǎng)絡(luò)異常,分支事務(wù)調(diào)用記錄為失敗,這個(gè)時(shí)候其實(shí)是沒有執(zhí)行Try階段,當(dāng)故障恢復(fù)后,分布式事務(wù)進(jìn)行回滾則會(huì)調(diào)用二階段的Cancel方法,從而形成空回滾。

解決思路是關(guān)鍵就是要識(shí)別出這個(gè)空回滾。思路很簡單就是需要知道一階段是否執(zhí)行,如果執(zhí)行了,那就是正常回滾;如果沒執(zhí)行,那就是空回滾。前面已經(jīng)說過TM在發(fā)起全局事務(wù)時(shí)生成全局事務(wù)記錄,全局事務(wù)ID貫穿整個(gè)分布式事務(wù)調(diào)用鏈條。再額外增加一張分支事務(wù)記錄表,其中有全局事務(wù) ID 和分支事務(wù) ID,第一階段 Try 方法里會(huì)插入一條記錄,表示一階段執(zhí)行了。Cancel 接口里讀取該記錄,如果該記錄存在,則正?;貪L;如果該記錄不存在,則是空回滾。

3、防懸掛控制;

如下圖所示,事務(wù)協(xié)調(diào)器在調(diào)用TCC服務(wù)的一階段Try操作時(shí),可能會(huì)出現(xiàn)因網(wǎng)絡(luò)擁堵而導(dǎo)致的超時(shí),此時(shí)事務(wù)協(xié)調(diào)器會(huì)觸發(fā)二階段回滾,調(diào)用TCC服務(wù)的Cancel操作;在此之后,擁堵在網(wǎng)絡(luò)上的一階段Try數(shù)據(jù)包被TCC服務(wù)收到,出現(xiàn)了二階段Cancel請求比一階段Try請求先執(zhí)行的情況;

用戶在實(shí)現(xiàn)TCC服務(wù)時(shí),應(yīng)當(dāng)允許空回滾,但是要拒絕執(zhí)行空回滾之后到來的一階段Try請求;


懸掛就是對于一個(gè)分布式事務(wù),其二階段 Cancel 接口比 Try 接口先執(zhí)行。出現(xiàn)原因是在 RPC 調(diào)用分支事務(wù)try時(shí),先注冊分支事務(wù),再執(zhí)行RPC調(diào)用,如果此時(shí) RPC 調(diào)用的網(wǎng)絡(luò)發(fā)生擁堵,通常 RPC 調(diào)用是有超時(shí)時(shí)間的,RPC 超時(shí)以后,TM就會(huì)通知RM回滾該分布式事務(wù),可能回滾完成后,RPC 請求才到達(dá)參與者真正執(zhí)行,而一個(gè) Try 方法預(yù)留的業(yè)務(wù)資源,只有該分布式事務(wù)才能使用,該分布式事務(wù)第一階段預(yù)留的業(yè)務(wù)資源就再也沒有人能夠處理了,對于這種情況,我們就稱為懸掛,即業(yè)務(wù)資源預(yù)留后沒法繼續(xù)處理。

解決思路是如果二階段執(zhí)行完成,那一階段就不能再繼續(xù)執(zhí)行。在執(zhí)行一階段事務(wù)時(shí)判斷在該全局事務(wù)下,“分支事務(wù)記錄”表中是否已經(jīng)有二階段事務(wù)記錄,如果有則不執(zhí)行Try。

4、冪等控制:

無論是網(wǎng)絡(luò)數(shù)據(jù)包重傳,還是異常事務(wù)的補(bǔ)償執(zhí)行,都會(huì)導(dǎo)致TCC服務(wù)的Try、Confirm或者Cancel操作被重復(fù)執(zhí)行;用戶在實(shí)現(xiàn)TCC服務(wù)時(shí),需要考慮冪等控制,即Try、Confirm、Cancel 執(zhí)行次和執(zhí)行多次的業(yè)務(wù)結(jié)果是一樣的;

通過前面介紹已經(jīng)了解到,為了保證TCC二階段提交重試機(jī)制不會(huì)引發(fā)數(shù)據(jù)不一致,要求 TCC 的二階段 Try、Confirm 和 Cancel 接口保證冪等,這樣不會(huì)重復(fù)使用或者釋放資源。如果冪等控制沒有做好,很有可能導(dǎo)致數(shù)據(jù)不一致等嚴(yán)重問題。

解決思路在上述“分支事務(wù)記錄”中增加執(zhí)行狀態(tài),每次執(zhí)行前都查詢該狀態(tài)。

5、業(yè)務(wù)數(shù)據(jù)可見性控制;

TCC服務(wù)的一階段Try操作會(huì)做資源的預(yù)留,在二階段操作執(zhí)行之前,如果其他事務(wù)需要讀取被預(yù)留的資源數(shù)據(jù),那么處于中間狀態(tài)的業(yè)務(wù)數(shù)據(jù)該如何向用戶展示,需要業(yè)務(wù)在實(shí)現(xiàn)時(shí)考慮清楚;通常的設(shè)計(jì)原則是“寧可不展示、少展示,也不多展示、錯(cuò)展示”;

6、業(yè)務(wù)數(shù)據(jù)并發(fā)訪問控制;

TCC服務(wù)的一階段Try操作預(yù)留資源之后,在二階段操作執(zhí)行之前,預(yù)留的資源都不會(huì)被釋放;如果此時(shí)其他分布式事務(wù)修改這些業(yè)務(wù)資源,會(huì)出現(xiàn)分布式事務(wù)的并發(fā)問題;

用戶在實(shí)現(xiàn)TCC服務(wù)時(shí),需要考慮業(yè)務(wù)數(shù)據(jù)的并發(fā)控制,盡量將邏輯鎖粒度降到最低,以最大限度的提高分布式事務(wù)的并發(fā)性;

舉例,場景為 A 轉(zhuǎn)賬 30 元給 B,A和B賬戶在不同的服務(wù)。

方案1:

賬戶A

try {
    檢查余額是否夠30元
    扣減30元
}
confirm {
    空
}
cancel {
    增加30元
}

賬戶B

try {
    增加30元
}
confirm {
    空
}
cancel {
    減少30元
}
方案1說明:
  • 1、賬戶A,這里的余額就是所謂的業(yè)務(wù)資源,按照前面提到的原則,在第一階段需要檢查并預(yù)留業(yè)務(wù)資源,因此,我們在扣錢 TCC 資源的 Try 接口里先檢查 A 賬戶余額是否足夠,如果足夠則扣除 30 元。 Confirm 接口表示正式提交,由于業(yè)務(wù)資源已經(jīng)在 Try 接口里扣除掉了,那么在第二階段的 Confirm 接口里可以什么都不用做。Cancel接口的執(zhí)行表示整個(gè)事務(wù)回滾,賬戶A回滾則需要把 Try 接口里扣除掉的 30 元還給賬戶。

  • 2、賬號(hào)B,在第一階段 Try 接口里實(shí)現(xiàn)給賬戶B加錢,Cancel 接口的執(zhí)行表示整個(gè)事務(wù)回滾,賬戶B回滾則需要把Try 接口里加的 30 元再減去。

方案1的問題分析:
  • 1、如果賬戶A的try沒有執(zhí)行在cancel則就多加了30元。
  • 2、由于try,cancel、confirm都是由單獨(dú)的線程去調(diào)用,且會(huì)出現(xiàn)重復(fù)調(diào)用,所以都需要實(shí)現(xiàn)冪等。
  • 3、賬號(hào)B在try中增加30元,當(dāng)try執(zhí)行完成后可能會(huì)其它線程給消費(fèi)了。
  • 4、如果賬戶B的try沒有執(zhí)行在cancel則就多減了30元。
問題解決:
  • 1、賬戶A的cancel方法需要判斷try方法是否執(zhí)行,正常執(zhí)行try后方可執(zhí)行cancel。
  • 2、try,cancel、confirm方法實(shí)現(xiàn)冪等。
  • 3、賬號(hào)B在try方法中不允許更新賬戶金額,在confirm中更新賬戶金額。
  • 4、賬戶B的cancel方法需要判斷try方法是否執(zhí)行,正常執(zhí)行try后方可執(zhí)行cancel。
優(yōu)化方案:

賬戶A

try {
    try冪等校驗(yàn)
    try懸掛處理
    檢查余額是否夠30元
    扣減30元
}
confirm {
    空
}
cancel {
    cancel冪等校驗(yàn)
    cancel空回滾處理
    增加可用余額30元
}

賬戶B

try {
    空
}
confirm {
    confirm冪等校驗(yàn)
    正式增加30元
}
cancel {
    空
}

Hmily實(shí)現(xiàn)TCC事務(wù)

本實(shí)例通過Hmily實(shí)現(xiàn)TCC分布式事務(wù),模擬兩個(gè)賬戶的轉(zhuǎn)賬交易過程。
兩個(gè)賬戶分別在不同的微服務(wù),要么一起成功,要么一起失敗,必須是一個(gè)整體性的事務(wù)。

微服務(wù)版本:
Nacos-Server:1.3.1
SpringBoot:2.2.10.RELEASE
spring-cloud-dependencies:Hoxton.SR8
spring-cloud-alibaba-dependencies:2.2.1.RELEASE
hmily-springcloud:2.0.6-RELEASE

引入maven

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.10.RELEASE</version>
    <relativePath/>
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.alibaba.nacos</groupId>
                <artifactId>nacos-client</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
        <version>1.3.1</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
        <groupId>org.dromara</groupId>
        <artifactId>hmily-springcloud</artifactId>
        <version>2.0.6-RELEASE</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.1</version>
    </dependency>

    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper-spring-boot-starter</artifactId>
        <version>2.1.5</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.18</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.1.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.6</version>
            <configuration>
                <configurationFile>
                    ${basedir}/src/main/resources/generator/generatorConfig.xml
                </configurationFile>
                <overwrite>true</overwrite>
                <verbose>true</verbose>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                    <version>5.1.41</version>
                </dependency>
                <dependency>
                    <groupId>tk.mybatis</groupId>
                    <artifactId>mapper</artifactId>
                    <version>4.1.5</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Hmily的application.properties配置

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

org.dromara.hmily.serializer=kryo
org.dromara.hmily.recoverDelayTime=128
org.dromara.hmily.retryMax=30
org.dromara.hmily.scheduledDelay=128
org.dromara.hmily.scheduledThreadMax=10
org.dromara.hmily.repositorySupport=db
# 服務(wù)調(diào)用方為true,其余為false
org.dromara.hmily.started=true
org.dromara.hmily.hmilyDbConfig.driverClassName=com.mysql.jdbc.Driver
org.dromara.hmily.hmilyDbConfig.url=jdbc:mysql://localhost:3306/hmily?useUnicode=true&useAffectedRows=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
org.dromara.hmily.hmilyDbConfig.username=root
org.dromara.hmily.hmilyDbConfig.password=yibo

新增配置類接收application.properties中的Hmily配置信息,創(chuàng)建HmilyTransactionBootstrap,并增加@EnableAspectJAutoProxy(proxyTargetClass=true)注解

@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;
    }

}

入口類增加org.dromara.hmily的掃描項(xiàng)

@ComponentScan({"com.yibo.hmily","org.dromara.hmily"})
@MapperScan("com.yibo.hmily.mapper")//掃描mybatis的指定包下的接口
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Hmily1Application {

    public static void main(String[] args) {

        SpringApplication.run(Hmily1Application.class,args);
    }
}

hmily-bank1實(shí)現(xiàn)try和cancel方法,如下:

try {
    try冪等校驗(yàn)
    try懸掛處理
    檢查余額是夠扣減金額
    扣減金額
}
confirm {
    空
}
cancel {
    cancel冪等校驗(yàn)
    cancel空回滾處理
    增加可用余額
}

Controller

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

    @Autowired
    private AccountService accountService;

    @GetMapping("/transfer/{amount}")
    public String transfer(@PathVariable("amount") Long amount){
        accountService.updateAccountBalance("1",amount);
        return "success";
    }
}

service服務(wù)的try和cancel方法

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountInfoMapper accountInfoMapper;

    @Autowired
    private Bank2Client bank2Client;

    /**
     * 賬戶扣款,就是tcc的try方法
     *  try冪等校驗(yàn)
     *  try懸掛處理
     *  檢查余額是夠扣減金額
     *  扣減金額
     * @param accountNo
     * @param amount
     */
    @Transactional
    //只要標(biāo)記@Hmily就是try方法,在注解中指定confirm、cancel兩個(gè)方法的名字
    @Hmily(confirmMethod="commit",cancelMethod="rollback")
    public void updateAccountBalance(String accountNo, Long amount) {
        //獲取全局事務(wù)id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("bank1 try begin 開始執(zhí)行...xid:{}",transId);

        //冪等判斷 判斷l(xiāng)ocal_try_log表中是否有try日志記錄,如果有則不再執(zhí)行
        if(accountInfoMapper.isExistTry(transId) > 0){
            log.info("bank1 try 已經(jīng)執(zhí)行,無需重復(fù)執(zhí)行,xid:{}",transId);
            return ;
        }

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

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

        //插入try執(zhí)行記錄,用于冪等判斷
        accountInfoMapper.addTry(transId);

        //遠(yuǎn)程調(diào)用轉(zhuǎn)賬
        if(!bank2Client.transfer(amount)){
            throw new RuntimeException("bank1 遠(yuǎn)程調(diào)用李四微服務(wù)失敗,xid:{}"+transId);
        }

        if(amount == 2){
            throw new RuntimeException("人為制造異常,xid:{}"+transId);
        }
        log.info("bank1 try end 結(jié)束執(zhí)行...xid:{}",transId);
    }

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

    /** cancel方法
     *  cancel冪等校驗(yàn)
     *  cancel空回滾處理
     *  增加可用余額
     * @param accountNo
     * @param amount
     */
    @Transactional
    public void rollback(String accountNo, Long amount){
        //獲取全局事務(wù)id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("bank1 cancel begin 開始執(zhí)行...xid:{}",transId);
        //  cancel冪等校驗(yàn)
        if(accountInfoMapper.isExistCancel(transId) > 0){
            log.info("bank1 cancel 已經(jīng)執(zhí)行,無需重復(fù)執(zhí)行,xid:{}",transId);
            return ;
        }

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

        //增加可用余額
        accountInfoMapper.addAccountBalance(accountNo,amount);
        //插入一條cancel的執(zhí)行記錄
        accountInfoMapper.addCancel(transId);
        log.info("bank1 cancel end 結(jié)束執(zhí)行...xid:{}",transId);
    }
}

feignClient

@FeignClient(value="hmily-bank2")
public interface Bank2Client {

    //遠(yuǎn)程調(diào)用微服務(wù)
    @GetMapping("/bank2/transfer/{amount}")
    @Hmily
    public boolean transfer(@PathVariable("amount") Long amount);
}

mapper

public interface AccountInfoMapper extends Mapper<AccountInfo> {

    int subtractAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Long amount);

    int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Long amount);

    /**
     * 增加某分支事務(wù)try執(zhí)行記錄
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int addTry(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務(wù)Confirm執(zhí)行記錄
     * @param localTradeNo
     * @return
     */
    int addConfirm(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務(wù)Cancel執(zhí)行記錄
     * @param localTradeNo
     * @return
     */
    int addCancel(@Param("txNo") String localTradeNo);

    /**
     * 查詢分支事務(wù)try是否已執(zhí)行
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int isExistTry(@Param("txNo") String localTradeNo);

    /**
     * 查詢分支事務(wù)confirm是否已執(zhí)行
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int isExistConfirm(@Param("txNo") String localTradeNo);

    /**
     * 查詢分支事務(wù)cancel是否已執(zhí)行
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int isExistCancel(@Param("txNo") String localTradeNo);
}

mapper.xml

  <update id="subtractAccountBalance">
    update account_info set account_balance=account_balance - #{amount}
    where account_balance>=#{amount} and account_no=#{accountNo}
  </update>

  <update id="addAccountBalance">
    update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo}
  </update>
  
  <insert id="addTry">
    insert into local_try_log values(#{txNo},now())
  </insert>

  <insert id="addConfirm">
    insert into local_confirm_log values(#{txNo},now())
  </insert>

  <insert id="addCancel">
    insert into local_cancel_log values(#{txNo},now())
  </insert>
  
  <select id="isExistTry" resultType="java.lang.Integer">
    select count(1) from local_try_log where tx_no = #{txNo}
  </select>

  <select id="isExistConfirm" resultType="java.lang.Integer">
    select count(1) from local_confirm_log where tx_no = #{txNo}
  </select>

  <select id="isExistCancel" resultType="java.lang.Integer">
    select count(1) from local_cancel_log where tx_no = #{txNo}
  </select>

hmily-bank2實(shí)現(xiàn)try、confirm、cancel功能,如下:

try {
    空
}
confirm {
    confirm冪等校驗(yàn)
    正式增加金額
}
cancel {
    空
}

Controller

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

    @Autowired
    private AccountInfoService accountInfoService;

    //張三轉(zhuǎn)賬
    @GetMapping("/transfer/{amount}")
    public boolean transfer(@PathVariable("amount") Long amount){
        accountInfoService.updateAccountBalance("2",amount);
        return true;
    }
}

Service實(shí)現(xiàn)confirm方法

@Service
@Slf4j
public class AccountInfoService {

    @Autowired
    private AccountInfoMapper accountInfoMapper;

    @Hmily(confirmMethod="confirmMethod", cancelMethod="cancelMethod")
    public void updateAccountBalance(String accountNo, Long amount) {
        //獲取全局事務(wù)id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("bank2 try begin 開始執(zhí)行...xid:{}",transId);
    }

    /**
     * confirm方法
     *  confirm冪等校驗(yàn)
     *  正式增加金額
     * @param accountNo
     * @param amount
     */
    @Transactional
    public void confirmMethod(String accountNo, Long amount){
        //獲取全局事務(wù)id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("bank2 confirm begin 開始執(zhí)行...xid:{}",transId);
        if(accountInfoMapper.isExistConfirm(transId)>0){
            log.info("bank2 confirm 已經(jīng)執(zhí)行,無需重復(fù)執(zhí)行...xid:{}",transId);
            return ;
        }
        //增加金額
        accountInfoMapper.addAccountBalance(accountNo,amount);

        //增加一條confirm日志,用于冪等
        accountInfoMapper.addConfirm(transId);
        log.info("bank2 confirm end 結(jié)束執(zhí)行...xid:{}",transId);
    }

    /**
     * @param accountNo
     * @param amount
     */
    public void cancelMethod(String accountNo, Long amount){
        //獲取全局事務(wù)id
        String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
        log.info("bank2 cancel begin 開始執(zhí)行...xid:{}",transId);
    }
}

Mapper

public interface AccountInfoMapper extends Mapper<AccountInfo> {

    int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Long amount);

    /**
     * 增加某分支事務(wù)try執(zhí)行記錄
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int addTry(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務(wù)Confirm執(zhí)行記錄
     * @param localTradeNo
     * @return
     */
    int addConfirm(@Param("txNo") String localTradeNo);

    /**
     * 增加某分支事務(wù)Cancel執(zhí)行記錄
     * @param localTradeNo
     * @return
     */
    int addCancel(@Param("txNo") String localTradeNo);

    /**
     * 查詢分支事務(wù)try是否已執(zhí)行
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int isExistTry(@Param("txNo") String localTradeNo);

    /**
     * 查詢分支事務(wù)confirm是否已執(zhí)行
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int isExistConfirm(@Param("txNo") String localTradeNo);

    /**
     * 查詢分支事務(wù)cancel是否已執(zhí)行
     * @param localTradeNo 本地事務(wù)編號(hào)
     * @return
     */
    int isExistCancel(@Param("txNo") String localTradeNo);
}

Mapper.xml

  <update id="addAccountBalance">
    update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo}
  </update>

  <insert id="addTry">
    insert into local_try_log values(#{txNo},now())
  </insert>

  <insert id="addConfirm">
    insert into local_confirm_log values(#{txNo},now())
  </insert>

  <insert id="addCancel">
    iinsert into local_cancel_log values(#{txNo},now())
  </insert>

  <select id="isExistTry" resultType="java.lang.Integer">
    select count(1) from local_try_log where tx_no = #{txNo}
  </select>

  <select id="isExistConfirm" resultType="java.lang.Integer">
    select count(1) from local_confirm_log where tx_no = #{txNo}
  </select>

  <select id="isExistCancel" resultType="java.lang.Integer">
    select count(1) from local_cancel_log where tx_no = #{txNo}
  </select>

TCC總結(jié):

如果拿TCC事務(wù)的處理流程與2PC兩階段提交做比較,2PC通常都是在跨庫的DB層面,而TCC則是在應(yīng)用層面的處理,需要通過業(yè)務(wù)邏輯來實(shí)現(xiàn)。

優(yōu)點(diǎn):

這種分布式事務(wù)的實(shí)現(xiàn)方式的優(yōu)勢在于,可以讓應(yīng)用自己定義數(shù)據(jù)操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。

缺點(diǎn):

而不足之處則在于對應(yīng)用的侵入性非常強(qiáng),業(yè)務(wù)邏輯的每個(gè)分支都需要實(shí)現(xiàn)try、confirm、cancel三個(gè)操作。此外,其實(shí)現(xiàn)難度也比較大,需要按照網(wǎng)絡(luò)狀態(tài)、系統(tǒng)故障等不同的失敗原因?qū)崿F(xiàn)不同的回滾策略。

github源碼地址:https://github.com/jjhyb/distributed-transaction

參考:
https://blog.csdn.net/hellozhxy/article/details/92843749

https://dromara.org/zh-cn/docs/hmily/index.html

https://www.cnblogs.com/rinack/p/9951970.html

https://www.pianshen.com/article/36421531670/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容