日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
阿里Seata新版本終于解決了TCC模式的冪等、懸掛和空回滾問題

今天來聊一聊阿里巴巴 Seata 新版本(1.5.0)是怎么解決 TCC 模式下的冪等、懸掛和空回滾問題的。

在平果等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)、成都外貿(mào)網(wǎng)站建設(shè) 網(wǎng)站設(shè)計(jì)制作按需開發(fā)網(wǎng)站,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),品牌網(wǎng)站建設(shè),網(wǎng)絡(luò)營銷推廣,外貿(mào)營銷網(wǎng)站建設(shè),平果網(wǎng)站建設(shè)費(fèi)用合理。

1.TCC 回顧

TCC 模式是最經(jīng)典的分布式事務(wù)解決方案,它將分布式事務(wù)分為兩個階段來執(zhí)行,try 階段對每個分支事務(wù)進(jìn)行預(yù)留資源,如果所有分支事務(wù)都預(yù)留資源成功,則進(jìn)入 commit 階段提交全局事務(wù),如果有一個節(jié)點(diǎn)預(yù)留資源失敗則進(jìn)入 cancel 階段回滾全局事務(wù)。

以傳統(tǒng)的訂單、庫存、賬戶服務(wù)為例,在 try 階段嘗試預(yù)留資源,插入訂單、扣減庫存、扣減金額,這三個服務(wù)都是要提交本地事務(wù)的,這里可以把資源轉(zhuǎn)入中間表。在 commit 階段,再把 try 階段預(yù)留的資源轉(zhuǎn)入最終表。而在 cancel 階段,把 try 階段預(yù)留的資源進(jìn)行釋放,比如把賬戶金額返回給客戶的賬戶。

注意:try 階段必須是要提交本地事務(wù)的,比如扣減訂單金額,必須把錢從客戶賬戶扣掉,如果不扣掉,在 commit 階段客戶賬戶錢不夠了,就會出問題。

1.1 try-commit

try 階段首先進(jìn)行預(yù)留資源,然后在 commit 階段扣除資源。如下圖:

1.2 try-cancel

try 階段首先進(jìn)行預(yù)留資源,預(yù)留資源時扣減庫存失敗導(dǎo)致全局事務(wù)回滾,在 cancel 階段釋放資源。如下圖:

2.TCC 優(yōu)勢

TCC 模式最大的優(yōu)勢是效率高。TCC 模式在 try 階段的鎖定資源并不是真正意義上的鎖定,而是真實(shí)提交了本地事務(wù),將資源預(yù)留到中間態(tài),并不需要阻塞等待,因此效率比其他模式要高。

同時 TCC 模式還可以進(jìn)行如下優(yōu)化:

2.1 異步提交

try 階段成功后,不立即進(jìn)入 confirm/cancel 階段,而是認(rèn)為全局事務(wù)已經(jīng)結(jié)束了,啟動定時任務(wù)來異步執(zhí)行 confirm/cancel,扣減或釋放資源,這樣會有很大的性能提升。

2.2 同庫模式

TCC 模式中有三個角色:

  • TM:管理全局事務(wù),包括開啟全局事務(wù),提交/回滾全局事務(wù);
  • RM:管理分支事務(wù);
  • TC: 管理全局事務(wù)和分支事務(wù)的狀態(tài)。

下圖來自 Seata 官網(wǎng):

TM 開啟全局事務(wù)時,RM 需要向 TC 發(fā)送注冊消息,TC 保存分支事務(wù)的狀態(tài)。TM 請求提交或回滾時,TC 需要向 RM 發(fā)送提交或回滾消息。這樣包含兩個個分支事務(wù)的分布式事務(wù)中,TC 和 RM 之間有四次 RPC。

優(yōu)化后的流程如下圖:

TC 保存全局事務(wù)的狀態(tài)。TM 開啟全局事務(wù)時,RM 不再需要向 TC 發(fā)送注冊消息,而是把分支事務(wù)狀態(tài)保存在了本地。TM 向 TC 發(fā)送提交或回滾消息后,RM 異步線程首先查出本地保存的未提交分支事務(wù),然后向 TC 發(fā)送消息獲?。ū镜胤种聞?wù)所在的)全局事務(wù)狀態(tài),以決定是提交還是回滾本地事務(wù)。

這樣優(yōu)化后,RPC 次數(shù)減少了 50%,性能大幅提升。

3.RM 代碼示例

以庫存服務(wù)為例,RM 庫存服務(wù)接口代碼如下:

@LocalTCC
public interface StorageService {

/**
* 扣減庫存
* @param xid 全局xid
* @param productId 產(chǎn)品id
* @param count 數(shù)量
* @return
*/
@TwoPhaseBusinessAction(name = "storageApi", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean decrease(String xid, Long productId, Integer count);

/**
* 提交事務(wù)
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);

/**
* 回滾事務(wù)
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}

通過 @LocalTCC 這個注解,RM 初始化的時候會向 TC 注冊一個分支事務(wù)。在 try 階段的方法(decrease方法)上有一個 @TwoPhaseBusinessAction 注解,這里定義了分支事務(wù)的 resourceId,commit 方法和 cancel 方法,useTCCFence 這個屬性下一節(jié)再講。

4.TCC 存在問題

TCC 模式中存在的三大問題是冪等、懸掛和空回滾。在 Seata1.5.0 版本中,增加了一張事務(wù)控制表,表名是 tcc_fence_log 來解決這個問題。而在上一節(jié) @TwoPhaseBusinessAction 注解中提到的屬性 useTCCFence 就是來指定是否開啟這個機(jī)制,這個屬性值默認(rèn)是 false。

tcc_fence_log 建表語句如下(MySQL 語法):

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

4.1 冪等

在 commit/cancel 階段,因?yàn)?TC 沒有收到分支事務(wù)的響應(yīng),需要進(jìn)行重試,這就要分支事務(wù)支持冪等。

我們看一下新版本是怎么解決的。下面的代碼在 TCCResourceManager 類:

@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
String applicationData) throws TransactionException {
TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
//省略判斷
Object targetTCCBean = tccResource.getTargetBean();
Method commitMethod = tccResource.getCommitMethod();
//省略判斷
try {
//BusinessActionContext
BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,
applicationData);
Object[] args = this.getTwoPhaseCommitArgs(tccResource, businessActionContext);
Object ret;
boolean result;
//注解 useTCCFence 屬性是否設(shè)置為 true
if (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) {
try {
result = TCCFenceHandler.commitFence(commitMethod, targetTCCBean, xid, branchId, args);
} catch (SkipCallbackWrapperException | UndeclaredThrowableException e) {
throw e.getCause();
}
} else {
//省略邏輯
}
LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId);
return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
} catch (Throwable t) {
//省略
return BranchStatus.PhaseTwo_CommitFailed_Retryable;
}
}

上面的代碼可以看到,執(zhí)行分支事務(wù)提交方法時,首先判斷 useTCCFence 屬性是否為 true,如果為 true,則走 TCCFenceHandler 類中的 commitFence 邏輯,否則走普通提交邏輯。

TCCFenceHandler 類中的 commitFence 方法調(diào)用了 TCCFenceHandler 類的 commitFence 方法,代碼如下:

public static boolean commitFence(Method commitMethod, Object targetTCCBean,
String xid, Long branchId, Object[] args) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
if (tccFenceDO == null) {
throw new TCCFenceException(String.format("TCC fence record not exists, commit fence method failed. xid= %s, branchId= %s", xid, branchId),
FrameworkErrorCode.RecordAlreadyExists);
}
if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
LOGGER.info("Branch transaction has already committed before. idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
return true;
}
if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
}
return false;
}
return updateStatusAndInvokeTargetMethod(conn, commitMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_COMMITTED, status, args);
} catch (Throwable t) {
status.setRollbackOnly();
throw new SkipCallbackWrapperException(t);
}
});
}

從代碼中可以看到,提交事務(wù)時首先會判斷 tcc_fence_log 表中是否已經(jīng)有記錄,如果有記錄,則判斷事務(wù)執(zhí)行狀態(tài)并返回。這樣如果判斷到事務(wù)的狀態(tài)已經(jīng)是 STATUS_COMMITTED,就不會再次提交,保證了冪等。如果 tcc_fence_log 表中沒有記錄,則插入一條記錄,供后面重試時判斷。

Rollback 的邏輯跟 commit 類似,邏輯在類 TCCFenceHandler 的 rollbackFence 方法。

4.2 空回滾

如下圖,賬戶服務(wù)是兩個節(jié)點(diǎn)的集群,在 try 階段賬戶服務(wù) 1 這個節(jié)點(diǎn)發(fā)生了故障,try 階段在不考慮重試的情況下,全局事務(wù)必須要走向結(jié)束狀態(tài),這樣就需要在賬戶服務(wù)上執(zhí)行一次 cancel 操作,這樣就空跑了一次回滾操作。

Seata 的解決方案是在 try 階段 往 tcc_fence_log  表插入一條記錄,status 字段值是 STATUS_TRIED,在 Rollback 階段判斷記錄是否存在,如果不存在,則不執(zhí)行回滾操作。代碼如下:

//TCCFenceHandler 類
public static Object prepareFence(String xid, Long branchId, String actionName, Callback targetCallback) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
LOGGER.info("TCC fence prepare result: {}. xid: {}, branchId: {}", result, xid, branchId);
if (result) {
return targetCallback.execute();
} else {
throw new TCCFenceException(String.format("Insert tcc fence record error, prepare fence failed. xid= %s, branchId= %s", xid, branchId),
FrameworkErrorCode.InsertRecordError);
}
} catch (TCCFenceException e) {
//省略
} catch (Throwable t) {
//省略
}
});
}

在 Rollback 階段的處理邏輯如下:

//TCCFenceHandler 類
public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
String xid, Long branchId, Object[] args, String actionName) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
// non_rollback
if (tccFenceDO == null) {
//不執(zhí)行回滾邏輯
return true;
} else {
if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {
LOGGER.info("Branch transaction had already rollbacked before, idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
return true;
}
if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());
}
return false;
}
}
return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
} catch (Throwable t) {
status.setRollbackOnly();
throw new SkipCallbackWrapperException(t);
}
});
}

updateStatusAndInvokeTargetMethod 方法執(zhí)行的 sql 如下:

update tcc_fence_log set status = ?, gmt_modified = ?
where xid = ? and branch_id = ? and status = ? ;

可見就是把 tcc_fence_log 表記錄的  status  字段值從 STATUS_TRIED 改為 STATUS_ROLLBACKED,如果更新成功,就執(zhí)行回滾邏輯。

4.3 懸掛

懸掛是指因?yàn)榫W(wǎng)絡(luò)問題,RM 開始沒有收到 try 指令,但是執(zhí)行了 Rollback 后 RM 又收到了 try 指令并且預(yù)留資源成功,這時全局事務(wù)已經(jīng)結(jié)束,最終導(dǎo)致預(yù)留的資源不能釋放。如下圖:

Seata 解決這個問題的方法是執(zhí)行 Rollback 方法時先判斷 tcc_fence_log 是否存在當(dāng)前 xid 的記錄,如果沒有則向 tcc_fence_log 表插入一條記錄,狀態(tài)是 STATUS_SUSPENDED,并且不再執(zhí)行回滾操作。代碼如下:

public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,
String xid, Long branchId, Object[] args, String actionName) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
// non_rollback
if (tccFenceDO == null) {
//插入防懸掛記錄
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);
//省略邏輯
return true;
} else {
//省略邏輯
}
return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);
} catch (Throwable t) {
//省略邏輯
}
});
}

而后面執(zhí)行 try 階段方法時首先會向 tcc_fence_log 表插入一條當(dāng)前 xid 的記錄,這樣就造成了主鍵沖突。代碼如下:

//TCCFenceHandler 類
public static Object prepareFence(String xid, Long branchId, String actionName, Callback targetCallback) {
return transactionTemplate.execute(status -> {
try {
Connection conn = DataSourceUtils.getConnection(dataSource);
boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);
//省略邏輯
} catch (TCCFenceException e) {
if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {
LOGGER.error("Branch transaction has already rollbacked before,prepare fence failed. xid= {},branchId = {}", xid, branchId);
addToLogCleanQueue(xid, branchId);
}
status.setRollbackOnly();
throw new SkipCallbackWrapperException(e);
} catch (Throwable t) {
//省略
}
});
}

注意:queryTCCFenceDO 方法 sql 中使用了 for update,這樣就不用擔(dān)心 Rollback 方法中獲取不到 tcc_fence_log 表記錄而無法判斷 try 階段本地事務(wù)的執(zhí)行結(jié)果了。

5.總結(jié)

TCC 模式是分布式事務(wù)使用最多的模式,但是冪等、懸掛和空回滾一直是 TCC 模式需要考慮的問題,Seata 框架在 1.5.0 版本完美解決了這些問題。

對 tcc_fence_log 表的操作也需要考慮事務(wù)的控制,Seata 使用了代理數(shù)據(jù)源,使 tcc_fence_log 表操作和 RM 業(yè)務(wù)操作在同一個本地事務(wù)中執(zhí)行,這樣就能保證本地操作和對 tcc_fence_log 的操作同時成功或失敗。


名稱欄目:阿里Seata新版本終于解決了TCC模式的冪等、懸掛和空回滾問題
鏈接URL:http://www.dlmjj.cn/article/djhhppc.html