一、前言
嚴格遵守ACID的分佈式事務我們稱爲剛性事務,而遵循BASE理論(基本可用:在故障出現時保證核心功能可用,軟狀態:允許中間狀態出現,最終一致性:不要求分佈式事務打成中時間點數據都是一致性的,但是保證達到某個時間點後,數據就處於了一致性了)的事務我們稱爲柔性事務,其中TCC編程模式就屬於柔性事務,本文我們來闡述其理論。
二、TCC編程模式
TCC編程模式本質上也是一種二階段協議,不同在於TCC編程模式需要與具體業務耦合,下面首先看下TCC編程模式步驟:
- 所有事務參與方都需要實現try,confirm,cancle接口。
- 事務發起方向事務協調器發起事務請求,事務協調器調用所有事務參與者的try方法完成資源的預留,這時候並沒有真正執行業務,而是爲後面具體要執行的業務預留資源,這裏完成了一階段。(狀態機加入)
-
如果事務協調器發現有參與者的try方法預留資源時候發現資源不夠,則調用參與方的cancle方法回滾預留的資源,需要注意cancle方法需要實現業務冪等,因爲有可能調用失敗(比如網絡原因參與者接受到了請求,但是由於網絡原因事務協調器沒有接受到回執)會重試。(補償機制)
-
如果事務協調器發現所有參與者的try方法返回都OK,則事務協調器調用所有參與者的confirm方法,不做資源檢查,直接進行具體的業務操作。
- 如果協調器發現所有參與者的confirm方法都OK了,則分佈式事務結束。
- 如果協調器發現有些參與者的confirm方法失敗了,或者由於網絡原因沒有收到回執,則協調器會進行重試。這裏如果重試一定次數後還是失敗,會怎麼樣那?常見的是做事務補償。
螞蟻金服基於TCC實現了XTS(雲上叫DTS),目前在螞蟻金服雲上有對外輸出,這裏我們來結合其提供的一個例子來具體理解TCC的含義,以下引入螞蟻金服雲實例:
“首先我們假想這樣一種場景:轉賬服務,從銀行 A 某個賬戶轉 100 元錢到銀行 B 的某個賬戶,銀行 A 和銀行 B 可以認爲是兩個單獨的系統,也就是兩套單獨的數據庫。
我們將賬戶系統簡化成只有賬戶和餘額 2 個字段,並且爲了適應 DTS 的兩階段設計要求,業務上又增加了一個凍結金額(凍結金額是指在一筆轉賬期間,在一階段的時候使用該字段臨時存儲轉賬金額,該轉賬額度不能被使用,只有等這筆分佈式事務全部提交成功時,纔會真正的計入可用餘額)。按這樣的設計,用戶的可用餘額等於賬戶餘額減去凍結金額。這點是理解參與者設計的關鍵,也是 DTS 保證最終一致的業務約束。”
在try階段並沒有對銀行A和B數據庫中的餘額字段做操作,而是對凍結金額做的操作,對應A銀行預留資源操作是對凍結金額加上100元,這時候A銀行賬號上可用錢爲餘額字段-凍結金額;對應B銀行的操作是對凍結金額上減去100,這時候B銀行賬號上可用的錢爲餘額字段-凍結金額。
如果事務協調器調用銀行A和銀行B的try方法有一個失敗了(比如銀行A的賬戶餘額不夠了),則調用cancle進行回滾操作(具體是對凍結金額做反向操作)。如果調用try方法都OK了,則進入confirm階段,confirm階段則不做資源檢查,直接做業務操作,對應銀行A要在賬戶餘額減去100,然後凍金額減去100;對應銀行B要對賬戶餘額字段加上100,然後凍結金額加上100。
最關心的,如果confirm階段如果有一個參與者失敗了,該如何處理,其實上面操作都是xts-client做的,還有一個xts-server專門做事務補償的。
三、總結
TCC是對二階段的一個改進,try階段通過預留資源的方式避免了同步阻塞資源的情況,但是TCC編程需要業務自己實現try,confirm,cancle方法,對業務入侵太大,實現起來也比較複雜。
四,實戰代碼
最後我寫了一個TCC變種分佈式事務模板=> TCCJ可供參考: 我這裏,我這裏的judge模塊是對try模塊產生結果的審覈, 審覈不通過執行回滾操作.
/**
* Description: 分佈式事務執行模板
* <p>
* 1. 先執行各個服務塊業務
* 2. 執行結束通過confirm判定執行結果, 如果失敗則進行取消(回滾操作)
* 3. 如果執行服務模塊發生異常,則判定後進行
* </p>
* User: zhouzhou
* Date: 2018-08-27
* Time: 10:27
*/
public class TccTemplate {
private static final Logger logger = LoggerFactory.getLogger(TccCallBack.class);
/**
* 分佈式事務模板
* @param tccCallBack 分佈式事務執行回調
* @param method 當前方法名(封裝參數, 可方便撈取數據)
*/
public static <T> TccResult process(TccCallBack tccCallBack, String method, T t) {
// 返回一個消息用於
TccResult tccResult = new TccResult();
String msg = "";
try {
// 執行主業務
tccCallBack.tryExecute();
// 進行確認執行結果,如果結果是false,則執行回滾操作
boolean judge = tccCallBack.judge();
if (judge) {
tccResult.setStatus(true);
msg = String.format("分佈式事務{%s}執行成功", method);
logger.info(msg);
// 執行確認操作
tccCallBack.confirm();
} else {
tccResult.setStatus(false);
msg = String.format("分佈式事務{%s}執行失敗,進行回滾操作", method);
logger.warn(msg);
tccCallBack.cancel();
}
} catch (Exception e) {
// 主流程發生異常, 則直接執行回滾操作
tccResult.setStatus(false);
msg = String.format("分佈式事務{%s}執行發生異常{%s},進行回滾操作", method,e.getMessage());
logger.warn(String.format("分佈式事務{%s}執行發生異常,進行回滾操作", method), e);
tccCallBack.cancel();
}finally {
// 返回結果Result
tccResult.setMsg(msg);
return tccResult;
}
}
}
當然你需要編寫回調接口:
/**
* Description: 基於TCC變成模式的分佈式事務回調
* User: zhouzhou
* Date: 2018-08-27
* Time: 10:20
*/
public interface TccCallBack {
/**
* 執行主要分佈式業務操作
*/
void tryExecute();
/**
* 確認分佈式業務操作最終結果,
* 如果返回true,則不執行cancel,返回false則執行cancel
*/
boolean judge();
/**
* 取消操作
*/
void cancel();
/**
* 確認操作
*/
void confirm();
}