本文地址:http://www.cnblogs.com/xybaby/p/7465816.htmlhtml
思考這個問題的初衷,是有一次給朋友轉帳,結果個人錢被扣了,朋友沒收到錢。而我以前一直認爲銀行轉帳必定是由事務保證強一致性的,因而學習、總結了一下分佈式事務的各類理論、方法。node
事務是一個很是廣義的詞彙,各行各業解讀都不同。對於程序員,事務等價於Transaction,是指一組連續的操做,這些操做組合成一個邏輯的、完整的操做。即這組操做執行先後,系統須要處於一個可預知的、一致的狀態。所以,這一組操做要麼都成功執行,要麼都不能執行;若是部分紅功,部分失敗,成功的部分須要回滾(rollback)。python
大多數人可能和我同樣,第一次據說事務是在學習關係型數據庫(mysql、sql server、Oracle)的時候,在關係型數據庫中,若是一組操做知足ACID特性,那麼稱之爲一個事務。關於關係型數據庫的ACID特性,無論是教材仍是網絡上都有大量的資料,這裏只簡單介紹。mysql
A(Atomic):原子性,構成事務的全部操做,要麼都執行完成,要麼所有不執行,不可能出現部分紅功部分失敗的狀況
C(Consistency):一致性,在事務執行先後,數據庫的一致性約束沒有被破壞。這裏的一致性含義後面會詳細解釋
I(Isolation):隔離性,數據庫中的事務通常都是併發的,隔離性是指併發的兩個事務的執行互不干擾,一個事務不能看到其餘事務運行過程的中間狀態
D(Durability):持久性,事務完成以後,該事務對數據的更改會被持久化到數據庫,且不會被回滾。程序員
咱們舉一個簡單的轉帳的例子,用戶A給玩家B轉100塊錢,那麼涉及到兩個操做:玩家A的帳戶扣100元,玩家B的帳戶加100元。即sql
UserA.account -= 100
UserB.account += 100mongodb
原子性很好理解,這兩個操做要麼都成功,要麼都不執行(更準確的是從效果上來看等價於都沒有執行)。不可能出現用戶A的錢減小了而用戶B的錢沒增長的狀況,用戶是不容許的;更不可能出現用戶B的錢增長 而 用戶A的錢沒有減小的狀況,銀行是絕對不幹的。數據庫
一致性說一塊兒來你們都懂,可是深究起來也是似懂非懂。ACID中的一致性,網絡上的介紹都很模糊,都是說要處於一致的狀態,那什麼是一致的狀態呢,好比轉帳操做中,A扣錢,B加錢,AB的錢的綜合是必定的,這個是否屬於ACID中的Consistency呢?我以爲不是的,Wiki Transaction_processing和Wiki: ACID分別是這麼描述的網絡
Consistency: A transaction is a correct transformation of the state. The actions taken as a group do not violate any of the integrity constraints associated with the state.多線程
The consistency property ensures that any transaction will bring the database from one valid state to another. Any data written to the database must be valid according to all defined rules, including constraints, cascades, triggers, and any combination thereof. This does not guarantee correctness of the transaction in all ways the application programmer might have wanted (that is the responsibility of application-level code), but merely that any programming errors cannot result in the violation of any defined rules.
上面黑色加粗的部分指出,ACID中的一致性是指完整性約束不被破壞,完整性包含實體完整性(主屬性不爲空)、參照完整性(外鍵必須存在原表中)、用戶自定義的完整性。用戶自定義的完整性好比列值非空(not null)、列值惟一(unique)、列值是否知足一個bool表達式(check語句,如性別只能有兩個值、歲數是必定範圍內的整數等),例如age smallint CHECK (age >=0 AND age <= 120).數據庫保證age的值在[0, 120]的範圍,若是不在這個範文,那麼更新操做失敗,事務也會失敗。另外,向mysql中的cascade,以及觸發器(trigger)都屬於用戶自定義的完整性約束。在MongoDB3.2中document validation就是用戶自定義的完整性約束,在插入或者更新docuemnt的時候檢查,不過用戶能夠自行設定validationAction,肯定當數據不符合約束時的表現,默認爲error,即拒絕數據寫操做。
所以,用戶A,B在此次事務操做先後,帳戶的總和必定,是應用層面的一致性,而不是數據庫保證的一致性,應用層面的一致性事實上是由原子性來保證的。
隔離性提及來簡單,但事實上背後的事情很複雜,數據庫的隔離性依賴於加鎖或者多版本控制。簡單來講,若是UserA.account初始值爲500,執行完第一條指令(即減去100),但事務尚未提交,其餘的事務是不能讀到這個中間結果(UserA.account的值爲400)的。這就是避免了髒讀(Drity Read),對應的隔離級別就是READ_COMMITTED。在SQL標準中,定義了四個隔離級別:
READ_UNCOMMITTED
READ_COMMITTED
REPEATABLE_READ
SERIALIZABLE
來解決事務併發中帶來的一下幾個問題髒讀(Dirty Read)、不可重複讀(Non-repeatable Read)、幻讀(Phantom Read)
不一樣的數據庫或者說存儲引擎默認支持不一樣的隔離級別,好比InnoDB存儲引擎默認支持REPEATABLE_READ,而Mongodb只支持READ_UNCOMMITTED
持久性須要考慮到一個事務在執行過程當中的各類狀況的異常。一個事務的流程是這樣的:
開啓一個事務
執行一組操做
若是都執行成功,那麼提交併結束事務
若是任何操做失敗,那麼回滾已經執行的操做,結束事務
在事務執行過程當中,若是出現故障,好比斷電、宕機,這個時候就要利用日誌(redo log或者undo log) 加上 checkpoint來保證事務的完整結束。
當數據的規模愈來愈大,超出了單個關係型數據庫的處理能力,這個時候就出現了關係型數據的垂直分表或者分表,也出現了自然支持水平擴展(sharding)的NoSql。另外,大型網站的服務化(SOA)以及這兩年很是火的微服務,每每將服務進行拆分,單獨部署,天然也使用獨立的數據庫,甚至是異構的數據庫。這個時候,關係型數據庫保證事務的手段,好比加鎖、日誌就行不通了。固然,本文討論的不只僅是數據庫,也包含分佈式存儲、消息隊列,以及任何要保證原子性、持久性的邏輯。
分佈式事務的最大挑戰在於CAP,在《CAP理論與MongoDB一致性、可用性的一些思考》一文中有詳細介紹。簡而言之,因爲網絡分割(P: Network Partition)的存在,用戶不得不在一致性(C Consistency)與可用性(A: Avaliable)以前作權衡。若是要保證強一致性(主要是應用層面的強一致性),那麼在網絡分割的時候,系統就不可用;若是要保證高可用性,那麼就只能提供弱一致性,保證最終一致。下面提到的各類實現分佈式事務的方法、協議都須要在一致性與可用性之間權衡。
提到分佈式事務,首先想到的確定是兩階段提交(2pc, two-phase commit protocol),2pc是很是經典的強一致性、中心化的原子提交協議。中心化是指協議中有兩類節點:一箇中心化協調者節點(coordinator)和N個參與者節點(participant、cohort)。
顧名思義,兩階段提交協議的每一次事務提交分爲兩個階段:
在第一階段,協調者詢問全部的參與者是否能夠提交事務(請參與者投票),全部參與者向協調者投票。
在第二階段,協調者根據全部參與者的投票結果作出是否事務能夠全局提交的決定,並通知全部的參與者執行該決定。在一個兩階段提交流程中,參與者不能改變本身的投票結果。兩階段提交協議的能夠全局提交的前提是全部的參與者都贊成提交事務,只要有一個參與者投票選擇放棄(abort)事務,則事務必須被放棄。
wiki上給出了簡要流程:
注意,上圖中洗下面一行也代表,兩階段提交協議也依賴與日誌,只要存儲介質不出問題,兩階段協議就能最終達到一致的狀態(成功或者回滾)
而下圖(來自slideshare)詳細描述了整個流程:
在劉傑的《分佈式原理介紹中》,有很是詳細的流程介紹,能夠配合上圖一塊兒看,另外還介紹了在各類異常狀況下(好比Coordinator、Participant宕機,網絡分割致使的超時)兩階段協議的工做狀況。另外,在這篇文章中也有比較清晰的流程介紹。在這裏只討論2PC的優缺點:
優勢:強一致性,只要節點或者網絡最終恢復正常,協議就能保證順利結束;部分關係型數據庫(Oracle)、框架直接支持
缺點:兩階段提交協議的容錯能力較差,好比在節點宕機或者超時的狀況下,沒法肯定流程的狀態,只能不斷重試;兩階段提交協議的性能較差, 消息交互多,且受最慢節點影響
這篇文章描述了爲何兩階段提交協議在分佈式系統中不適用:
系統「水平」伸縮的死敵。基於兩階段提交的分佈式事務在提交事務時須要在多個節點之間進行協調,最大限度地推後了提交事務的時間點,客觀上延長了事務的執行時間,這會致使事務在訪問共享資源時發生衝突和死鎖的機率增高,隨着數據庫節點的增多,這種趨勢會愈來愈嚴重,從而成爲系統在數據庫層面上水平伸縮的"枷鎖", 這是不少Sharding系統不採用分佈式事務的主要緣由。
所言甚是!
三階段提交協議(3pc Three-phase_commit_protocol)主要是爲了解決兩階段提交協議的阻塞問題,從原來的兩個階段擴展爲三個階段,而且增長了超時機制。
3PC只是解決了在異常狀況下2PC的阻塞問題,但致使一次提交要傳遞6條消息,延時很大。具體流程描述可參見《關於分佈式事務、兩階段提交協議、三階提交協議 》一文。
TCC是Try、Commit、Cancel的縮寫,在國內因爲支付寶的佈道而廣爲人知,TCC在保證強一致性的同時,最大限度提升系統的可伸縮性與可用性。
咱們假設一個完整的爲業務包含一組子業務,Try操做完成全部的子業務檢查,預留必要的業務資源,實現與其餘事務的隔離;Confirm使用Try階段預留的業務資源真正執行業務,並且Confirm操做知足冪等性,以遍支持重試;Cancel操做釋放Try階段預留的業務資源,一樣也知足冪等性。「一次完整的交易由一系列微交易的Try 操做組成,若是全部的Try 操做都成功,最終由微交易框架來統一Confirm,不然統一Cancel,從而實現了相似經典兩階段提交協議(2PC)的強一致性。」
與2PC協議比較 ,TCC擁有如下特色:
位於業務服務層而非資源層 ,由業務層保證原子性
沒有單獨的準備(Prepare)階段,下降了提交協議的成本
Try操做 兼備資源操做與準備能力
Try操做能夠靈活選擇業務資源的鎖定粒度,而不是鎖住整個資源,提升了併發度
固然,TCC須要較的高開發成本,每一個子業務都須要有響應的comfirm、Cancel操做,即實現相應的補償邏輯。
這類事務機制將分佈式事務分紅多個本地事務,這裏稱之爲主事務與從事務。首先主事務本地先行提交,而後經過消息通知從事務,從事務從消息中獲取信息進行本地提交。能夠看出這是一種異步事務機制、只能保證最終一致性;但可用性很是高,不會由於故障而發生阻塞。另外,主事務已經先行提交,若是由於從事務沒法提交,要回滾主事務仍是比較麻煩,因此這種模式只適用於理論上大機率等成功的業務狀況,即從事務的提交失敗多是因爲故障,而不大多是邏輯錯誤。
基於異步消息的事務機制主要有兩種方式:本地消息表與事務消息。兩者的區別在於:怎麼保證主事務的提交與消息發送這兩個操做的原子性。
若是用異步消息實現轉帳的例子,那麼操做分爲四部:用戶A扣錢,發消息,用戶B收消息,用戶B扣錢。前兩步必須保證原子性,若是A扣錢成功可是沒有發出消息,那麼用戶A損失了;若是發消息成功,可是沒有扣錢,那麼用戶B就多得了一筆錢,銀行確定不幹。
基於本地消息表的方案是指將消息寫入本地數據庫,經過本地事務保證主事務與消息寫入的原子性。例如銀行轉帳的例子,僞碼以下:
begin transaction:update User set account = account - 100 where userId = 'A'
insert into message(userId, amount, status) values('A', 100, 1)commit transaction
而後經過pull或者push模式,從業務獲取消息並執行。若是是push模式,那麼通常使用具備持久化功能的消息隊列,從事務務訂閱消息。若是是pull模式,那麼從事務定時去拉取消息,而後執行。
mongodb的寫入就很像本地消息表,在WriteConcern爲w:1的狀況下,更新操做只要寫到oplog以及primary就能夠向客戶端返回。secondary異步拉取oplog並本地記錄執行。
事務消息依賴於支持「事務消息」的消息隊列,其基本思想是 利用消息中間間實施兩階段提交,將本地事務和發消息放在了一個分佈式事務裏,保證要麼本地操做成功成功而且對外發消息成功,要麼二者都失敗。流程以下:
主事務向消息隊列發送預備消息主事務收到ACK以後本地執行主事務
根據執行的結果(成功或失敗)向消息隊列發送提交或者回滾消息
詳細的流程以下圖(圖片來源見水印)所示:
不難看到,相比本地消息表的方式,事務消息由消息中間件保證本地事務與消息的原子性,不依賴於本地數據庫存儲消息。但實現了「事務消息」的消息隊列比較少,還不夠通用。
無論是本地消息表仍是事務消息,都須要保證從事務執行且僅僅執行一次,exact once。若是失敗,須要重試,但也不可能無限次的重試,當從事務最終失敗的狀況下,須要通知主業務回滾嗎?可是此時,主事務已經提交,所以只能經過補償,實現邏輯上的回滾,而當前時間點距主事務的提交已經有必定時間,回滾也可能失敗。所以,最好是保證從事務邏輯上不會失敗,萬一失敗,記錄log並報警,人工介入。
1PC(one phase commit)這個概念,我是在《Distributed systems for fun and profit》一文中看到的,應該是對標2PC,3PC。在wiki中並無正式的詞條,在google上的文章也不是不少。在個人理解中,1PC適用於分佈式存儲系統的複製集,即複製集中多個節點的數據提交,。通常來講,這些節點存儲一樣的數據,只要單個節點能提交,其餘節點理論上也應該能夠提交。 在《Distributed systems for fun and profit》中是這麼描述的:
Having a second phase in place before the commit is considered permanent is useful, because it allows the system to roll back an update when a node fails. In contrast, in primary/backup ("1PC"), there is no step for rolling back an operation that has failed on some nodes and succeeded on others, and hence the replicas could diverge.
即對於分佈式存儲中使用很是普遍的中心化複製集協議Primary Secondary,在部分節點失敗、部分節點成功的狀況下沒有回滾操做,可能會致使不一致。不過這些分佈式存儲系統都竭力保證,這些不一致是暫時的,會經過重試等手段保證最終的一致。
1PC的優勢是性能很是好,並且只有在出現物理故障的時候纔會出現不一致。
好比在MongoDB中,更新操做會寫入Primary節點以及oplog collection,Secondary節點從Primary節點的oplog collection拉取操做日誌並執行,這是一個異步的過程。及時Secondary節點由於故障執行oplog失敗,Promary節點的數據也不會回滾。在《帶着問題學習分佈式系統之中心化複製集》中也提到過,爲了提升數據可靠性(避免極端狀況下數據被回滾),設定WriteConcern爲w:Majority,(shard有一個Primary 一個Secondary 一個Arbiter組成)。若是這個時候因爲其中一個secondary掛掉,寫入操做是不可能成功的。所以,在超時時間到達以後,會向客戶端返回出錯信息。可是在這個時候數據是持久化到了primary節點,不會被回滾。若是此時Secondary重啓,那麼是會從Primary拉取日誌並執行。因此當客戶端返回的出錯信息包含WriteResult.writeConcernError 時,應該謹慎處理
對於分佈式文件系統GFS、haystack,若是Secondary節點失敗,也會採起簡單粗暴的重試,並經過一些機制(cheksum,offset)來保證最終能讀到正確的數據
更多的時候,分佈式事務只須要保證原子性,這個原子性也保證了應用層面上的一致性,而由本地事務來保證隔離性、持久性。
原子性這個東西,即便不是分佈式,僅僅是單進程單線程也是須要考慮的,這就是C++中的RAII,python中的with statement,以及各類語言的try...finally...。當涉及到跨進程、異步通訊的時候,就很難經過語言層面的機制保證原子性了。
在分佈式領域,因爲網絡或者機器故障,常常須要重試,所以冪等性很是重要
不少場景,好比電商、網絡購票,首先要保證的是高可用,不大可能採用強一致性,所以咱們也會看到‘正在處理中...‘這種中間狀態,後臺極可能是異步處理的,在12306買過票的話都知道,下單成功到最後是否能出票由很長一段時間。
在筆者的業務領域,並無涉及到強一致性的場景,只要最終一致性就好了。上面的提到的各類辦法,無論是2PC、TCC、本地消息表、事務消息,都須要引入額外的框架或者組件。因此更多的時候是採起業務補償的方式,好比一個涉及兩個進程的操做須要保證原子性,進程間RPC通訊,那麼通常是A進程先執行,而後RPC調用B進程接口,根據B進程的返回結果,絕對是否回滾(補償);但若是涉及到異步RPC、或者多線程、或者兩個以上進程的串聯時,那麼就不必定能補償、甚至很難補償了,這個時候只記錄一個error log,而後通知人工排查。所以,事務補償只適合業務比較簡單的常見,並且很難造成通用的框架,或者說實用性不強。
以前一直覺得像銀行轉帳這種場景,必定是強一致性的。後來本身遇到這麼一回事,我給朋友轉帳,我這邊顯示轉帳成功,但朋友並無收到錢。我覺得是須要必定時間,結果24小時以後尚未收到。我本身從新比對轉帳單,才發現是把對方的開戶銀行寫錯了。所以可見,轉帳這個操做確定不是強一致性,具體怎麼搞的在網上也沒有查到。更坑爹的是,轉帳失敗,個人錢被扣了,朋友也沒有收到錢,可是我沒有收到任何消息,也沒有給我把錢退回來,在我打電話到銀行去諮詢以後才退回來。這個體驗真的不好,但銀行是大爺,沒辦法!
Wiki:two-phase commit protocol
劉傑:分佈式原理介紹
Distributed systems for fun and profit