分佈式事務一致性解決方案

1、從數據一致性談起

一致性問題,「萬惡之源」是數據冗餘和分佈並經過網絡交互+網絡異常是常態。java

一、數據一致性的情形

  • 主庫、從庫和緩存數據一致性,相同數據冗餘,關係數據庫,爲保證關據庫的高可用和高性能,通常會採用主從(備)架構並引入緩存。其中數據不一致性存在於數據冗餘的時間窗口內。
  • 多副本數據之間的數據一致性,相同數據副本,大數據領域,一份數據會有多個副本並存儲到不一樣的節點上。客戶端能夠訪問任何一個節點進行讀寫操做。經常使用的解決方案是基於Paxos、ZAB、Raft、Quorum、Gossip等的開源實現。這裏只是一提,暫不探討。感興趣能夠自行谷歌或百度。
  • 分佈式服務之間的數據一致性,相關數據分佈,分佈式服務,不一樣的服務操做不一樣的庫(表),並且庫(表)間要保持一致。

二、數據一致性的概念

  • 強一致性
  • 弱一致性
  • 最終一致性

三、數據一致性的原理

  • ACID
  • CAP
  • BASE

四、數據一致性的協議

  • 兩階段提交協議
  • 三階段提交協議
  • TCC協議
  • Paxos協議
  • ZAB協議
  • Raft協議
  • Quorum協議
  • Gossip協議

2、分佈式服務間的數據一致性

所謂分佈式服務,就是把以前經過本地接口交互的模塊,拆分紅單獨的應用獨立部署,並經過遠程接口和網絡消息交互。且無論這樣說嚴不嚴密,正不正確,理解就好。本文的重點也不是這個話題。簡單畫一張圖輔助理解,如圖。集中式架構,要想保證訂單表和庫存表的一致性,只要一個本地事務(ACID)就能保證二者的強一致性。分佈式架構,訂單表由訂單服務操做,庫存表由庫存服務操做。要想保證訂單表和庫存表的一致性,那麼就必須保證訂單服務對訂單表的操做和庫存服務對庫存表的操做同事成功。以前的一個本地事務就變成了一個分佈式事務。因爲服務之間經過網絡交互+網絡異常是常態,就會產生服務間數據不一致的狀況。這就涉及一個分佈式事務一致性的問題。數據庫

3、分佈式事務一致性解決方案

一、接口同步調用模式與一致性解決方案

模式分析:A服務同步調用B服務的接口並等待結果返回,後續的流程會依賴B服務的返回結果。這種交互模式下,A服務獲得的結果細分有三種狀況。apache

  1. 請求發起階段網絡超時或異常,此時,B服務未收到請求,未做出相應的處理;
  2. 結果返回階段網絡超時或異常,此時,B服務已收到請求,並做出相應的處理;
  3. 正常結果返回(明確的成功或失敗)。

業務場景:適用於大規模、高併發的短小操做且依賴返回值的場景。例如,交易服務和庫存服務(卡券服務、紅包服務等)的交互、用戶登陸和准入服務的交互等。緩存

解決方案:方案一,服務調用方查詢重試方案;方案二,TCC方案。服務器

:這兩種方案,保證數據一致性實際上仍是靠「異步」,只不過須要快速校準,準實時。markdown

服務調用方查詢重試方案,適合一個從業務服務場景。

下單減庫存方法() {
    // 1.準備操做
    // 2.重試調用B服務
    result = RetryUtil {
        while(重試次數 < 最大重試次數) {
            try {
                if (重試次數 != 0) {
                    // case1:網絡超時或異常(catch分支)
                    // case2:查詢到扣減庫存操做,result=成功(return)
                    // case3:查不到扣減庫存操做,result=失敗(繼續下面操做)
                    result = rpc.查詢扣減庫存是否成功();
                    if (result == 成功) {
                        return result;
                    }
                }
                // case1:網絡超時或異常(catch分支)
                // case2:扣減庫存成功,result=成功(return)
                // case3:扣減庫存失敗,result=失敗(return)
                return rpc.扣減庫存();
            } catch (Exception e) {
                if (重試次數 = 最大重試次數) {
                    // 報警,人工處理或者(近實時)對帳系統自動校準
                    // 拋出異常,中斷後續流程
                    throw 自定義異常; //或者result封裝異常
                }
            }
        }
    };
    // 3.後續操做
}複製代碼

:1) 查詢重試後依然失敗(極少),報警,人工處理或者準實時對帳系統自動校準;網絡

  2) 重試次數不宜多,甚至只重試一次;架構

  3) B服務處理請求要作冪等。併發

  1. TCC方案,適合多個從業務服務場景。TCC是阿里在二階段提交協議的基礎上提出的一種解決分佈式事務一致性的協議,原理圖以下。其對應的產品是DTX(老版是DTS)。DTS中有個快速開始的例子看明白了,TCC就基本OK了。在螞蟻金服內部被普遍地應用於交易、轉帳、紅包等核心資金鍊路,服務於億級用戶的資金操做。
    :1) 關於TCC,我的認爲,理解原理很重要。工做中遇到吻合的場景能夠根據原理自行實現,知足業務便可;
      2) 一個開源實現:tcc-transaction

二、接口異步調用模式與一致性解決方案

模式分析:A服務調用B服務,B服務先受理請求並落庫,狀態是待處理。B服務處理請求很耗時,或者要依賴其餘的服務。B服務處理完後通知A服務或者A服務定時去查詢B服務的處理結果。這種交互模式下,對於CASE-1,第1步和第2步同接口同步調用模式,第3步同消息異步處理模式;對於CASE-2,至關於兩次接口同步調用模式
異步

業務場景:適用於非核心鏈路上負載較高的處理環節,這個環節常常耗時較長,而且對時效性要求不高。例如,用戶提現時,帳戶系統和提現系統的交互(CASE-1);提現系統和三方系統(銀行系統或者三方託管系統)的交互(CASE-2)。

解決方案服務被調方最大努力處理方案。因爲B服務中請求有落庫,因此能夠用定時任務不斷重試盡最大努力將請求處理出結果。處理後,將請求狀態設置成對應的結果落庫。而後再通知A服務或者A服務異步主動查詢。

受理請求方法() {
    // 1.請求落庫,狀態爲待處理
    // 2.返回受理結果
    if (落庫成功) {
        // 返回受理成功
    } else {
        // 返回受理失敗
    }
}

定時任務處理請求方法() {
    // 1.掃描待處理請求
    try {
        // 2.處理請求
        if (處理成功) {
            // 設置請求處理狀態爲處理成功
        } eles {
            // 設置請求處理狀態爲處理失敗
        }
    } catch (Exception e) {
        // 不作任何操做,請求狀態依舊爲待處理
    }
    // 3.消息通知A服務處理結果或者等待A服務查詢處理結果
}複製代碼

:1) B服務一般都是接受請求並持久化後才返回A服務受理成功。避免服務進程被殺掉而致使請求丟失。

  2) 不論是第(1,2)兩步仍是CASE-2中的第(3,4)兩步,若是查詢重試失敗,能夠落庫,用定時任務處理,知道成功。反正不像接口同步調用模式,A服務不須要實時的結果。

三、消息異步處理模式與一致性解決方案

模式分析:A服務將B服務須要的信息經過消息中間件傳遞給B服務,A服務無需知道B服務的處理結果。這種交互模式下,消息生產者要確保消息發送成功;消息消費者要確保消息消費成功。

業務場景:消息異步處理模式與接口異步調用模式相似,多應用於非核心鏈路上負載較高的處理環節中,井且服務的上游不關心下游的處理結果,下游也不須要向上遊返回處理結果。例如,在電商系統中,用戶下訂單支付且交易成功後,發送消息給物流系統或者帳務系統進行後續的處理。

解決方案生產者最大努力通知+消費者最大努力處理方案。

  1. 非事務消息,生產者先執行本地事務並將消息落庫,狀態標記爲待發送,而後發送消息。若是發送成功,則將消息改成發送成功。定時任務定時從數據庫撈取在必定時間內待發送的消息並將消息發送。經過定時任務來保證消息的發送。爲確保消息必定能消費,消費者通常採用手動ACK機制,那麼消息服務器必然會重發未ACK的消息,這就要求消息消費者作好冪等。

    交易完成發消息方法() {
        // 1.設置交易狀態爲已完成
        // 2.消息落庫,狀態爲待發送
        // 可異步發送,也建議異步發送
        try {
            // 3.發送消息
            if (發送成功) {
                // 設置消息狀態爲發送成功
            }
        } catch (Exception e) {
            // 不作任何操做,消息狀態依舊爲待發送
        }
    }
    
    定時任務發送消息方法() {
        // 1.掃描待發送消息
        try {
            // 2.發送消息
            if (發送成功) {
                // 設置消息狀態爲發送成功
            }
        } catch (Exception e) {
            // 不作任何操做,消息狀態依舊爲待發送
        }
    }複製代碼

  2. 事務消息,以RocketMQ爲例,下圖是RocketMQ事務消息的流程。官網有示例代碼。和不支持事務的消息中間相比,只是消息發送的時候,保證了和本地事務的一致。消費者實現仍是不變。

:1) 定時任務重試發送消息和消息服務器重發未ACK的消息通常都是時間階梯式的(2n*時間間隔);

  2) 支持事務消息中間件之RocketMQ

4、保證操做冪等性的經常使用方法

  1. 有業務狀態,業務邏輯來保證冪等。好比接到支付成功的消息訂單狀態變成支付完成,若是當前狀態是支付完成,則再收到一個支付成功或者支付成功以前狀態的消息則說明消息重複了,不用再次處理。
  2. 無業務狀態,業務惟一ID保證冪等。增長一個去重表(或分佈式緩存)來記錄有業務惟一ID的操做。好比調用充值接口,當請求過來時,會根據惟一充值ID去查充值流水錶,若已經存在,則直接返回;不然繼續進行充值操做。

:保證冪等性的方法不少,根據具體的業務場景,總能找到保證冪等性的方法。

5、總結

  1. 接口同步調用模式,服務調用方查詢重試方案和TCC方案。
  2. 接口異步調用模式,服務被調方最大努力處理方案。
  3. 消息異步處理模式,生產者最大努力通知+消費者最大努力處理方案。
  4. 任何服務操做都須要提供一個查詢接口,用來向外部輸出操做執行的狀態。
  5. 永遠不要在本地事務中調用遠程服務,在這種場景下若是遠程服務出現了問題,則會拖長事務,致使應用服務器佔用太多的數據庫鏈接,讓服務器負載迅速攀升,在嚴重狀況下會壓垮數據庫。
  6. 最後一道防線 - 對帳系統。
  7. 同步和異步的抉擇:
  • 能夠異步的地方,就應該異步實現。若是業務邏輯容許,則咱們能夠將一些耗時較長的、用戶對響應沒有特別要求的操做異步化,以此來減小核心鏈路的層級,釋放系統的壓力。
  • 能用同步解決的問題,不要引入異步。若是性能不是問題,或者所處理的操做是短小的輕量級處理邏輯,那麼同步調用方式是最理想不過的,由於這樣不須要引入異步化的複雜處理流程。
相關文章
相關標籤/搜索