關於分佈式事務的一些思考

在咱們剛開始接觸學習事務的時候,一般會經過本地事務的例子來理解,但在實際生產場景中,咱們面對的更可能是分佈式事務!咱們先來簡單回顧一下本地事務!node

 

一.本地事務

談到本地事務,你們可能都很熟悉,由於這個數據庫引擎層面能支持的!因此也稱數據庫事務,數據庫事務四大特徵:git

  •   原子性(A)
  •   一致性(C)
  •   隔離性(I)
  •   持久性(D)

而在這四大特性中,我認爲一致性是最基本的特性,其它的三個特性都爲了保證一致性而存在的!github

回到學生時代老師給咱們舉的經典栗子,A帳戶給B帳戶轉帳100元(A、B處於同一個庫中),若是A的帳戶發生扣款,B的帳戶卻沒有到帳,這就出現了數據的不一致!爲了保證數據的一致性,數據庫的事務機制會讓A帳戶扣款和B在帳戶到帳的兩個操做要麼同時成功,若是有一個操做失敗,則多個操做同時回滾,這就是事務的原子性,爲了保證事務操做的原子性,就必須實現基於日誌的REDO/UNDO機制!可是,僅有原子性還不夠,由於咱們的系統是運行在多線程環境下,若是多個事務並行,即便保證了每個事務的原子性,仍然會出現數據不一致的狀況。例如A帳戶原來有200元的餘額, A帳戶給B帳戶轉帳100元,先讀取A帳戶的餘額,而後在這個值上減去100元,可是在這兩個操做之間,A帳戶又給C帳戶轉帳100元,那麼最後的結果應該是A減去了200元。但事實上,A帳戶給B帳戶最終完成轉帳後,A帳戶只減掉了100元,由於A帳戶向C帳戶轉帳減掉的100元被覆蓋了!因此爲了保證併發狀況下的一致性,又引入的隔離性,即多個事務併發執行後的狀態,和它們串行執行後的狀態是等價的!隔離性又有多種隔離級別,爲了實現隔離性(最終都是爲了保證一致性)數據庫又引入了悲觀鎖、樂觀鎖等等……本文的主題是分佈式事務,因此本地事務就只是簡單回顧一下,須要記住的一點是,事務是爲了保證數據的一致性數據庫

 

二.分佈式理論

還記得剛畢業那年,領導給個人一個任務就是在列表上增長一個修改數據的功能。這能難倒我?我分分鐘給你搞出來!不就是在列表上增長了一個「修改」按鈕,點擊按鈕彈出框修改後保存就行了麼。然而一切不像我想象的那麼順利,點擊保存並刷新列表後,頁面上的數據仍是顯示的修改以前的內容,像沒有修改爲功同樣!過一下子再刷新列表,數據就能正常顯示了!測試屢次以後都是這樣!沒見過什麼大場面的我開始有點慌了,是我哪裏寫得不對麼?最終,我不得不求助組內經驗比較豐富的前輩!他深吸了一口氣告訴我說:「畢竟是剛畢業的小夥子啊!我來跟你講講緣由吧!咱們的數據庫是作了讀寫分離的,部分讀庫與寫庫在不一樣的網絡分區。你的數據更新到了寫庫,而讀數據的時候是從讀庫讀取的。更新到寫庫的數據同步到讀庫是有必定的延遲的,也就是說讀庫與寫庫會有短暫的數據不一致」! 「這樣不會體驗很差麼?爲何不能作到寫入的數據立馬能讀出來?那我這個功能該怎麼實現呢?」 面對個人一堆問題,同事有些不耐煩的說:「據說過CAP理論嗎?你先本身去了解一下吧」!是我開始查閱各類資料去了解這個陌生的詞背後的祕密!編程

CAP理論是由加州大學Eric Brewer教授提出來的,這個理論告訴咱們,一個分佈式系統不可能同時知足一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance)這三個基本需求,最多隻能同時知足其中兩項。
  一致性:這裏的一致性是指數據的強一致,也稱爲線性一致性。是指在分佈式環境中,數據在多個副本之間是否可以保持一致的特性。也就是說對某個數據進行寫操做後立馬執行讀操做,必須能讀取到剛剛寫入的值。(any read operation that begins after a write operation completes must return that value, or the result of a later write operation)
  可用性:任意被無端障節點接收到的請求,必須可以在有限的時間內響應結果。(every request received by a non-failing node in the system must result in a response)
  分區容錯性:若是集羣中的機器被分紅了兩部分,這兩部分不能互相通訊,系統是否能繼續正常工做。(the network will be allowed to lose arbitrarily many messages sent from one node to another)網絡

在分佈式系統中,分區容錯性是基本要保證的。也就是說只能在一致性和可用性之間進行取捨。一致性和可用性,爲何不可能同時成立?回到以前修改列表的例子,因爲數據會分佈在不一樣的網絡分區,必然會存在數據同步的問題,而同步會存在網絡延遲、異常等問題,因此會出現數據的不一致!若是要保證數據的一致性,那麼就必須在對寫庫進行操做時,鎖定其餘讀庫的操做。只有寫入成功且完成數據同步後,才能從新放開讀寫,而這樣在鎖按期間,系統喪失了可用性。更詳細關於CAP理論能夠參考這篇文章,該文章講得比較通俗易懂!多線程

 

三.分佈式事務

分佈式事務就是在分佈式的場景下,須要知足事務的需求!上篇文章咱們聊過了消息中間件,那這篇文章咱們要聊的是分佈式事務,把二者一結合,便有了基於消息中間件的分佈式事務解決方案!無論是本地事務,仍是分佈式事務,都是爲了解決數據的一致性問題!一致性這個詞我們前面屢次說起!與本地事務不一樣的是,分佈式事務須要保證的是分佈式環境下,不一樣數據庫表中的數據的一致性問題。分佈式事務的解決方案有多種,如XA協議、TCC三階段提交、基於消息隊列等等,本文只會涉及基於消息隊列的解決方案!架構

   本地事務講到了一致性,分佈式事務不可避免的面臨着一致性的問題!回到最開始跨行轉帳的例子,若是A銀行用戶向B銀行用戶轉帳,正常流程應該是:併發

一、A銀行對轉出帳戶執行檢查校驗,進行金額扣減。
二、A銀行同步調用B銀行轉帳接口。
三、B銀行對轉入帳戶進行檢查校驗,進行金額增長。
四、B銀行返回處理結果給A銀行。app

   

   在正常狀況對一致性要求不高的場景,這樣的設計是能夠知足需求的。可是像銀行這樣的系統,若是這樣實現大概早就破產了吧。咱們先看看這樣的設計最主要的問題:

一、同步調用遠程接口,若是接口比較耗時,會致使主線程阻塞時間較長。
二、流量不能很好控制,A銀行系統的流量高峯可能壓垮B銀行系統(固然B銀行確定會有本身的限流機制)。
三、若是「第1步」剛執行完,系統因爲某種緣由宕機了,那會致使A銀行帳戶扣款了,可是B銀行沒有收到接口的調用,這就出現了兩個系統數據的不一致。
四、若是在執行「第3步」後,B銀行因爲某種緣由宕機了而沒法正確迴應請求(實際上轉帳操做在B銀行系統已經執行且入庫),這時候A銀行等待接口響應會異常,誤覺得轉帳失敗而回滾「第1步」操做,這也會出現了兩個系統數據的不一致。

   對於問題的一、2都很好解決,若是對消息隊列熟悉的朋友應該很快能想到能夠引入消息中間件進行異步和削峯處理,因而又從新設計了一個方案,流程以下:

一、A銀行對帳戶進行檢查校驗,進行金額扣減。
二、將對B銀行的請求異步寫入隊列,主線程返回。
三、啓動後臺程序從隊列獲取待處理數據。
四、後臺程序對B銀行接口進行遠程調用。
五、B銀行對轉入帳戶進行檢查校驗,進行金額增長。
六、B銀行處理完成回調A銀行接口通知處理結果。

   

   經過上面的圖咱們能看到,引入消息隊列後,系統的複雜性瞬間提高了,雖然彌補了咱們第一種方案的幾個不足點,但也帶來了更多的問題,好比消息隊列系統自己的可用性、消息隊列的延遲等等!而且,這樣的設計依然沒有解決咱們面臨的核心問題-數據的一致性

一、若是「第1步」剛執行完,系統因爲某種緣由宕機了,那會致使A銀行帳戶扣款了,可是寫入消息隊列失敗,沒法進行B銀行接口調用,從而致使數據不一致。
二、若是B銀行在執行「第5步」時因爲校驗失敗而未能成功轉帳,在回調A銀行接口通知回滾時網絡異常或者宕機,會致使A銀行轉帳沒法完成回滾,從而致使數據不一致。

   面對上述問題,咱們不得不對系統再次進行升級改造。爲了解決「A銀行帳戶扣款了,可是寫入消息隊列失敗」的問題,咱們須要藉助一個轉帳日誌表,或者叫轉帳流水錶,該表簡單的設計以下:

字段名稱 字段描述
tId 交易流水id
accountNo 轉出帳戶卡號
targetBankNo 目標銀行編碼
targetAccountNo 目標銀行卡號
amount 交易金額
status 交易狀態(待處理、處理成功、處理失敗)
lastUpdateTime 最後更新時間

   這個流水錶須要怎麼用呢?咱們在「第1步」進行扣款時,同時往流水錶寫入一條操做流水,狀態爲「待處理」,而且這兩個操做必須是原子的,也就是說必須通過本地事務保證這兩個操做要麼同時成功,要麼同時失敗!這就保證了只要轉帳扣款成功,一定會記錄一條狀態爲「待處理」的轉帳流水。若是在這一步失敗了,那天然就是轉帳失敗,沒有後續操做了。若是這步操做後系統宕機了致使沒有將消息成功寫入消息隊列(也就是「第2步」)也不要緊,由於咱們的流水數據已經持久化了!這時候咱們只須要加入一個後臺線程進行補償,按期的從轉帳流水錶中讀取狀態爲「待處理」且最後更新的時間距當前時間大於某個閾值的數據,從新放入消息隊列進行補償。這樣,就保證了消息即便丟失,也會有補償機制!B銀行在處理完轉帳請求後會回調A銀行的接口通知轉帳的狀態,從而更新A銀行流水錶中的狀態字段!這樣就完美解決了上一個方案中的兩個不足點。系統設計圖以下:
   

   到目前爲止,咱們很好的解決了消息丟失的問題,保證了只要A銀行轉帳操做成功,轉帳的請求就必定能發送到B銀行!可是該方案又引入了一個問題,經過後臺線程輪詢將消息放入消息隊列處理,同一次轉帳請求可能會出現屢次放入消息隊列而屢次消費的狀況,這樣B銀行會對同一轉帳屢次處理致使數據出現不一致!那怎麼保證B銀行轉帳接口的冪等性呢?

   一樣的,咱們能夠在B銀行系統中須要增長一個轉帳日誌表,或者叫轉帳流水錶,B銀行每次接收到轉帳請求,在對帳戶進行操做的時候同時往轉帳日誌表中插入一條轉帳日誌記錄,一樣這兩個操做也必須是原子的!在接收到轉帳請求後,首先根據惟一轉帳流水Id在日誌表中查找判斷該轉帳是否已經處理過,若是未處理過則進行處理,不然直接回調返回! 最終的架構圖以下:
   

   因此,咱們這裏最核心的就是A銀行經過本地事務保證日誌記錄+後臺線程輪詢保證消息不丟失。B銀行經過本地事務保證日誌記錄從而保證消息不重複消費!B銀行在回調A銀行的接口時會通知處理結果,若是轉帳失敗,A銀行會根據處理結果進行回滾。

 

四.總結

分佈式場景,要用分佈式的思惟去思考問題。要考慮任何的超時,斷電,維護不一樣物理存儲的數據的可能存在的狀態不一致的場景。面向失敗編程。通常來講,業務規則只能經過數據庫惟一索引、版本號樂觀併發控制、分佈式鎖,等這些靠譜的方式去實現。永遠不要認爲請求不會重複發送,消息不會重複消費,請求不會超時,數據必定一致,內存中的if判斷必定有效,這種錯誤的想法。

相關文章
相關標籤/搜索