分佈式事務有2種實現方式:java
使用數據庫自己自帶的特性(強一致)數據庫
複雜的業務交互過程當中,不建議使用強一致性的分佈式事務。解決分佈式事務的最好辦法就是不考慮分佈式事務。就像剛說的問題同樣,把分佈式的事務過程拆解成多箇中間狀態,中間狀態的東西不容許用戶直接操做,等狀態都一致成功,或者檢測到不一致的時候所有失敗掉。就解耦了這個強一致性的過程。服務器
通常狀況下準實時就成了。涉及到錢,有時候也能夠這麼搞。
淘寶幾s內完整一個訂單處理,不是什麼問題吧。
銀行也不是所有都強一致性。也會扎差,也會衝正。
併發
特別是涉及到多個系統的時候,咱們好比買記票,支付完成之後,只支付完成狀態,而後返回給用戶了,咱們過幾分鐘再刷新頁面,纔會看到變成已出票,訂單完成狀態。
這個時候,若是咱們要求全部處理,都是強一致性的,那麼久完蛋了。頁面要死在那兒幾分鐘,才把這個事務處理完成,返回給用戶。分佈式
這樣就確定涉及一個問題,支付了,可是最終出票沒出來。那就沒辦法,商量換票或退款。
淘寶的訂單改爲出票失敗,給支付發消息通知退款。spa
慢的時候,有多是手工出票,這時出一張票半小時均可能,若是要求都必須強一致性的話,全部處理線程都掛在哪兒,系統早就完蛋了。線程
解決分佈式事務的最好辦法就是不考慮分佈式事務。設計
拆分,大的業務流程,轉化成幾個小的業務流程,而後考慮最終一致性。日誌
模擬分佈式事務流程(兩段式提交就是解決分佈式事務的一種方式):code
兩段式提交設計自己的思路很是的容易理解,步驟以下:
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自行決定。