一、什麼是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分為三個階段:
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
賬戶Btry: 檢查餘額是否夠30元 扣減30元 confirm: 空 cancel: 增加30元
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
賬戶Btry: try幂等校驗 try懸挂處理 檢查餘額是否夠30元 扣減30元 confirm: 空 cancel: cancel幂等校驗 cancel空復原處理 增加可用餘額30元
try: 空 confirm: confirm幂等校驗 正式增加30元 cancel: 空
三、Hmily實作TCC事務
3.1 業務說明
本執行個體通過Hmily實作TCC分布式事務,模拟兩個賬戶的轉賬交易過程。
兩個賬戶分别在不同的銀行(張三在bank1、李四在bank2),bank1、bank2是兩個微服務。
交易過程是,張三給李四轉賬指定金額。
上述交易步驟,要麼一起成功,要麼一起失敗,必須是一個整體性的事務。
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事務的處理流程與2PC兩階段送出做比較,2PC通常都是在跨庫的DB層面,而TCC則在應用層面的處理,需要通過業務邏輯來實作。
這種分布式事務的實作方式的優勢在于,可以讓應用自己定義資料操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。
而不足之處則在于對應用的侵入性非常強,業務邏輯的每個分支都需要實作try、confirm、cancel三個操作。
此外,其實作難度也比較大,需要按照網絡狀态、系統故障等不同的失敗原因實作不同的復原政策。
視訊教程、參考部落格、gitt源碼