1 LAF-DTX要解決的問題
分散式事務產生的根本原因在於一段業務邏輯中涉及到多個資料的一致性問題,這裡的多個資料,可能是多個資料庫表,這也是大家通常所理解的分散式事務的範疇。但是還有一種“多個資料”的情況也普遍存在,那就是資料庫資料與非資料庫資料混合存在的情況,在該情況中對資料庫的操作被稱為“主流程”,對非資料庫的操作被稱為“邊緣流程”,比如發jmq訊息、發簡訊、發郵件、寫快取、寫ES等等。LAF-DTX所要解決的就是所謂的“主流程與邊緣流程”的分散式事務問題。
例如在“使用者報名課程”這個場景中,主流程:使用者報名課程的資料庫操作,邊緣流程:傳送郵件和簡訊通知。在電商場景中,也存在類似情況,比如完成庫存扣減的資料庫操作後,發訊息給其他應用。這裡的“主流程和邊緣流程”的場景必須符合事務特性,在上述例子中,如果使用者報名的資料庫操作失敗,但是已經給使用者發“已報名”的簡訊和郵件,則會給使用者帶來誤解,嚴重的話可能會發生:使用者去上課了,發現自己還沒有報名,使使用者白跑一趟;如果使用者報名的資料庫操作成功,但是沒有收到簡訊或郵件,則使用者有可能錯失去上課的機會,也可能給使用者造成一定的經濟損失(課程費用)。
2 LAF-DTX的設計目標
眾所周知,對於涉及多個資料庫表的分散式事務,業界已經有XA兩階段提交、TCC(Try-Complete-Cancel)、Saga等解決方案,XA由於提供強一致性而引入成本很高的全域性鎖機制,造成其併發效能差,不適用於網際網路行業這種強調“高併發”的場景。因此,TCC和Saga採用了“柔性事務”的策略:單資料庫操作靠資料庫事務保證強一致,多資料庫間是最終一致。但是TCC和Saga都需要業務重度參與,提供反向或補償的業務程式碼,因此使用成本很高,但是它們提供的效能是滿足業務需要的。
結合上述解決方案的優劣以及我們要解決的“主流程和邊緣流程”這種分散式事務的自身特點,LAF-DTX提出瞭如下的設計目標:
柔性事務
保證資料最終一致性,確保效能足夠好;
業務無侵入,簡單好用
只需加一個註解,業務零成本使用,簡單易懂;
輕量架構
依賴資源少,部署簡單;支援單獨使用;
3 LAF-DTX的實現架構
LAF-DTX的架構設計如圖1所示,非常簡單,除JED外無任何其他依賴:
LAF-DTX SDK
完成分散式事務的核心控制邏輯,包括註解及AOP處理、事務恢復、Task補償排程等;
LAF-DTX Server端
完成事務的協調管理,包括事務狀態管理等;
JED
採用JED作為儲存,存放LAF-DTX的後設資料;
4 LAF-DTX的技術實現
4.1 LAF-DTX的事務模型
邊緣流程都是非資料庫的操作(其本身不提供事務特性),對於該類操作的標準做法是:a)獲取必需的資訊;b)利用各種前置條件邏輯對這些資訊進行校驗;c)拼裝好邊緣流程操作所需的引數併發出請求。當在a或b中發生錯誤時,可以透過丟擲RuntimeException或其子類的異常,引發本地資料庫操作的事務回滾。而造成c失敗的根本原因無非是網路異常、底層系統異常等等,而這些錯誤基本上都可以透過“多次重試”來解決。因此,LAF-DTX的事務模型只需要保證以下兩點:1)當上述的a或b出現錯誤時,有能力將包含本地資料庫操作在內的所有操作一起進行回滾;2)當上述的c出現錯誤時,可以透過“多次重試”的機制加以解決。
基於以上思路,在LAF-DTX的事務模型中,將各個邊緣流程抽象為一個個Task物件,在Task物件中封裝了執行該邊緣流程所需的要素(類、方法、引數等),並持久化在Task表中,以便“多次重試”。LAF-DTX分散式事務模型包含兩部分功能:A)保證本地業務資料庫和Task資料庫的資料一致性(注意這兩個庫位於物理上的不同地方,無法在一個JDBC事務中自動完成),在此我們借鑑了TCC事務模型的思路來保證這兩個庫的資料一致性,但是無需使用者提供TCC事務模式所需的反向/補償操作,而由LAF-DTX自身提供。另外透過在我們的註解中合併@Transactional註解(Spring的事務機制)完成了前面的1)點所要求的功能。B)透過Task任務補償機制來非同步地執行Task,從而真正地完成邊緣流程的操作。
該方案已被整理成一項專利,並且已經透過了公司的稽核。TCC相關的流程參加見圖3、4和5:
4.2 邊緣流程的處理
如前所述,邊緣流程將被抽象為Task物件而被非同步執行。在LAF-DTX中,使用者可以透過一個配置檔案告知哪些類的什麼方法是邊緣流程。在使用者不改變程式碼的條件下,如何在實際執行過程中,將邊緣流程的真正執行過程忽略掉,轉而“偷換”成建立Task物件的邏輯呢?答案就是Java位元組碼增強技術。透過使用Javaassist技術,對使用者指定的方法進行增強,需要達到如下效果:1)當該方法參與了分散式事務時,將真正執行過程忽略掉,換成建立Task物件的邏輯;2)當該方法沒有參與分散式事務時,執行原來的真正邏輯。
在LAF-DTX中,該增強邏輯被封裝成一個jar包,業務需要在啟動時透過javaagent引數來載入該jar包,才能取得預期效果。
4.3 任務的補償排程
Task表除了記錄執行一個邊緣流程所需的要素外,還要記錄產生這個Task的應用資訊以及一個雜湊值(由應用資訊和例項ip計算產生)。LAF-DTX SDK中有一個後臺執行緒持續向LAF-DTX Server端請求需要在自己所在例項上執行的Task列表,然後依次執行。
在LAF-DTX的任務排程實現中沒有所謂的“中心控制節點”,即由一箇中心化的控制模組來決定任務該分配給誰,而完全是由LAF-DTX SDK根據特定演算法“自主”得出要在自己所在的例項上執行的Task列表,然後構造出對應的查詢條件從LAD-DTX Server端請求得到這些Task。秘訣就在於Task表中的雜湊值,同屬於一個應用的所有例項都對應一個雜湊值,這些雜湊值共同構成了一個“一致性Hash環”。預設情況下,每個例項都應該去LAF-DTX Server端請求雜湊值為自己的雜湊值(根據應用資訊和例項ip自己算出)且應用資訊相等的Task列表;當某個例項出現問題時(比如當機或者因故障被永久摘除了),在Hash環上跟它相鄰的(按順時針計算)且狀態正常的例項將替它去執行對應的任務,這樣就避免了任務被漏掉且保證任務以較均勻的方式被分配。
為了預防極端情況下,同一個Task被多個例項獲取而造成“多次執行同一Task”的問題,在Task表中還增加了版本欄位,利用“樂觀鎖”的機制控制同一個Task只能被一個例項所執行。
判斷例項的狀態是根據心跳機制來進行的,因此LAF-DTX SDK中還會定期向LAF-DTX Server傳送心跳資訊。
該任務排程方案也已被整理成一項專利,並且已經透過了公司的稽核。
5 例子及注意事項
5.1 例子
圖6:使用例子
在這個例子中,@SimplePSTransaction是LAF-DTX提供的註解(PS就是primary/secondary的簡寫),所有加上這個註解的方法都會被按照LAF-DTX分散式事務模型進行處理。在這個例子裡,sp1Service.sendCreateOrderMsg和sp2Service.cache4CreateOrder是邊緣流程(需要配置在4.2中所提的配置檔案中)。其實,根據前面的內容可以推論出:除了被指定為邊緣流程的方法外,剩下的都會被看做主流程。
5.2 注意事項
LAF-DTX之所以能夠成為輕量化的中介軟體,除了在設計上強調簡單實用外,還有一個原因就是它充分利用了現有的一些框架和機制,從而使得自己顯得很輕量。因此,在使用LAF-DTX前,需要使用者確認LAF-DTX依賴的框架和機制是否已經具備(所幸這些框架和機制都是很常見的,不屬於很強的條件)。這些框架和機制如下:
Spring容器環境
LAF-DTX抽象了任意邊緣流程執行所涉及的內容,並進行持久化,以方便後續補償任務排程。邊緣流程執行所需的上下文環境需要由Spring容器來提供和保證。
資料庫事務相關的bean
@SimplePSTransaction註解合併了Spring事務註解@Transactional,因此暗含支援Spring資料庫事務的所有特性,這也是實現4.1節中的1)的關鍵所在。業務需要配置好相關的bean,例如對應的TransactionManager等。
邊緣流程需要“冪等性”
由於可能需要多次補償邊緣流程,因此其需要滿足“冪等性”。
主流程和邊緣流程必須執行在一個執行緒中
如果邊緣流程透過其他執行緒執行(比如執行緒池等),那麼LAF-DTX的事務邏輯將會受到干擾,最終影響事務的一致性。其實LAF-DTX已經是非同步執行邊緣流程了,所以就不需要業務這樣做了。
選擇最單純、無依賴的方法(比如jmq的sendMq方法)進行增強
如果選擇的方法粒度過大(即包含了太多的邏輯,比如4.1節所提到的a和b邏輯),由於該方法會被抽象為Task物件而被延後非同步執行,那麼a或者b中的錯誤本來應該引發整個事務回滾,但實際上並不會發生。
要對抽象類的方法進行增強
測試中發現對抽象類的方法進行增強會引發錯誤,一定要對實體類的方法進行增強。
AspectJ框架
@SimplePSTransaction註解的AOP邏輯採用了AspectJ來完成,因此業務程式碼需要遵循AspectJ的規範,尤其需要指出的是AspectJ不支援“自呼叫”的情況。
長事務問題
LAF-DTX的事務範圍就是加上@SimplePSTransaction註解的方法所覆蓋的範圍,在這個範圍裡包含了主流程和若干個邊緣流程。需要強調的是要控制這個範圍的大小,否則事務包含的內容太多,不管是提交還是回滾都會比較耗時,影響了業務的響應。在一些批次操作中,尤其要注意這個問題。