思考這個(gè)問(wèn)題的初衷,是有一次給朋友轉(zhuǎn)賬,結(jié)果我的錢被扣了,朋友沒收到錢。而我之前一直認(rèn)為銀行轉(zhuǎn)賬一定是由事務(wù)保證強(qiáng)一致性的,于是學(xué)習(xí)、總結(jié)了一下分布式事務(wù)的各種理論、方法。 事務(wù)是一個(gè)非常廣義的詞匯,各行各業(yè)解讀都不一樣。對(duì)于程序員,事務(wù)等價(jià)于Transaction,是指一組連續(xù)的操作,這些操作組合成一個(gè)邏輯的、完整的操作。即這組操作執(zhí)行前后,系統(tǒng)需要處于一個(gè)可預(yù)知的、一致的狀態(tài)。因此,這一組操作要么都成功執(zhí)行,要么都不能執(zhí)行;如果部分成功,部分失敗,成功的部分需要回滾(rollback)。
關(guān)系型數(shù)據(jù)庫(kù)事務(wù)大多數(shù)人可能和我一樣,第一次聽說(shuō)事務(wù)是在學(xué)習(xí)關(guān)系型數(shù)據(jù)庫(kù)(mysql、sql server、Oracle)的時(shí)候,在關(guān)系型數(shù)據(jù)庫(kù)中,如果一組操作滿足ACID特性,那么稱之為一個(gè)事務(wù)。關(guān)于關(guān)系型數(shù)據(jù)庫(kù)的ACID特性,不管是教材還是網(wǎng)絡(luò)上都有大量的資料,這里只簡(jiǎn)單介紹。
我們舉一個(gè)簡(jiǎn)單的轉(zhuǎn)賬的例子,用戶A給玩家B轉(zhuǎn)100塊錢,那么涉及到兩個(gè)操作:玩家A的賬戶扣100元,玩家B的賬戶加100元。即
原子性很好理解,這兩個(gè)操作要么都成功,要么都不執(zhí)行(更準(zhǔn)確的是從效果上來(lái)看等價(jià)于都沒有執(zhí)行)。不可能出現(xiàn)用戶A的錢減少了而用戶B的錢沒增加的情況,用戶是不允許的;更不可能出現(xiàn)用戶B的錢增加 而 用戶A的錢沒有減少的情況,銀行是絕對(duì)不干的。 一致性說(shuō)一起來(lái)大家都懂,但是深究起來(lái)也是似懂非懂。ACID中的一致性,網(wǎng)絡(luò)上的介紹都很模糊,都是說(shuō)要處于一致的狀態(tài),那什么是一致的狀態(tài)呢,比如轉(zhuǎn)賬操作中,A扣錢,B加錢,AB的錢的綜合是一定的,這個(gè)是否屬于ACID中的Consistency呢?我覺得不是的,Wiki Transaction_processing(https://en./wiki/Transaction_processing)和Wiki: ACID(https://en./wiki/ACID)分別是這么描述的:
上面黑色加粗的部分指出,ACID中的一致性是指完整性約束不被破壞,完整性包含實(shí)體完整性(主屬性不為空)、參照完整性(外鍵必須存在原表中)、用戶自定義的完整性。用戶自定義的完整性比如列值非空(not null)、列值唯一(unique)、列值是否滿足一個(gè)bool表達(dá)式(check語(yǔ)句,如性別只能有兩個(gè)值、歲數(shù)是一定范圍內(nèi)的整數(shù)等),例如age smallint CHECK (age >=0 AND age <= 120).數(shù)據(jù)庫(kù)保證age的值在[0,="" 120]的范圍,如果不在這個(gè)范文,那么更新操作失敗,事務(wù)也會(huì)失敗。另外,向mysql中的cascade,以及觸發(fā)器(trigger)都屬于用戶自定義的完整性約束。在mongodb3.2中document="" validation="">=>http://www.cnblogs.com/xybaby/p/%20https://docs./manual/core/document-validation/)就是用戶自定義的完整性約束,在插入或者更新docuemnt的時(shí)候檢查,不過(guò)用戶可以自行設(shè)定validationAction,確定當(dāng)數(shù)據(jù)不符合約束時(shí)的表現(xiàn),默認(rèn)為error,即拒絕數(shù)據(jù)寫操作。 因此,用戶A,B在這次事務(wù)操作前后,賬戶的總和一定,是應(yīng)用層面的一致性,而不是數(shù)據(jù)庫(kù)保證的一致性,應(yīng)用層面的一致性事實(shí)上是由原子性來(lái)保證的。 隔離性說(shuō)起來(lái)簡(jiǎn)單,但事實(shí)上背后的事情很復(fù)雜,數(shù)據(jù)庫(kù)的隔離性依賴于加鎖或者多版本控制。簡(jiǎn)單來(lái)說(shuō),如果UserA.account初始值為500,執(zhí)行完第一條指令(即減去100),但事務(wù)還沒有提交,其他的事務(wù)是不能讀到這個(gè)中間結(jié)果(UserA.account的值為400)的。這就是避免了臟讀(Drity Read),對(duì)應(yīng)的隔離級(jí)別就是READ_COMMITTED。在SQL標(biāo)準(zhǔn)中,定義了四個(gè)隔離級(jí)別:
來(lái)解決事務(wù)并發(fā)中帶來(lái)的一下幾個(gè)問(wèn)題臟讀(Dirty Read)、不可重復(fù)讀(Non-repeatable Read)、幻讀(Phantom Read) 不同的數(shù)據(jù)庫(kù)或者說(shuō)存儲(chǔ)引擎默認(rèn)支持不同的隔離級(jí)別,比如InnoDB存儲(chǔ)引擎默認(rèn)支持REPEATABLE_READ,而Mongodb只支持READ_UNCOMMITTED
持久性需要考慮到一個(gè)事務(wù)在執(zhí)行過(guò)程中的各種情況的異常。一個(gè)事務(wù)的流程是這樣的:
在事務(wù)執(zhí)行過(guò)程中,如果出現(xiàn)故障,比如斷電、宕機(jī),這個(gè)時(shí)候就要利用日志(redo log或者undo log) 加上 checkpoint來(lái)保證事務(wù)的完整結(jié)束。 分布式事務(wù)當(dāng)數(shù)據(jù)的規(guī)模越來(lái)越大,超出了單個(gè)關(guān)系型數(shù)據(jù)庫(kù)的處理能力,這個(gè)時(shí)候就出現(xiàn)了關(guān)系型數(shù)據(jù)的垂直分表或者水平分表,也出現(xiàn)了天然支持水平擴(kuò)展(sharding)的NoSql。另外,大型網(wǎng)站的服務(wù)化(SOA)以及這兩年非?;鸬奈⒎?wù),往往將服務(wù)進(jìn)行拆分,單獨(dú)部署,自然也使用獨(dú)立的數(shù)據(jù)庫(kù),甚至是異構(gòu)的數(shù)據(jù)庫(kù)。這個(gè)時(shí)候,關(guān)系型數(shù)據(jù)庫(kù)保證事務(wù)的手段,比如加鎖、日志就行不通了。當(dāng)然,本文討論的不僅僅是數(shù)據(jù)庫(kù),也包含分布式存儲(chǔ)、消息隊(duì)列,以及任何要保證原子性、持久性的邏輯。 分布式事務(wù)的最大挑戰(zhàn)在于CAP,在《CAP理論與MongoDB一致性、可用性的一些思考》(http://www.cnblogs.com/xybaby/p/6871764.html)一文中有詳細(xì)介紹。簡(jiǎn)而言之,由于網(wǎng)絡(luò)分割(P: Network Partition)的存在,用戶不得不在一致性(C Consistency)與可用性(A: Avaliable)之前做權(quán)衡。如果要保證強(qiáng)一致性(主要是應(yīng)用層面的強(qiáng)一致性),那么在網(wǎng)絡(luò)分割的時(shí)候,系統(tǒng)就不可用;如果要保證高可用性,那么就只能提供弱一致性,保證最終一致。下面提到的各種實(shí)現(xiàn)分布式事務(wù)的方法、協(xié)議都需要在一致性與可用性之間權(quán)衡。 2PC提到分布式事務(wù),首先想到的肯定是兩階段提交(2pc, two-phase commit protocol),2pc是非常經(jīng)典的強(qiáng)一致性、中心化的原子提交協(xié)議。中心化是指協(xié)議中有兩類節(jié)點(diǎn):一個(gè)中心化協(xié)調(diào)者節(jié)點(diǎn)(coordinator)和N個(gè)參與者節(jié)點(diǎn)(participant、cohort)。 顧名思義,兩階段提交協(xié)議的每一次事務(wù)提交分為兩個(gè)階段: 在第一階段,協(xié)調(diào)者詢問(wèn)所有的參與者是否可以提交事務(wù)(請(qǐng)參與者投票),所有參與者向協(xié)調(diào)者投票。 在第二階段,協(xié)調(diào)者根據(jù)所有參與者的投票結(jié)果做出是否事務(wù)可以全局提交的決定,并通知所有的參與者執(zhí)行該決定。在一個(gè)兩階段提交流程中,參與者不能改變自己的投票結(jié)果。兩階段提交協(xié)議的可以全局提交的前提是所有的參與者都同意提交事務(wù),只要有一個(gè)參與者投票選擇放棄(abort)事務(wù),則事務(wù)必須被放棄。 wiki上給出了簡(jiǎn)要流程: 注意,上圖中最下面一行也表明,兩階段提交協(xié)議也依賴與日志,只要存儲(chǔ)介質(zhì)不出問(wèn)題,兩階段協(xié)議就能最終達(dá)到一致的狀態(tài)(成功或者回滾) 而下圖(來(lái)自slideshare:https://www./sourabhdave/distributed-databases-24839617)詳細(xì)描述了整個(gè)流程: 在劉杰的《分布式原理介紹中》,有非常詳細(xì)的流程介紹,可以配合上圖一起看,另外還介紹了在各種異常情況下(比如Coordinator、Participant宕機(jī),網(wǎng)絡(luò)分割導(dǎo)致的超時(shí))兩階段協(xié)議的工作情況、工作效率。另外,在這篇文章(http://blog./95632/)中也有比較清晰的流程介紹。在這里只討論2PC的優(yōu)缺點(diǎn):
這篇文章(http://www.cnblogs.com/xybaby/p/%20http://blog.csdn.net/m0_38031406/article/details/76474800)描述了為什么兩階段提交協(xié)議在分布式系統(tǒng)中不適用:
所言甚是! 3PC三階段提交協(xié)議(3pc Three-phase_commit_protocol)主要是為了解決兩階段提交協(xié)議的阻塞問(wèn)題,從原來(lái)的兩個(gè)階段擴(kuò)展為三個(gè)階段,并且增加了超時(shí)機(jī)制。 3PC只是解決了在異常情況下2PC的阻塞問(wèn)題,但導(dǎo)致一次提交要傳遞6條消息,延時(shí)很大。具體流程描述可參見《關(guān)于分布式事務(wù)、兩階段提交協(xié)議、三階提交協(xié)議 》(https://en./wiki/ACID)一文。 TCCTCC是Try、Commit、Cancel的縮寫,在國(guó)內(nèi)由于支付寶的布道(http://www.sohu.com/a/124709543_468650%20)而廣為人知,TCC在保證強(qiáng)一致性的同時(shí),最大限度提高系統(tǒng)的可伸縮性與可用性。 我們假設(shè)一個(gè)完整的業(yè)務(wù)包含一組子業(yè)務(wù),Try操作完成所有的子業(yè)務(wù)檢查,預(yù)留必要的業(yè)務(wù)資源,實(shí)現(xiàn)與其他事務(wù)的隔離;Confirm使用Try階段預(yù)留的業(yè)務(wù)資源真正執(zhí)行業(yè)務(wù),而且Confirm操作滿足冪等性,以遍支持重試;Cancel操作釋放Try階段預(yù)留的業(yè)務(wù)資源,同樣也滿足冪等性。“一次完整的交易由一系列微交易的Try 操作組成,如果所有的Try 操作都成功,最終由微交易框架來(lái)統(tǒng)一Confirm,否則統(tǒng)一Cancel,從而實(shí)現(xiàn)了類似經(jīng)典兩階段提交協(xié)議(2PC)的強(qiáng)一致性?!?/span> 與2PC協(xié)議比較 ,TCC擁有以下特點(diǎn):
當(dāng)然,TCC需要較高的開發(fā)成本,每個(gè)子業(yè)務(wù)都需要有響應(yīng)的comfirm、Cancel操作,即實(shí)現(xiàn)相應(yīng)的補(bǔ)償邏輯。 基于消息的分布式事務(wù)這類事務(wù)機(jī)制將分布式事務(wù)分成多個(gè)本地事務(wù),這里稱之為主事務(wù)與從事務(wù)。首先主事務(wù)本地先行提交,然后通過(guò)消息通知從事務(wù),從事務(wù)從消息中獲取信息進(jìn)行本地提交??梢钥闯鲞@是一種異步事務(wù)機(jī)制、只能保證最終一致性;但可用性非常高,不會(huì)因?yàn)楣收隙l(fā)生阻塞。另外,主事務(wù)已經(jīng)先行提交,如果因?yàn)閺氖聞?wù)無(wú)法提交,要回滾主事務(wù)還是比較麻煩,所以這種模式只適用于理論上大概率等成功的業(yè)務(wù)情況,即從事務(wù)的提交失敗可能是由于故障,而不大可能是邏輯錯(cuò)誤。 基于異步消息的事務(wù)機(jī)制主要有兩種方式:本地消息表與事務(wù)消息。二者的區(qū)別在于:怎么保證主事務(wù)的提交與消息發(fā)送這兩個(gè)操作的原子性。 如果用異步消息實(shí)現(xiàn)轉(zhuǎn)賬的例子,那么操作分為四部:用戶A扣錢,發(fā)消息,用戶B收消息,用戶B扣錢。前兩步必須保證原子性,如果A扣錢成功但是沒有發(fā)出消息,那么用戶A損失了;如果發(fā)消息成功,但是沒有扣錢,那么用戶B就多得了一筆錢,銀行肯定不干。 本地消息表基于本地消息表的方案是指將消息寫入本地?cái)?shù)據(jù)庫(kù),通過(guò)本地事務(wù)保證主事務(wù)與消息寫入的原子性。例如銀行轉(zhuǎn)賬的例子,偽碼(http://blog.csdn.net/gaowenhui2008/article/details/53910341)如下: begin transaction: 然后通過(guò)pull或者push模式,從業(yè)務(wù)獲取消息并執(zhí)行。如果是push模式,那么一般使用具有持久化功能的消息隊(duì)列,從事務(wù)務(wù)訂閱消息。如果是pull模式,那么從事務(wù)定時(shí)去拉取消息,然后執(zhí)行。 MongoDB的寫入就很像本地消息表,在WriteConcern為w:1的情況下,更新操作只要寫到oplog以及primary就可以向客戶端返回。secondary異步拉取oplog并本地記錄執(zhí)行。 事務(wù)消息:事務(wù)消息依賴于支持“事務(wù)消息”的消息隊(duì)列,其基本思想是 利用消息中間間實(shí)施兩階段提交,將本地事務(wù)和發(fā)消息放在了一個(gè)分布式事務(wù)里,保證要么本地操作成功成功并且對(duì)外發(fā)消息成功,要么兩者都失敗。流程如下:
主事務(wù)向消息隊(duì)列發(fā)送預(yù)備消息
不難看到,相比本地消息表的方式,事務(wù)消息由消息中間件保證本地事務(wù)與消息的原子性,不依賴于本地?cái)?shù)據(jù)庫(kù)存儲(chǔ)消息。但實(shí)現(xiàn)了“事務(wù)消息”的消息隊(duì)列比較少,還不夠通用。
不管是本地消息表還是事務(wù)消息,都需要保證從事務(wù)執(zhí)行且僅僅執(zhí)行一次,exact once。如果失敗,需要重試,但也不可能無(wú)限次的重試,當(dāng)從事務(wù)最終失敗的情況下,需要通知主業(yè)務(wù)回滾嗎?但是此時(shí),主事務(wù)已經(jīng)提交,因此只能通過(guò)補(bǔ)償,實(shí)現(xiàn)邏輯上的回滾,而當(dāng)前時(shí)間點(diǎn)距主事務(wù)的提交已經(jīng)有一定時(shí)間,回滾也可能失敗。因此,最好是保證從事務(wù)邏輯上不會(huì)失敗,萬(wàn)一失敗,記錄log并報(bào)警,人工介入。 1PC1PC(one phase commit)這個(gè)概念,我是在《Distributed systems for fun and profit》(http://book./distsys/)一文中看到的,應(yīng)該是對(duì)標(biāo)2PC,3PC。在wiki中并沒有正式的詞條,在google上的文章也不是很多。在我的理解中,1PC適用于分布式存儲(chǔ)系統(tǒng)的復(fù)制集,即復(fù)制集中多個(gè)節(jié)點(diǎn)的數(shù)據(jù)提交,。一般來(lái)說(shuō),這些節(jié)點(diǎn)存儲(chǔ)同樣的數(shù)據(jù),只要單個(gè)節(jié)點(diǎn)能提交,其他節(jié)點(diǎn)理論上也應(yīng)該可以提交。 在《Distributed systems for fun and profit》中是這么描述的:
即對(duì)于分布式存儲(chǔ)中使用非常廣泛的中心化復(fù)制集協(xié)議Primary Secondary,在部分節(jié)點(diǎn)失敗、部分節(jié)點(diǎn)成功的情況下沒有回滾操作,可能會(huì)導(dǎo)致不一致。不過(guò)這些分布式存儲(chǔ)系統(tǒng)都竭力保證,這些不一致是暫時(shí)的,會(huì)通過(guò)重試等手段保證最終的一致。 1PC的優(yōu)點(diǎn)是性能非常好,而且只有在出現(xiàn)物理故障的時(shí)候才會(huì)出現(xiàn)不一致。 比如在MongoDB中,更新操作會(huì)寫入Primary節(jié)點(diǎn)以及oplog collection,Secondary節(jié)點(diǎn)從Primary節(jié)點(diǎn)的oplog collection拉取操作日志并執(zhí)行,這是一個(gè)異步的過(guò)程。及時(shí)Secondary節(jié)點(diǎn)因?yàn)楣收蠄?zhí)行oplog失敗,Promary節(jié)點(diǎn)的數(shù)據(jù)也不會(huì)回滾。在《帶著問(wèn)題學(xué)習(xí)分布式系統(tǒng)之中心化復(fù)制集》(http://www.cnblogs.com/xybaby/p/7153755.html)中也提到過(guò),為了提高數(shù)據(jù)可靠性(避免極端情況下數(shù)據(jù)被回滾),設(shè)定WriteConcern為w:Majority,(shard有一個(gè)Primary 一個(gè)Secondary 一個(gè)Arbiter組成)。如果這個(gè)時(shí)候由于其中一個(gè)secondary掛掉,寫入操作是不可能成功的。因此,在超時(shí)時(shí)間到達(dá)之后,會(huì)向客戶端返回出錯(cuò)信息。但是在這個(gè)時(shí)候數(shù)據(jù)是持久化到了primary節(jié)點(diǎn),不會(huì)被回滾。如果此時(shí)Secondary重啟,那么是會(huì)從Primary拉取日志并執(zhí)行。所以當(dāng)客戶端返回的出錯(cuò)信息包含WriteResult.writeConcernError (https://docs./manual/reference/method/WriteResult/#WriteResult.writeConcernError)時(shí),應(yīng)該謹(jǐn)慎處理 對(duì)于分布式文件系統(tǒng)GFS、haystack,如果Secondary節(jié)點(diǎn)失敗,也會(huì)采取簡(jiǎn)單粗暴的重試,并通過(guò)一些機(jī)制(cheksum,offset)來(lái)保證最終能讀到正確的數(shù)據(jù) 思考與總結(jié)更多的時(shí)候,分布式事務(wù)只需要保證原子性,這個(gè)原子性也保證了應(yīng)用層面上的一致性,而由本地事務(wù)來(lái)保證隔離性、持久性。 原子性這個(gè)東西,即使不是分布式,僅僅是單進(jìn)程單線程也是需要考慮的,這就是C++中的RAII,python中的with statement,以及各種語(yǔ)言的try...finally...。當(dāng)涉及到跨進(jìn)程、異步通信的時(shí)候,就很難通過(guò)語(yǔ)言層面的機(jī)制保證原子性了。 在分布式領(lǐng)域,由于網(wǎng)絡(luò)或者機(jī)器故障,經(jīng)常需要重試,因此冪等性非常重要 很多場(chǎng)景,比如電商、網(wǎng)絡(luò)購(gòu)票,首先要保證的是高可用,不大可能采用強(qiáng)一致性,因此我們也會(huì)看到‘正在處理中...‘這種中間狀態(tài),后臺(tái)很可能是異步處理的,在12306買過(guò)票的話都知道,下單成功到最后是否能出票由很長(zhǎng)一段時(shí)間。 在筆者的業(yè)務(wù)領(lǐng)域,并沒有涉及到強(qiáng)一致性的場(chǎng)景,只要最終一致性就行了。上面的提到的各種辦法,不管是2PC、TCC、本地消息表、事務(wù)消息,都需要引入額外的框架或者組件。所以更多的時(shí)候是采取業(yè)務(wù)補(bǔ)償?shù)姆绞剑热缫粋€(gè)涉及兩個(gè)進(jìn)程的操作需要保證原子性,進(jìn)程間RPC通信,那么一般是A進(jìn)程先執(zhí)行,然后RPC調(diào)用B進(jìn)程接口,根據(jù)B進(jìn)程的返回結(jié)果,絕對(duì)是否回滾(補(bǔ)償);但如果涉及到異步RPC、或者多線程、或者兩個(gè)以上進(jìn)程的串聯(lián)時(shí),那么就不一定能補(bǔ)償、甚至很難補(bǔ)償了,這個(gè)時(shí)候只記錄一個(gè)error log,然后通知人工排查。因此,事務(wù)補(bǔ)償只適合業(yè)務(wù)比較簡(jiǎn)單的常見,而且很難形成通用的框架,或者說(shuō)實(shí)用性不強(qiáng)。 之前一直以為像銀行轉(zhuǎn)賬這種場(chǎng)景,一定是強(qiáng)一致性的。后來(lái)自己遇到這么一回事,我給朋友轉(zhuǎn)賬,我這邊顯示轉(zhuǎn)賬成功,但朋友并沒有收到錢。我以為是需要一定時(shí)間,結(jié)果24小時(shí)之后還沒有收到。我自己重新比對(duì)轉(zhuǎn)賬單,才發(fā)現(xiàn)是把對(duì)方的開戶銀行寫錯(cuò)了。因此可見,轉(zhuǎn)賬這個(gè)操作肯定不是強(qiáng)一致性,具體怎么搞的在網(wǎng)上也沒有查到。更坑爹的是,轉(zhuǎn)賬失敗,我的錢被扣了,朋友也沒有收到錢,但是我沒有收到任何消息,也沒有給我把錢退回來(lái),在我打電話到銀行去咨詢之后才退回來(lái)。這個(gè)體驗(yàn)真的很差,但銀行是大爺,沒辦法! References
|
|