TCC是處理分佈式事務的一種技術,每個服務提供者提供TRY/CONFIRM/CANCEL三個接口,分別對應資源鎖定,提交,取消操作。看到github上有些複雜完善的TCC框架,本着簡單用AOP與ThreadLocal來做一個簡單的框架,驗證下自己的想法是否可行,同時練練手。
其中的TCC三調用的方法切換,以及考慮後續要使用try返回值處理,本人採用了一種投機取巧的方式來實現。
一、主要目標
只考慮幾個簡單的目標
1. TCC調用的關係信息
每個TCC服務調用一般都要包裝一下,而且要先定義好誰是TRY,誰是CONFIRM/CANCEL。比如在spring cloud的hystrix中,有註解中設定降級方法,另外寫好降級方法,在掃描時,相關信息都記錄下來備用。比如try失敗了要找到執行哪個cancel。
2. 具體調用的參數
在運行中,具體的調用與參數,都需要在事務過程中存下來,可以通過在調用過程中增加AOP,得到相關信息。不僅當時的調用參數,包括try的結果要記錄下來。
3. 事務中相關的調用
因爲要麼所有的try都成功,再全部執行confirm操作,如果有一個失敗,所有已經成功的都調用cancel操作。所以已經執行的都要記錄下來
4. 完整性保障
如果confirm/cancel操作失敗了,可能記錄,可能重試,要考慮。
二、設計方案
一般是對Try方法進行註解,而且註解中指明成功與失敗執行什麼。可是這些註解解析,保存,工作量不少,這個先不考慮。這裏要刪繁就簡的處理。
在執行方法時,肯定有公共的信息要存,所以spring AOP來切入保存數據。對每個參與者方法與聚合方法都要處理,所以使用兩個註解進行AOP即可。一個總控,一個在具體執行中。
由於在AOP的@Around過程中,可以得到當前調用的ProceedingJoinPoint對象,它包含了當前調用的所有信息的封裝,可以很好的利用一下。
比如ProceedingJoinPoint可以得到當前調用的參數,而參數是可以修改後再執行的,那麼切換TCC方法就可以利用這個特點。於是對一個參與者的TCC調用包裝成一個單一方法,並用一個TccType的參數進行區分,switch內部一分爲三,對應TCC。爲了方便修改參數,這個參數排第一位置。
另外,Try方法可能返回參與者自己系統的ID,用於根據ID進行確認或者取消,這個參數正好也利用一下ProceedingJoinPoint。爲了方便設置這個參數,就定在單一方法的第二個參數位置,Try調用時,這個位置置null即可。
至於每個調用都的調用保存,就把它們的ProceedingJoinPoint放在一個list中,這個list保存在ThreadLocal中就行了,一開始產生一個空的list,成功了就放進去,如果都成功,就取出來修改參數運行,失敗了也是修改參數再運行。
另外考慮本地事務如果與多個參與者在一起怎麼辦?本地就用@Transactional吧,不管哪個步驟有問題,都拋出異常,讓本地事務也回滾。
最後提到完整性,如果Confirm/Cancel失敗了怎麼辦?同樣爲了簡化,提供一個接口,把ProceedingJoinPoint傳出來,由使用都來實現,是先記錄呢,還是再重試呢,還是同時要分發進行不同的處理都不管了,外部實現。
三、主要代碼
核心方法都已經註解
//兩個AOP切點
@Pointcut("@annotation(com.so_mini.tcc.annotation.Tcc)")
public void tccAspect() {
System.out.println("Tcc");//註解聚合方法
}
@Pointcut("@annotation(com.so_mini.tcc.annotation.TccEach)")
public void tccEachAspect() {
System.out.println("TccEach");//註解每一個TCC參與者方法
}
//對應的環繞AOP處理
@Around("tccAspect()")
public Object aroundMethod(ProceedingJoinPoint pjd) {
Object result = null;
String methodName = pjd.getSignature().getName();
Map<String,Object> tccInfo=new HashMap<String,Object>();
tccInfo.put("list", new ArrayList<ProceedingJoinPoint>());
threadLocal.set(tccInfo);//當前線程局部變量中保存
try {
result = pjd.proceed();
Map<String,Object> tccInfoRe=(Map<String, Object>) threadLocal.get();
List<ProceedingJoinPoint> participant=(List<ProceedingJoinPoint>)(tccInfoRe.get("list"));
System.out.println("【正常,需確認數】"+participant.size());
//沒有異常,執行所有try對應的confirm廣場
for(ProceedingJoinPoint pj:participant){
toConfirm(pj, _CCFailListenerList);
}
// participant.forEach(joinPoint-->(org.aspectj.lang.JoinPoint)joinPoint);
return result;
} catch (Throwable e) {
Map<String,Object> tccInfoRe=(Map<String, Object>) threadLocal.get();
List<ProceedingJoinPoint> participant=(List<ProceedingJoinPoint>)(tccInfoRe.get("list"));
System.out.println("【有問題,取消已完成數】"+participant.size());
//有異常,已經成功的try方法,都要執行對應的cancel方法
for(ProceedingJoinPoint pj:participant){
toCancle(pj,_CCFailListenerList);
}
throw new RuntimeException("TCC異常");//拋異常,本地DB事務回滾。
}
finally{
threadLocal.remove();
System.out.println("【threadLocal removed!】");
}
}
@Around("tccEachAspect()")
public Object aroundEachMethod(ProceedingJoinPoint pjd) {
Object result = null;
String methodName = pjd.getSignature().getName();
Map tccInfo=(Map) threadLocal.get();
List participant=(List)(tccInfo.get("list"));
try {
participant.add(pjd);//預存下來
result = pjd.proceed();
System.out.println("["+methodName+"] return value:"+result);
setTryResult(pjd,result);//【設置try執行的結果】
return result;
} catch (Throwable e) {
participant.remove(pjd);//從成功的列表中移除,拋異常讓整體cancel
throw new RuntimeException(e);
}
finally{
}
}
//正常try,第二個參數記錄結果
public static void setTryResult(ProceedingJoinPoint point,Object result) {
int i=0;
Object[] args = point.getArgs();
args[1]=result;
}
//需要確認時,第一個參數修改爲confirm
public static void toConfirm(ProceedingJoinPoint point,List<CCFailListener> cCFailListenerList) {
Object[] args = point.getArgs();
args[0]=(Object)(TccPhaseEnum.Confirm);
try {
point.proceed(args);
} catch (Throwable e) {
cCFailListenerList.forEach(listener->listener.notify(point, TccPhaseEnum.Confirm));//出錯由監聽器處理,用戶自己實現
}
}
四、結果
聚合方法及其中一個參與者Service
@Transactional
@Tcc
public void createTripTrans(String orderId){
System.out.println("BEGIN db-trans...");
staffService.addStaff(orderId);//簡單的本地數據庫操作
System.out.println("BEGIN remote...");
eatBook.bookEat(TccPhaseEnum.Try,null,orderId);//前兩個參數固定!!!
houseBook.bookHouse(TccPhaseEnum.Try,null,orderId);
planeBook.bookPlane(TccPhaseEnum.Try,null,orderId);
}
@Service
public class HouseBook {
@TccEach
public String bookHouse(TccPhaseEnum type,Object tryResult,String orderId){
switch (type) {
case Try:
System.out.println("house book...!");
if("222".equals(orderId))
throw new RuntimeException("house failure");
return "HS100083";//模擬try的返回值
case Confirm:
confirmHouse(tryResult);//必須使用前面的返回值,否則異常
return null;
case Cancel:
System.out.println("house cancel...!"+tryResult);
return null;
}
return null;
}
private void confirmHouse(Object houseId){
if("HS100083".equals(houseId))
System.out.println("house confirm...!"+houseId);
else{
throw new RuntimeException("houseId missing and confirme failure");
}
}
}
正常與回滾的運行結果
//第三個TCC出錯,前兩個執行CANCEL,本地DB事務回滾
BEGIN db-trans...
Hibernate: select staffbo0_.id .....
BEGIN remote...
Eat book...!//第一個參與者 try
[bookEat] return value:null//try 的 返回值
house book...!//第二個參與者 try
[bookHouse] return value:HS100083//try 的 返回值
【有問題,取消已完成數】2//第三個失敗了,取消前兩個成功的try
Eat cnacel...!//第一個參與者
house cancel...!HS100083//第二個參與者 cancel使用了try的結果
【threadLocal removed!】//all finished
java.lang.RuntimeException: TCC異常
//正常情況
BEGIN db-trans...
Hibernate: select staffbo0_.id ...
BEGIN remote...
Eat book...!
[bookEat] return value:null
house book...!
[bookHouse] return value:HS100083
plane booked!
[bookPlane] return value:null
【正常,需確認數】3
Eat confirm...!
house confirm...!HS100083
plane confirmed!
【threadLocal removed!】
Hibernate: insert into staff (age, name, id) values (?, ?, ?)
五、源碼
https://github.com/herriman76/tcc-mini