TCC和兩階段分散式事務處理的區別

OkidoGreen發表於2020-04-05

https://blog.csdn.net/Paranoia_ZK/article/details/79481976

 

一個TCC事務框架需要解決的當然是分散式事務的管理。關於TCC事務機制的介紹,可以參考TCC事務機制簡介
TCC事務模型雖然說起來簡單,然而要基於TCC實現一個通用的分散式事務框架,卻比它看上去要複雜的多,不只是簡單的呼叫一下Confirm/Cancel業務就可以了的。

本文將以Spring容器為例,試圖分析一下,實現一個通用的TCC分散式事務框架需要注意的一些問題。

一、TCC全域性事務必須基於RM本地事務來實現全域性事務

TCC服務是由Try/Confirm/Cancel業務構成的,
其Try/Confirm/Cancel業務在執行時,會訪問資源管理器(Resource Manager,下文簡稱RM)來存取資料。這些存取操作,必須要參與RM本地事務,以使其更改的資料要麼都commit,要麼都rollback。

這一點不難理解,考慮一下如下場景:

image


假設圖中的服務B沒有基於RM本地事務(以RDBS為例,可通過設定auto-commit為true來模擬),那麼一旦[B:Try]操作中途執行失敗,TCC事務框架後續決定回滾全域性事務時,該[B:Cancel]則需要判斷[B:Try]中哪些操作已經寫到DB、哪些操作還沒有寫到DB:假設[B:Try]業務有5個寫庫操作,[B:Cancel]業務則需要逐個判斷這5個操作是否生效,並將生效的操作執行反向操作。
不幸的是,由於[B:Cancel]業務也有n(0<=n<=5)個反向的寫庫操作,此時一旦[B:Cancel]也中途出錯,則後續的[B:Cancel]執行任務更加繁重。因為,相比第一次[B:Cancel]操作,後續的[B:Cancel]操作還需要判斷先前的[B:Cancel]操作的n(0<=n<=5)個寫庫中哪幾個已經執行、哪幾個還沒有執行,這就涉及到了冪等性問題。而對冪等性的保障,又很可能還需要涉及額外的寫庫操作,該寫庫操作又會因為沒有RM本地事務的支援而存在類似問題。。。可想而知,如果不基於RM本地事務,TCC事務框架是無法有效的管理TCC全域性事務的。

 

反之,基於RM本地事務的TCC事務,這種情況則會很容易處理:[B:Try]操作中途執行失敗,TCC事務框架將其參與RM本地事務直接rollback即可。後續TCC事務框架決定回滾全域性事務時,在知道“[B:Try]操作涉及的RM本地事務已經rollback”的情況下,根本無需執行[B:Cancel]操作。

換句話說,基於RM本地事務實現TCC事務框架時,一個TCC型服務的cancel業務要麼執行,要麼不執行,不需要考慮部分執行的情況。

二、TCC事務框架應該接管Spring容器的TransactionManager

基於RM本地事務的TCC事務框架,可以將各Try/Confirm/Cancel業務看著一個原子服務:一個RM本地事務提交,參與該RM本地事務的所有Try/Confirm/Cancel業務操作都生效;反之,則都不生效。掌握每個RM本地事務的狀態以及它們與Try/Confirm/Cancel業務方法之間的對應關係,以此為基礎,TCC事務框架才能有效的構建TCC全域性事務。

TCC服務的Try/Confirm/Cancel業務方法在RM上的資料存取操作,其RM本地事務是由Spring容器的PlatformTransactionManager來commit/rollback的,TCC事務框架想要了解RM本地事務的狀態,只能通過接管Spring的事務管理器功能。

2.1. 為什麼TCC事務框架需要掌握RM本地事務的狀態?
首先,根據TCC機制的定義,TCC事務是通過執行Cancel業務來達到回滾效果的。仔細分析一下,這裡暗含一個事實:
只有生效的Try業務操作才需要執行對應的Cancel業務操作。換句話說,只有Try業務操作所參與的RM本地事務被commit了,後續TCC全域性事務回滾時才需要執行其對應的Cancel業務操作;否則,如果Try業務操作所參與的RM本地事務被rollback了,後續TCC全域性事務回滾時就不能執行其Cancel業務,此時若盲目執行Cancel業務反而會導致資料不一致。

其次,Confirm/Cancel業務操作必須保證生效。Confirm/Cancel業務操作也會涉及RM資料存取操作,其參與的RM本地事務也必須被commit。TCC事務框架需要在確切的知道所有Confirm/Cancel業務操作參與的RM本地事務都被成功commit後,才能將標記該TCC全域性事務為完成。如果TCC事務框架誤判了Confirm/Cancel業務參與RM本地事務的狀態,就會造成全域性事務不一致。

最後,未完成的TCC全域性,TCC事務框架必須重新嘗試提交/回滾操作。重試時會再次呼叫各TCC服務的Confirm/Cancel業務操作。如果某個服務的Confirm/Cancel業務之前已經生效(其參與的RM本地事務已經提交),重試時就不應該再次被呼叫。否則,其Confirm/Cancel業務被多次呼叫,就會有“服務冪等性”的問題。

2.2. 攔截TCC服務的Try/Confirm/Cancel業務方法的執行,根據其異常資訊可否知道其RM本地事務是否commit/rollback了呢?
基本上很難做到。為什麼這麼說呢?
第一,事務是可以在多個(本地/遠端)服務之間互相傳播其事務上下文的,一個業務方法(Try/Confirm/Cancel)執行完畢並不一定會觸發當前事務的commit/rollback操作。比如,被傳播事務上下文的業務方法,在它開始執行時,容器並不會為其建立新的事務,而是它的呼叫方參與的事務,使得二者操作在同一個事務中;同樣,在它執行完畢時,容器也不會提交/回滾它參與的事務的。因此,這類業務方法上的異常情況並不能反映他們是否生效。不接管Spring的TransactionManager,就無法瞭解事務於何時被建立,也無法瞭解它於何時被提交/回滾。
第二、一個業務方法可能會包含多個RM本地事務的情況。比如: A(REQUIRED)->B(REQUIRES_NEW)->C(REQUIRED),這種情況下,A服務所參與的RM本地事務被提交時,B服務和C服務參與的RM本地事務則可能會被回滾。
第三、並不是丟擲了異常的業務方法,其參與的事務就回滾了。Spring容器的宣告式事務定義了兩類異常,其事務完成方向都不一樣:系統異常(一般為Unchecked異常,預設事務完成方向是rollback)、應用異常(一般為Checked異常,預設事務完成方向是commit)。二者的事務完成方向又可以通過@Transactional配置顯式的指定,如rollbackFor/noRollbackFor等。
第四、Spring容器還支援使用setRollbackOnly的方式顯式的控制事務完成方向;
最後、自行攔截業務方法的攔截器和Spring的事務處理的攔截器還會存在執行先後、攔截範圍不同等問題。例如,如果自行攔截器執行在前,就會出現業務方法雖然已經執行完畢但此時其參與的RM本地事務還沒有commit/rollback。

TCC事務框架的定位應該是一個TransactionManager,其職責是負責commit/rollback事務。而一個事務應該commit、還是rollback,則應該是由Spring容器來決定的:Spring決定提交事務時,會呼叫TransactionManager來完成commit操作;Spring決定回滾事務時,會呼叫TransactionManager來完成rollback操作。

接管Spring容器的TransactionManager,TCC事務框架可以明確的得到Spring的事務性指令,並管理Spring容器中各服務的RM本地事務。否則,如果通過自行攔截的機制,則使得業務系統存在TCC事務處理、RM本地事務處理兩套事務處理邏輯,二者互不通訊,各行其是。這種情況下要協調TCC全域性事務,基本上可以說是緣木求魚,本地事務尚且無法管理,更何談管理分散式事務?

三、TCC事務框架應該具備故障恢復機制

一個TCC事務框架,若是沒有故障恢復的保障,是不成其為分散式事務框架的。

分散式事務管理框架的職責,不是做出全域性事務提交/回滾的指令,而是管理全域性事務提交/回滾的過程。它需要能夠協調多個RM資源、多個節點的分支事務,保證它們按全域性事務的完成方向各自完成自己的分支事務。這一點,是不容易做到的。因為,實際應用中,會有各種故障出現,很多都會造成事務的中斷,從而使得統一提交/回滾全域性事務的目標不能達到,甚至出現”一部分分支事務已經提交,而另一部分分支事務則已回滾”的情況。比較常見的故障,比如:業務系統伺服器當機、重啟;資料庫伺服器當機、重啟;網路故障;斷電等。這些故障可能單獨發生,也可能會同時發生。作為分散式事務框架,應該具備相應的故障恢復機制,無視這些故障的影響是不負責任的做法。

一個完整的分散式事務框架,應該保障即使在最嚴苛的條件下也能保證全域性事務的一致性,而不是隻能在最理想的環境下才能提供這種保障。退一步說,如果能有所謂“理想的環境”,那也無需使用分散式事務了。

TCC事務框架要支援故障恢復,就必須記錄相應的事務日誌。事務日誌是故障恢復的基礎和前提,它記錄了事務的各項資料。TCC事務框架做故障恢復時,可以根據事務日誌的資料將中斷的事務恢復至正確的狀態,並在此基礎上繼續執行先前未完成的提交/回滾操作。

四、TCC事務框架應該提供Confirm/Cancel服務的冪等性保障

一般認為,服務的冪等性,是指標對同一個服務的多次(n>1)請求和對它的單次(n=1)請求,二者具有相同的副作用。

在TCC事務模型中,Confirm/Cancel業務可能會被重複呼叫,其原因很多。比如,全域性事務在提交/回滾時會呼叫各TCC服務的Confirm/Cancel業務邏輯。執行這些Confirm/Cancel業務時,可能會出現如網路中斷的故障而使得全域性事務不能完成。因此,故障恢復機制後續仍然會重新提交/回滾這些未完成的全域性事務,這樣就會再次呼叫參與該全域性事務的各TCC服務的Confirm/Cancel業務邏輯。

既然Confirm/Cancel業務可能會被多次呼叫,就需要保障其冪等性。
那麼,應該由TCC事務框架來提供冪等性保障?還是應該由業務系統自行來保障冪等性呢?
個人認為,應該是由TCC事務框架來提供冪等性保障。如果僅僅只是極個別服務存在這個問題的話,那麼由業務系統來負責也是可以的;然而,這是一類公共問題,毫無疑問,所有TCC服務的Confirm/Cancel業務存在冪等性問題。TCC服務的公共問題應該由TCC事務框架來解決;而且,考慮一下由業務系統來負責冪等性需要考慮的問題,就會發現,這無疑增大了業務系統的複雜度。

五、TCC事務框架不能盲目的依賴Cancel業務來回滾事務

前文以及提到過,TCC事務通過Cancel業務來對Try業務進行回撤的機制暗含了一個事實:Try操作已經生效。也就是說,只有Try操作所參與的RM本地事務已經提交的情況下,才需要執行其Cancel操作進行回撤。沒有執行、或者執行了但是其RM本地事務被rollback的Try業務,是一定不能執行其Cancel業務進行回撤的。因此,TCC事務框架在全域性事務回滾時,應該根據TCC服務的Try業務的執行情況選擇合適的處理機制。而不能盲目的執行Cancel業務,否則就會導致資料不一致。

一個TCC服務的Try操作是否生效,這是TCC事務框架應該知道的,因為其Try業務所參與的RM事務也是由TCC事務框架所commit/rollbac的(前提是TCC事務框架接管了Spring的事務管理器)。所以,TCC事務回滾時,TCC事務框架可考慮如下處理策略:
1)如果TCC事務框架發現某個服務的Try操作的本地事務尚未提交,應該直接將其回滾,而後就不必再執行該服務的cancel業務;
2)如果TCC事務框架發現某個服務的Try操作的本地事務已經回滾,則不必再執行該服務的cancel業務;
3)如果TCC事務框架發現某個服務的Try操作尚未被執行過,那麼,也不必再執行該服務的cancel業務。

總之,TCC事務框架應該保障:
1)已生效的Try操作應該被其Cancel操作所回撤;
2)尚未生效的Try操作,則不應該執行其Cancel操作。這一點,不是冪等性所能解決的問題。如上文所述,冪等性是指服務被執行一次和被執行n(n>0)次所產生的影響相同。但是,未被執行和被執行過,二者效果肯定是不一樣的,這不屬於冪等性的範疇。

六、Cancel業務與Try業務並行,甚至先於Try操作完成

這應該算TCC事務機制特有的一個不可思議的陷阱。一般來說,一個特定的TCC服務,其Try操作的執行,是應該在其Confirm/Cancel操作之前的。Try操作執行完畢之後,Spring容器再根據Try操作的執行情況,指示TCC事務框架提交/回滾全域性事務。然後,TCC事務框架再去逐個呼叫各TCC服務的Confirm/Cancel操作。

然而,超時、網路故障、伺服器的重啟等故障的存在,使得這個順序會被打亂。比如:

 

image


上圖中,假設[B:Try]操作執行過程中,網路閃斷,[A:Try]會收到一個RPC遠端呼叫異常。A不處理該異常,導致全域性事務決定回滾,TCC事務框架就會去呼叫[B:Cancel],而此刻A、B之間網路剛好已經恢復。如果[B:Try]操作耗時較長(網路阻塞/資料庫操作阻塞),就會出現[B:Try]和[B:Cancel]二者並行處理的現象,甚至[B:Cancel]先完成的現象。

 

這種情況下,由於[B:Cancel]執行時,[B:Try]尚未生效(其RM本地事務尚未提交),因此,[B:Cancel]是不能執行的,至少是不能生效(執行了其RM本地事務也要rollback)的。然而,當
[B:Cancel]處理完畢(跳過執行、或者執行後rollback其RM本地事務)後,[B:Try]操作完成又生效了(其RM本地事務成功提交),這就會使得[B:Cancel]雖然提供了,但卻沒有起到回撤[B:Try]的作用,導致資料的不一致。

所以,TCC框架在這種情況下,需要:
1)將[B:Try]的本地事務標註為rollbackOnly,阻止其後續生效;
2)禁止其再次將事務上下文傳遞給其他遠端分支,否則該問題將在其他分支上出現;
3)相應地,[B:Cancel]也不必執行,至少不能生效。

當然,TCC事務框架也可以簡單的選擇阻塞[B:Cancel]的處理,待[B:Try]執行完畢後,再根據它的執行情況判斷是否需要執行[B:Cancel]。不過,這種處理方式因為需要等待,所以,處理效率上會有所不及。

同樣的情況也會出現在confirm業務上,只不過,發生在Confirm業務上的處理邏輯與發生在Cancel業務上的處理邏輯會不一樣,TCC框架必須保證:
1)Confirm業務在Try業務之後執行,若發現並行,則只能阻塞相應的Confirm業務操作;
2)在進入Confirm執行階段之後,也不可以再提交同一全域性事務內的新的Try操作的RM本地事務。

七、TCC服務複用性是不是相對較差?

TCC事務機制的定義,決定了一個服務需要提供三個業務實現:Try業務、Confirm業務、Cancel業務。可能會有人因此認為TCC服務的複用性較差。怎麼說呢,要是將 Try/Confirm/Cancel業務邏輯單獨拿出來複用,其複用性當然是不好的,Try/Confirm/Cancel 邏輯作為TCC型服務中的一部分,是不能單獨作為一個元件來複用的。Try、Confirm、Cancel業務共同才構成一個元件,如果要複用,應該是複用整個TCC服務元件,而不是單獨的Try/Confirm/Cancel業務。

八、TCC服務是否需要對外暴露三個服務介面?

不需要。TCC服務與普通的服務一樣,只需要暴露一個介面,也就是它的Try業務。Confirm/Cancel業務邏輯,只是因為全域性事務提交/回滾的需要才提供的,因此Confirm/Cancel業務只需要被TCC事務框架發現即可,不需要被呼叫它的其他業務服務所感知。

換句話說,業務系統的其他服務在需要呼叫TCC服務時,根本不需要知道它是否為TCC型服務。因為,TCC服務能被其他業務服務呼叫的也僅僅是其Try業務,Confirm/Cancel業務是不能被其他業務服務直接呼叫的。

九、TCC服務A的Confirm/Cancel業務中能否呼叫它依賴的TCC服務B的Confirm/Cancel業務?

最好是不要這樣做。首先,沒有必要。TCC服務A依賴TCC服務B,那麼[A:Try]已經將事務上下文傳播給[B:Try]了,後續由TCC事務框架來呼叫各自的Confirm/Cancel業務即可;其次,Confirm/Cancel業務如果被允許呼叫其他服務,那麼它就有可能再次發起新的TCC全域性事務。如此遞迴下去,將會導致全域性事務關係混亂且不可控。

TCC全域性事務,應該儘量在Try操作階段傳播事務上下文。Confirm/Cancel操作階段僅需要完成各自Try業務操作的確認操作/補償操作即可,不適合再做遠端呼叫,更不能再對外傳播事務上下文。

綜上所述,本文傾向於認為,實現一個通用的TCC分散式事務管理框架,還是相對比較複雜的。一般業務系統如果需要使用TCC事務機制,並不推薦自行設計實現。
這裡,給大家推薦一款開源的TCC分散式事務管理器ByteTCC。ByteTCC基於Try/Confirm/Cancel機制實現,可與Spring容器無縫整合,相容Spring的宣告式事務管理。提供對dubbo框架、Spring Cloud的開箱即用的支援,可滿足多資料來源、跨應用、跨伺服器等各種分散式事務場景的需求。

 

TCC是兩階段提交的一種麼?

經常在網路上看見有人介紹TCC時,都提一句,”TCC是兩階段提交的一種”。其理由是TCC將業務邏輯分成try、confirm/cancel在兩個不同的階段中執行。其實這個說法,是不正確的。可能是因為既不太瞭解兩階段提交機制、也不太瞭解TCC機制的緣故,於是將兩階段提交機制的prepare、commit兩個事務提交階段和TCC機制的try、confirm/cancel兩個業務執行階段互相混淆,才有了這種說法。

兩階段提交(Two Phase Commit,下文簡稱2PC),簡單的說,是將事務的提交操作分成了prepare、commit兩個階段。其事務處理方式為:
1、 在全域性事務決定提交時,a)逐個向RM傳送prepare請求;b)若所有RM都返回OK,則逐個傳送commit請求最終提交事務;否則,逐個傳送rollback請求來回滾事務;
2、 在全域性事務決定回滾時,直接逐個傳送rollback請求即可,不必分階段。
* 需要注意的是:2PC機制需要RM提供底層支援(一般是相容XA),而TCC機制則不需要。

TCC(Try-Confirm-Cancel),則是將業務邏輯分成try、confirm/cancel兩個階段執行,具體介紹見TCC事務機制簡介。其事務處理方式為:
1、 在全域性事務決定提交時,呼叫與try業務邏輯相對應的confirm業務邏輯;
2、 在全域性事務決定回滾時,呼叫與try業務邏輯相對應的cancel業務邏輯。
可見,TCC在事務處理方式上,是很簡單的:要麼呼叫confirm業務邏輯,要麼呼叫cancel邏輯。這裡為什麼沒有提到try業務邏輯呢?因為try邏輯與全域性事務處理無關。

當討論2PC時,我們只專注於事務處理階段,因而只討論prepare和commit,所以,可能很多人都忘了,使用2PC事務管理機制時也是有業務邏輯階段的。正是因為業務邏輯的執行,發起了全域性事務,這才有其後的事務處理階段。實際上,使用2PC機制時————以提交為例————一個完整的事務生命週期是:begin -> 業務邏輯 -> prepare -> commit。

再看TCC,也不外乎如此。我們要發起全域性事務,同樣也必須通過執行一段業務邏輯來實現。該業務邏輯一來通過執行觸發TCC全域性事務的建立;二來也需要執行部分資料寫操作;此外,還要通過執行來向TCC全域性事務註冊自己,以便後續TCC全域性事務commit/rollback時回撥其相應的confirm/cancel業務邏輯。所以,使用TCC機制時————以提交為例————一個完整的事務生命週期是:begin -> 業務邏輯(try業務) -> commit(comfirm業務)。

綜上,我們可以從執行的階段上將二者一一對應起來:
1、 2PC機制的業務階段 等價於 TCC機制的try業務階段;
2、 2PC機制的提交階段(prepare & commit) 等價於 TCC機制的提交階段(confirm);
3、 2PC機制的回滾階段(rollback) 等價於 TCC機制的回滾階段(cancel)。

因此,可以看出,雖然TCC機制中有兩個階段都存在業務邏輯的執行,但其中try業務階段其實是與全域性事務處理無關的。認清了這一點,當我們再比較TCC和2PC時,就會很容易地發現,TCC不是兩階段提交,而只是它對事務的提交/回滾是通過執行一段confirm/cancel業務邏輯來實現,僅此而已。

 

TCC事務機制簡介

關於TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland於2007年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。在該論文中,TCC還是以Tentative-Confirmation-Cancellation作為名稱;正式以Try-Confirm-Cancel作為名稱的,可能是Atomikos(Gregor Hohpe所著書籍《Enterprise Integration Patterns》中收錄了關於TCC的介紹,提到了Atomikos的Try-Confirm-Cancel,並認為二者是相似的概念)。

國內最早關於TCC的報導,應該是InfoQ上對阿里程立博士的一篇採訪。經過程博士的這一次傳道之後,TCC在國內逐漸被大家廣為了解並接受。相應的實現方案和開源框架也先後被髮布出來,ByteTCC就是其中之一。

TCC事務機制相對於傳統事務機制(X/Open XA Two-Phase-Commit),其特徵在於它不依賴資源管理器(RM)對XA的支援,而是通過對(由業務系統提供的)業務邏輯的排程來實現分散式事務。

對於業務系統中一個特定的業務邏輯S,其對外提供服務時,必須接受一些不確定性,即對業務邏輯執行的一次呼叫僅是一個臨時性操作,呼叫它的消費方服務M保留了後續的取消權。如果M認為全域性事務應該rollback,它會要求取消之前的臨時性操作,這將對應S的一個取消操作;而當M認為全域性事務應該commit時,它會放棄之前臨時性操作的取消權,這對應S的一個確認操作。

每一個初步操作,最終都會被確認或取消。因此,針對一個具體的業務服務,TCC事務機制需要業務系統提供三段業務邏輯:初步操作Try、確認操作Confirm、取消操作Cancel。

1. 初步操作(Try)
TCC事務機制中的業務邏輯(Try),從執行階段來看,與傳統事務機制中業務邏輯相同。但從業務角度來看,卻不一樣。TCC機制中的Try僅是一個初步操作,它和後續的確認一起才能真正構成一個完整的業務邏輯。可以認為


 

1


 

[傳統事務機制]的業務邏輯 = [TCC事務機制]的初步操作(Try) + [TCC事務機制]的確認邏輯(Confirm)。

 

TCC機制將傳統事務機制中的業務邏輯一分為二,拆分後保留的部分即為初步操作(Try);而分離出的部分即為確認操作(Confirm),被延遲到事務提交階段執行。
TCC事務機制以初步操作(Try)為中心的,確認操作(Confirm)和取消操作(Cancel)都是圍繞初步操作(Try)而展開。因此,Try階段中的操作,其保障性是最好的,即使失敗,仍然有取消操作(Cancel)可以將其不良影響進行回撤。

2. 確認操作(Confirm)
確認操作(Confirm)是對初步操作(Try)的一個補充。當TCC事務管理器決定commit全域性事務時,就會逐個執行初步操作(Try)指定的確認操作(Confirm),將初步操作(Try)未完成的事項最終完成。

3. 取消操作(Cancel)
取消操作(Cancel)是對初步操作(Try)的一個回撤。當TCC事務管理器決定rollback全域性事務時,就會逐個執行初步操作(Try)指定的取消操作(Cancel),將初步操作(Try)已完成的事項全部撤回。

在傳統事務機制中,業務邏輯的執行和事務的處理,是在不同的階段由不同的部件來完成的:業務邏輯部分訪問資源實現資料儲存,其處理是由業務系統負責;事務處理部分通過協調資源管理器以實現事務管理,其處理由事務管理器來負責。二者沒有太多互動的地方,所以,傳統事務管理器的事務處理邏輯,僅需要著眼於事務完成(commit/rollback)階段,而不必關注業務執行階段。

而在TCC事務機制中的業務邏輯處理和事務處理,其關係就錯綜複雜:業務邏輯(Try/Confirm/Cancel)階段涉及所參與資源事務的commit/rollback;全域性事務commit/rollback時又涉及到業務邏輯(Try/Confirm/Cancel)的執行。其中關係,本站將另行撰文詳細介紹,敬請關注!

參考文獻:

  1. http://www.infoq.com/cn/interviews/soa-chengli
  2. https://cs.brown.edu/courses/cs227/archives/2012/papers/weaker/cidr07p15.pdf
  3. http://www.enterpriseintegrationpatterns.com/patterns/conversation/TryConfirmCancel.html
  4. http://blog.51cto.com/robertleepeak/2083454?wx=

相關文章