1. 程式人生 > >從銀行微信約戰棋牌源碼下載轉賬失敗到分布式事務:總結與思考

從銀行微信約戰棋牌源碼下載轉賬失敗到分布式事務:總結與思考

網上 不執行 分布式存 sys 優缺點 用戶 分別是 擇業 col

思考這微信約戰棋牌源碼下載( h5.super-mans.com Q:2012035031)微信約戰棋牌源碼下載個問題的初衷,是有一次給朋友轉賬,結果我的錢被扣了,朋友沒收到錢。而我之前一直認為銀行轉賬一定是由事務保證強一致性的,於是學習、總結了一下分布式事務的各種理論、方法。

  事務是一個非常廣義的詞匯,各行各業解讀都不一樣。對於程序員,事務等價於Transaction,是指一組連續的操作,這些操作組合成一個邏輯的、完整的操作。即這組操作執行前後,系統需要處於一個可預知的、一致的狀態。因此,這一組操作要麽都成功執行,要麽都不能執行;如果部分成功,部分失敗,成功的部分需要回滾(rollback)。

  姊妹篇:再論分布式事務:從理論到實踐

  本文地址:http://www.cnblogs.com/xybaby/p/7465816.html

關系型數據庫事務
回到頂部
  大多數人可能和我一樣,第一次聽說事務是在學習關系型數據庫(mysql、sql server、Oracle)的時候,在關系型數據庫中,如果一組操作滿足ACID特性,那麽稱之為一個事務。關於關系型數據庫的ACID特性,不管是教材還是網絡上都有大量的資料,這裏只簡單介紹。

  A(Atomic):原子性,構成事務的所有操作,要麽都執行完成,要麽全部不執行,不可能出現部分成功部分失敗的情況
  C(Consistency):一致性,在事務執行前後,數據庫的一致性約束沒有被破壞。這裏的一致性含義後面會詳細解釋

  I(Isolation):隔離性,數據庫中的事務一般都是並發的,隔離性是指並發的兩個事務的執行互不幹擾,一個事務不能看到其他事務運行過程的中間狀態
  D(Durability):持久性,事務完成之後,該事務對數據的更改會被持久化到數據庫,且不會被回滾。

  我們舉一個簡單的轉賬的例子,用戶A給玩家B轉100塊錢,那麽涉及到兩個操作:玩家A的賬戶扣100元,玩家B的賬戶加100元。即

UserA.account -= 100
UserB.account += 100

  原子性很好理解,這兩個操作要麽都成功,要麽都不執行(更準確的是從效果上來看等價於都沒有執行)。不可能出現用戶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
  提到分布式事務,首先想到的肯定是兩階段提交(2pc, two-phase commit protocol),2pc是非常經典的強一致性、中心化的原子提交協議。中心化是指協議中有兩類節點:一個中心化協調者節點(coordinator)和N個參與者節點(participant、cohort)。

  顧名思義,兩階段提交協議的每一次事務提交分為兩個階段:

  在第一階段,協調者詢問所有的參與者是否可以提交事務(請參與者投票),所有參與者向協調者投票。

  在第二階段,協調者根據所有參與者的投票結果做出是否事務可以全局提交的決定,並通知所有的參與者執行該決定。在一個兩階段提交流程中,參與者不能改變自己的投票結果。兩階段提交協議的可以全局提交的前提是所有的參與者都同意提交事務,只要有一個參與者投票選擇放棄(abort)事務,則事務必須被放棄。

  wiki上給出了簡要流程:

  註意,上圖中最下面一行也表明,兩階段提交協議也依賴與日誌,只要存儲介質不出問題,兩階段協議就能最終達到一致的狀態(成功或者回滾)

  而下圖(來自slideshare)詳細描述了整個流程:

  在劉傑的《分布式原理介紹中》,有非常詳細的流程介紹,可以配合上圖一起看,另外還介紹了在各種異常情況下(比如Coordinator、Participant宕機,網絡分割導致的超時)兩階段協議的工作情況、工作效率。另外,在這篇文章中也有比較清晰的流程介紹。在這裏只討論2PC的優缺點:

  優點:強一致性,只要節點或者網絡最終恢復正常,協議就能保證順利結束;部分關系型數據庫(Oracle)、框架直接支持

  缺點:兩階段提交協議的容錯能力較差,比如在節點宕機或者超時的情況下,無法確定流程的狀態,只能不斷重試;兩階段提交協議的性能較差, 消息交互多,且受最慢節點影響

  這篇文章描述了為什麽兩階段提交協議在分布式系統中不適用:

  系統“水平”伸縮的死敵。基於兩階段提交的分布式事務在提交事務時需要在多個節點之間進行協調,最大限度地推後了提交事務的時間點,客觀上延長了事務的執行時間,這會導致事務在訪問共享資源時發生沖突和死鎖的概率增高,隨著數據庫節點的增多,這種趨勢會越來越嚴重,從而成為系統在數據庫層面上水平伸縮的"枷鎖", 這是很多Sharding系統不采用分布式事務的主要原因。

  所言甚是!

3PC
  三階段提交協議(3pc Three-phase_commit_protocol)主要是為了解決兩階段提交協議的阻塞問題,從原來的兩個階段擴展為三個階段,並且增加了超時機制。

  

  3PC只是解決了在異常情況下2PC的阻塞問題,但導致一次提交要傳遞6條消息,延時很大。具體流程描述可參見《關於分布式事務、兩階段提交協議、三階提交協議 》一文。

TCC
  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
  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節點的數據也不會回滾。在《帶著問題學習分微信約戰棋牌源碼下載( h5.super-mans.com Q:2012035031)微信約戰棋牌源碼下載布式系統之中心化復制集》中也提到過,為了提高數據可靠性(避免極端情況下數據被回滾),設定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...。當涉及到跨進程、異步通信的時候,就很難通過語言層面的機制保證原子性了。

  在分布式領域,由於網絡或者機器故障,經常需要重試,因此冪等性非常重要

  很多場景,微信約戰棋牌源碼下載( h5.super-mans.com Q:2012035031)微信約戰棋牌源碼下載比如電商、網絡購票,首先要保證的是高可用,不大可能采用強一致性,因此我們也會看到‘正在處理中...‘這種中間狀態,後臺很可能是異步處理的,在12306買過票的話都知道,下單成功到最後是否能出票由很長一段時間。

  在筆者的業務領域,並沒有涉及到強一致性的場景,只要最終一致性就行了。上面的提到的各種辦法,不管是2PC、TCC、本地消息表、事務消息,都需要引入額外的框架或者組件。所以更多的時候是采取業務補償的方式,比如一個涉及兩個進程的操作需要保證原子性,進程間RPC通信,那麽一般是A進程先執行,然後RPC調用B進程接口,根據B進程的返回結果,絕對是否回滾(補償);但如果涉及到異步RPC、或者多線程、或者兩個以上進程的串聯時,那麽就不一定能補償、甚至很難補償了,這個時候只記錄一個error log,然後通知人工排查。因此,事務補償只適合業務比較簡單的常見,而且很難形成通用的框架,或者說實用性不強。

  之前一直以為像銀行轉賬這種場景,一定是強一致性的。後來自己遇到這麽一回事,我給朋友轉賬,我這邊顯示轉賬成功,但朋友並沒有收到錢。我以為是需要一定時間,結果24小時之後還沒有收到。我自己重新比對轉賬單,才發現是把對方的開戶銀行寫錯了。因此可見,轉賬這個操作肯定不是強一致性,具體怎麽搞的在網上也沒有查到。更坑爹的是,轉賬失敗,我的錢被扣了,朋友也沒有收到錢,但是我沒有收到任何消息,也沒有給我把錢退回來,在我打電話到銀行去咨詢之後才退回來。這個體驗真的很差,但銀行是大爺,沒辦法!

從銀行微信約戰棋牌源碼下載轉賬失敗到分布式事務:總結與思考