【MySQL如何實現分布式事務】
Innodb存儲引擎支持XA事務,通過XA事務可以支持分布式事務的實現。分布式事務指的是允許多個獨立的事務資源(transac tional resources)參與一個全局的事務中。事務資源通常是關系型數據庫系統,也可以是其它類型的資源。
全局事務要求在其中所有參與的事務要么全部提交,要么全部回滾,這對於事務原有的ACID要求又有了提高。另外,在使用分布式事務時候,InnoDB存儲引擎的事務隔離級別必須設置成serialiable。
XA事務允許不同數據庫之間的分布式事務,如:一台服務器是mysql數據庫,一台是Oracle的,又有可能還有一台是sqlserver的,只要參與全局事務中的每個節點都支持XA事務。
分布式事務可能在銀行系統的轉帳中比較常見,如一個用戶需要從上海轉1000元到北京的一個用戶賬號上面:
# bank ofshanghai:Update user_account set money=money – 10000 where user=’xiaozhang’;
# bank ofBeijing:Update user_account set money= money + 10000 where user=’xiaoli’;
分布式事務使用兩段式提交(two-phase commit)的方式。
第一個階段,所有參與全局事務的節點都開始准備,告訴事務管理器它們准備好提交了。
第二個階段,事務管理器告訴資源管理器執行rollback或者commit,如果任何一個節點顯示不能commit,那么所有的節點就得全部rollback。
當前的java的JTA(java transaction API)可以很好的支持mysql的分布式事務,可以仔細參考JTA手冊。
下面的例子顯示了如何使用jta支持調用mysql分布式事務。
分布式事務處理( Distributed Transaction Processing , DTP )涉及多個分布在不同地方的數據庫,但對數據庫的操作必須全部被提交或者回滾。只要任一數據庫操作時失敗,所有參與事務的數據庫都需要回滾。
Open 組織定義的分布式事務處理模型X/Open DTP 模型(1994)包括應用程序( AP )、事務管理器( TM )、資源管理器( RM ,即數據庫 )、通信資源管理器( CRM )四部分。而 XA 是 X/Open DTP 定義的事務管理器與數據庫之間的接口規范(即接口函數),事務管理器用它來通知數據庫事務的開始、結束以及提交、回滾等。
XA 接口規范 使用兩階段提交協議來完成一個全局事務,保證同一事務中所有數據庫同時成功或者回滾。
兩階段提交協議假設每個數據庫點都存在一個 write-ahead log,每一次的write請求都是 先記log后才真正執行寫入。
第一階段為提交請求階段(Commit-request phase):
1. 事務管理器給所有數據庫發query to commit消息請求,然后開始等待回應;
2. 數據庫如果可以提交屬於自己的事務分支,則將自己在該事務分支中所做的操作固定記錄下來(在undo log和redo log中各記一項);
3. 數據庫都回應是否同意提交的應答。
第二階段為提交階段(Commit phase):
如果事務管理器收到的所有回應都是agreement,
1. 事務管理器記日志並給所有數據庫發commit消息請求;
2. 各個數據庫執行操作,釋放所有該事務相關的鎖和資源;
3. 各個數據庫給事務管理器回復;
4.當收到所有回復,事務管理器結束當前事務
如果事務管理器收到的任一回應是abort,
1. 事務管理器記日志並給所有數據庫發rollback消息請求;
2. 各個數據庫執行undo操作,釋放所有該事務相關的鎖和資源;
3. 各個數據庫給事務管理器回復;
4.當收到所有回復,事務管理器結束當前事務
兩階段提交協議的問題在於數據庫在提交請求階段應答后對很多資源處於鎖定狀態,要等到事務管理器收集齊所有數據庫的應答后,才能發commit或者rollback消息結束這種鎖定。鎖定時間的長度是由最慢的一個數據庫制約,如果數據庫一直沒有應答,所有其他庫也需要無休止的鎖並等待。並且,如果事務管理器出現故障,被鎖定的資源將長時間處於鎖定狀態。無論是任一數據庫或者事務管理器故障,其他數據庫都需要永久鎖定或者至少長時間鎖定。並且,分布式系統中節點越多,存在緩慢網絡或者故障節點的概率也就越大,資源被長時間鎖定的概率指數上升。
兩階段提交協議的另一個問題是只要有任意一個數據庫不可用都會導致事務失敗,這導致事務更傾向於失敗。對於多個副本的備份系統,很多時候我們希望部分副本點失效時系統仍然可用,使用該協議則不能實現。並且,分布式系統中節點越多,存在故障節點的概率也就越大,系統的可用性指數下降。
另外,如果數據庫在第一階段應答后到第二階段正式提交前的某個階段網絡故障或者節點故障,該協議無法提交或回滾,數據不一致不能絕對避免。
兩階段提交協議。
事務是一個很重要的概念,它必須滿足ACID特性,在單機的數據庫中,這很容易實現。但在分布式數據庫中,各個表分散在各台不同的機器上,如何對這些表實施分布式的事務處理就成為一個比較困難的問題,其中兩段式提交就是解決分布式事務的一種方式。
兩段式提交設計本身的思路非常的容易理解,步驟如下:
1. 協調員服務器(協調員)發送一條投票請求消息給所有參與這次事務的服務器(參與者)。
2. 當一個參與者收到一條投票請求,它會向協調員發送一條響應請求消息,該響應消息包含了參與者的投票:YES 或者NO。如果參與者的消息的投票是NO,那就意味着由於某些原因,參與者不能參與這次事務,等價於收到了ABORT決定,本次事務的工作到此為止。
3. 協調員收集所有參與者的響應投票,如果所有的響應投票都是YES,那么協調員就會做出決定:COMMIT,並且會把COMMIT消息發送給所有參與者。否則,協調員則會做出決定:ABORT,此時協調員會把ABORT消息發給那些投票為YES的那些參與者(投票為NO的參與者已經單方面ABORT了這次事務,協調員不必再發送消息給這些參與者)。發送完決定后,協調員對於本次事務的工作就此停止了。
4. 投了YES票的參與者等待着來自協調員的決定(COMMIT或者ABORT),然后根據決定做完相應的操作,然后本次事務的工作也就此為止。
步驟1,2屬於兩段式提交的階段1,步驟3,4屬於兩段式提交的階段2。在整個過程中,參與者會存在一段不確定時間段(從它發送YES的票開始,到它收到COMMIT/ABORT的決定結束),在此時間段內,參與者的進程會被block住,它需要等待接下來的決定。而協調員則不存在任何不確定時間段,它可以繼續處理其它的事務請求,發送其它事務的投票請求,在做完COMMIT/ABORT決定之后,它可以馬上去干別的事情,無需任何等待。因為協調員的工作不具有原子性,它可以交叉地做任何事。而參與者完成的是事務,具有原子性,它做出承諾后,他必須保持好事務的現場,避免別的事務的交叉感染,從而違反了ACID中的Isolated。
從描述來看非常簡單,很容易理解,但是請注意,在整個過程中的任何時間點,都有可能發生的各種各樣的故障,有的是鏈路故障,有的是服務器故障。如果詳細考慮這些情況,實現就不是這么簡單了。
先考慮第一個問題,在整個執行的過程中,無論是參與者的進程,還是協調者的進程,他們在做下一步的處理前都必須等待消息。但是,消息可能會失敗,並不總是能夠到達。為了避免無休止的等待消息,因此需要加入Timeout 。當消息超過一定的時間還沒到來的時候,我們必須做出處理,這些處理我們稱之為Timeout-Action。當服務器或者服務器的進程(無論是協調員還是參與者)從一次失敗中恢復過來的時候,我們希望服務器的進程能夠嘗試着獲得一個和其他進程一致的決定。這很好理解,
COMMIT/ABORT的決定已經由協調員發出了,那么恢復的參與者進程也希望能夠得到這個決定從而參與完成該事務。當然,在參與者從失敗中恢復過來的時候,由於其它的一些可能的失敗,可能COMMIT/ABORT的決定還未能做出,此時該參與者也需要做出相應的正確處理。因此,服務器的進程必須保存一些信息,比如是一些Log。有了這些Log,才能使得從失敗中恢復的進程能夠正確恢復事務處理。
Timeout-Action
進程需要在3個地方等待消息:在(2),(3),(4)步開始的地方:
在(2)步驟中,參與者進程需要等來來自協調員進程的投票請求。此時如果在等待投票請求時發生了timeout,參與者服務器就可以簡單得停止該事務的工作就可以了。
在(3)步驟中,協調員需要等待接受所有參與者回應的YES或NO的投票,在此時,協調員還未達成任何決定,參與者也沒有提交任何數據,因此協調員在Timeout發生后,只需要發送ABORT決定給所有的參與者就可以了。
在(4)步驟中,參與者p已經投了YES票,正在等待來自協調員的COMMIT或ABORT命令。在這個時間節點上,p處在不確定時間段。因此此時,p不能在timeout的時候簡單得單方面作出決定,他需要向其他服務器做咨詢才能知道該如何處理。最簡單的終止設計可以是這樣的:p依然被block住,一直詢問等待協調員,直到p重新建立起和協調員之間的聯系。接着,協調員就會告訴p已經作出的決定(協調員沒有不確定時間期),然后p就可以接着處理決定。
簡單終止協議的缺點是參與者p會被不必要得block住一段時間。比如,假如有2個參與者p和q,協調員把COMMIT/ABORT決定成功發送給q了,但是在它給p發送的決定失敗了。的確,p這時是處在不確定時期,但是q已經不在不確定期了,如果p能夠和q通信的話,p可以從q那里得到協調員發出的決定,不必一直block等到協調員恢復。
這需要參與者能夠互相知道對方,參與者之間可以直接交換信息,不必總是通過協調員的中介。要實現這種自由的信息交換也並不是十分困難,協調員在發送投票請求的時候可以把所有參與者的ID列表附在投票請求消息后面發送給所有的參與者,這樣參與者p在收到投票請求后就可以直接和其他所有的參與者進行交流了。這么做也不會帶來什么副作用,在收到投票請求之前,參與者之間還是互相不認識,因此在此之前(2),(3)發生的timeout還是可以單方面得中止任務或者停止事務。這個思路就出現另外的一個設計-協同終止設計,設計如下:
當一個參與者p在其不確定時間段內發生了timeout,他會依次向所有其他的進程發送一個詢問請求消息,詢問做出的決定是什么或者是否能單方面得做出一個決定(因為如果有一個被詢問的參與者已經向協調員回復了一個NO的投票,那么詢問者自然就可以單方面得做出決定ABORT這次事務,因為只要有一個參與者回復了NO,那么協調員做出的決定肯定是ABORT,無需再向協調員確認了)。在這種場景下,參與者p就被稱之為發起人,作出詢問回答的服務器進程 q就可以稱之為回應人。那么回應人q可能有3種情況:
1. q已經收到了COMMIT/ABORT決定:q只需要把該決定回應給p,然后p就可以自行處理了。
2. q還沒進行投票:q此時可以單方面做出決定,因為此時協調員已經發生故障,此時q可以回應ABORT給p,p就可以自己做出處理。
3. q已經回復YES投票給協調員,處在不確定期內,也沒有收到來自協調員的決定。此時q也無法給p任何幫助。
根據這個設計,如果p發送詢問請求給q,碰巧q處在情況(1)或者(2)時,p馬上就可以達成(也就是獲得)一個決定而無需任何block。如果p能通訊的其他所有的進程都處在情況(3),那么p也會被block住,直到足夠的故障被修復使得p至少能夠和一個處在情況(1)或(2)的參與者進程q通訊。需要注意的是詢問請求可以發給所有的其他服務器進程,包括協調員進程,這樣至少可以確認協調員在沒有故障的狀態下可以回復投票請求,避免了碰巧所有其他的參與者進程都在不確定期而無法提供幫助回應這樣的窘境。
總之,協同終止設計可以降低block的概率,但不能完全排除它。
恢復
一個服務器進程p剛剛從一次故障中恢復,我們希望p能夠獲得一個和其它進程們已經達成的決定一致的決定,如果不能馬上恢復這個決定,那么至少在其它的故障被修復后能夠恢復這個決定。
當一個服務器進程p把系統恢復到了故障發生時現場保存的狀態,我們來進一步考慮一下。如果p是在它發送YES投票到協調員之前就發生故障了,那么該進程就可以單方面的決定取消這次事務,發送NO投票給協調員,不做任何處理。同樣,如果p是在已經收到COMMIT/ABORT決定之后或者自己已經作出ABORT的決定之后發生故障了,那么此時p由於已經做出了決定,p就可以作出相應的處理,比如說取消事務操作,或者繼續把COMMIT決定的操作執行完畢。在這些情況下,p都能夠獨立得進行故障恢復。
但是,如果p發生故障時是處在它的不確定期時,那么它就無法在恢復時獨立得做決定了,這就是問題的復雜之處。因為它投了YES,在p故障時,可能其他的參與者全部投了YES並且協調者做出了COMMIT的決定。又或者p發生故障時,其他參與者並未全部投票YES,因此協調者作出的是ABORT的決定。此時p無法根據本地信息就能獨立得進行恢復,他需要和其他進程進行交流。在這種情況下,p所面臨的情況是和time-action的情況(3)是一樣的。(設想一下,p設置了一個非常長的timeout 時間,整個故障期間都沒有超過timeout的期限)。因此此時p也采用前面提到的終止設計來解決問題。
為了保存故障發生時的狀態,每個進程都必須維護一個DT Log(Database Transaction Log)。每個進程只能訪問他自己服務器上的DT Log。假設我們采用的是協同終止設計,我們來看看如果管理這些DT log.
1. 當協調員發送投票請求之前或之后,它寫了一條開始兩階段記錄在DT log中。該記錄大概類似這樣:

- {
- Type: start-2PC,
- time: 2011-10-30 19:20:20,
- Participants:
- [
- {
- Hostname:participant-1,
- Ip:192.168.0.3
- },
- {
- Hostname:participant-2,
- Ip:192.168.0.4
- },
- {
- Hostname:participant-3,
- Ip:192.168.0.5
- }
- ]
- }
2. 如果參與者線程發送了YES投票,那么他必須在發送投票之前寫這么YES 投票記錄在DT Log中,大概類似這樣:

- {
- Type: VOTE,
- Value:YES,
- time: 2011-10-30 19:20:20,
- Coordinator: 192.168.0.2
- OtherParticipants:
- [
- {
- Hostname:participant-2,
- Ip:192.168.0.4
- },
- {
- Hostname:participant-3,
- Ip:192.168.0.5
- }
- ]
- }
如果參與者發送了NO投票,那么它可以在發送投票之前或之后寫一條ABORT ACCEPT記錄在DT log中。
3. 在協調員發送COMMIT決定給所有參與者進程之前,他寫入一條COMMIT DECISION記錄。
4. 當協調員發送ABORT決定給所有參與者進程之前或之后,它寫入一條ABORT DECISION記錄
5. 參與者服務器進程在收到COMMIT/ABORT決定之后,參與者進程寫入一條COMMIT ACCEPT/ABORT ACCPET記錄。
對上述Log做一些說明,一旦參與者服務器進程在DT日志中寫入COMMIT ACCEPT或者ABORT ACCEPT記錄后,DM(database manager)就可以執行commit或者abort數據庫操作。具體來講還有很多細節,比如系統中的DT Log可能是DM Log中的一部分,因此DT Log中的COMMIT ACCEPT/ABORT ACCEPT記錄是通過本地DM的Commit/Abort子程序來實現的,在子程序中進行具體的操作之前,DM會寫入COMMIT ACCEPT/ABORT ACCEPT記錄到日志中去。
有了這個日志系統,當服務器S就可以按照下面的方式進行恢復:
1> 如果S檢查DT Log發現了記錄,那么S就知道自己是一台協調員。如果發現日志還包含了COMMIT DECISION或者ABORT DECISION日志,那就證明在故障發生之前已經產生了決定,他可以選擇重新發送這些決定。如果沒有發現這兩條記錄中的任何一條,那么S就可以單方面得決定Abort,同時向日志中寫入ABORT DECISION記錄,並重發決定。需要注意的是,要先插入COMMIT DECISION日志,再發送COMMIT決定給各個參與者進程,這很關鍵。為什么順序這么關鍵呢?試想一下,如果發送決定消息在前,插入日志在后,那么就會有一種可能,消息COMMIT DECISION發送完了但日志還沒來得及寫入的時候服務器發生故障了,當服務器恢復之后,按照前面的邏輯,它會認為還未做出任何決定,於是又單方面的決定ABORT DECISION,這下就和實際情況沖突了,參與者就會受到兩條完全沖突的決定:ABORT DECISION和COMMIT DECISION,系統會無法處理。如果寫日志在前,發送消息在后,系統也有可能在兩個時間點之間發生故障,協調員恢復時會看見日志,因此不會做任何事或者把決定重新發送一遍,因為決定事先已經達成,即使有可能消息還沒有發送,但至少不會做出自相矛盾的決定令參與者無法是從。
2> 如果S沒有發現任何記錄,S就會認為自己是一台參與者。那么就會有三種情況:
1. DT log中包含了COMMIT ACCEPT或者ABORT ACCEPT記錄,那參與者已經獲得了決定,那么參與者可以自己來決定,可以根據記錄來查看相應的操作是否完成,如果還未完成可以繼續從而完成相應操作。
2. 如果日志中沒有包含VOTE YES記錄以及任何COMMIT ACCEPT或者ABORT ACCEPT記錄,我們無法得到它當時是選擇YES還是NO。我們寫VOTE YES記錄的時間也要比發送實際消息早,盡可能早得保存決定。此時S可以單方面得決定ABORT ACCEPT。
3. 如果日志中包含VOTE YES記錄但沒有任何COMMIT ACCEPT或者ABORT ACCEPT記錄。那么參與者是在不確定期發生故障的,因此它采用終止協議來獲得決定。
對於一個實際的系統而言,系統需要處理的是很多的事務,因此不同事務的日志是交錯得存放在DT Log里。因此每條日志記錄需要包含事務的名字。而且隨着時間的積累,事務越來越多,日志的體積也會越來越龐大。因此需要定期對日志進行垃圾回收。日志垃圾回收有2個准則:
GC1:一台服務器不能刪除事務T的日志,直到它的RM(Recovery Manager)已經處理完了RM-Commit(T)或者RM-Abort(T)
GC2:一台服務器不能刪除事務T的日志,直到該服務器收到消息,所有其他服務器的RM-Commit(T)或者Rm-Abort(T)已經處理完畢。
對於GC1,通過本地的信息很容易得到。對於GC2,則需要服務器之間能夠相互通信,你可以讓協調員來執行GC2,或者完全分布式得由各個服務器通過相互交流完成GC2.
由於實際系統同時並發得處理很多事務,因此在某台服務器恢復的時候,我們還需要考慮一些細節問題。當服務器恢復時,它需要把繼續完成那些還未COMMIT或ABORT的事務,這些事務在完全恢復之前都會被block住從而無法訪問數據庫這部分資源,這會造成浪費。因此解決的方法是不是在整個恢復階段一直hold住這些待恢復並且在故障之前處於不確定期被block住得事務的所有的讀寫鎖,而是把這些鎖暫時全部釋放,然后再通過重新爭取鎖的方式來和新到的事務來競爭鎖,這樣避免了在整個恢復階段所有的block資源都無法訪問。具體的流程是這樣的,服務器恢復后,先處理那些沒有被block住的事務,為這些事務做出決定。然后再處那些故障前被block的事務,這時候恢復程序先釋放這些事務的所有讀寫鎖,然后再與故障之后新的事務一起競爭重新請求這些讀寫鎖。一旦恢復程序先釋放了待恢復的block事務的讀寫鎖,那么這些事務所持有的數據庫資源就可以被訪問了。當然由於有競爭,原來本來可以COMMIT的事務可能由於資源競爭被ABORT掉了,但帶來的好處是吞吐量大大提高。在原來的方案中,事務的鎖可以保存在DT Log里,在競爭的方案中,鎖可以不必保存,因為服務器進程可以根據Log自行決定。