用AOP與Threadlocal實現超簡單TCC事務框架

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章