支付重構
考慮支付重構的時候,天然想到本來屬於一個本地事務中的處理,如今要跨應用了要怎麼處理。拿充值訂單舉個栗子吧,假設:本來訂單模塊和帳戶模塊是放在一塊兒的,如今須要作服務拆分,拆分紅訂單服務,帳戶服務。本來收到充值回調後,能夠將修改訂單狀態和增長金幣放在一個mysql事務中完成的,可是呢,由於服務拆分了,就面臨着須要協調2個服務才能完成這個事務mysql
因此就帶出來,咱們今天要分享和討論的話題是:怎麼解決分佈式場景下數據一致性問題,暫且用分佈式事務來定義吧。redis
一樣的問題還存在於其餘的場景:sql
送禮:數據庫
調用支付服務:先扣送禮用戶的金幣,而後給主播加相應的荔枝編程
確認第一步成功後,播放特效,發聊天室送禮評論等複製代碼緩存
充值成功消息:服務器
完成充值訂單微信
發送訂單完成的kafka消息網絡
在涉及支付交易等付費接口的時候,數據一致性的問題就顯得尤其重要,由於都是錢啊數據結構
目前分佈式事務是怎麼解決的呢?
問題確定不是新問題,也就是目前已經有相應的解決方案了,那就看一下如今是怎麼來解決這類問題的吧。
以購買基礎商品成功後發送支付訂單完成消息爲例:
假設支付下單購買基礎商品,此刻已經收到支付回調,訂單已經處理成功了,這個時候kafka服務故障,消息發送失敗;而這個時候處理訂單的事務已經提交了,怎麼保證訂單完成的消息必定能發出去呢?
解讀一下這個流程:
綠色部分,表示流程正常運行的交互過程:
先往JobController中提交一個job(用於故障恢復)
提交成功後,開始處理訂單邏輯
處理完訂單邏輯以後,開始發送kafka消息
消息也發送成功後,刪除第一步提交的job
黃色部分,表示流程出現了異常,數據可能存在不一致現象。這個時候就須要進行流程恢復
JobController任務控制器定時去redis查詢延時任務列表(每一個任務都有一個時間戳,按時間戳排序過濾)
將任務進行恢復(調用job註冊時定義的處理方法)
任務執行成功,表示流程完成;不然下一個定時週期重試
問題:
基於redis存儲恢復任務,可能存在數據丟失風險
架構體系中沒有統一的分佈式事務規範,能否將這層邏輯獨立爲分佈式事務中間件
缺乏事務執行策略管理,如:控制最大重試次數等
事務執行狀態沒有記錄,追查須要去翻看日誌
行業中有什麼解決方案
說解決方案以前,咱們先了解一下這些方案的理論依據,有助於幫助咱們來理解和實踐這些方案
理論依據(討論的前提)
本地事務、分佈式事務
若是說本地事務是解決單個數據源上的數據操做的一致性問題的話,那麼分佈式事務則是爲了解決跨越多個數據源上數據操做的一致性問題。
強一致性、弱一致性、最終一致性
從客戶端角度,多進程併發訪問時,更新過的數據在不一樣進程如何獲取的不一樣策略,決定了不一樣的一致性。對於關係型數據庫,要求更新過的數據能被後續的訪問都能看到,這是強一致性。若是能容忍後續的部分或者所有訪問不到,則是弱一致性。若是通過一段時間後要求能訪問到更新後的數據,則是最終一致性
從服務端角度,如何儘快將更新後的數據分佈到整個系統,下降達到最終一致性的時間窗口,是提升系統的可用度和用戶體驗很是重要的方面。對於分佈式數據系統:
N — 數據複製的份數
W — 更新數據時須要保證寫完成的節點數
R — 讀取數據的時候須要讀取的節點數
若是W+R>N,寫的節點和讀的節點重疊,則是強一致性。例如對於典型的一主一備同步複製的關係型數據庫,N=2,W=2,R=1,則無論讀的是主庫仍是備庫的數據,都是一致的。
若是W+R<=N,則是弱一致性。例如對於一主一備異步複製的關係型數據庫,N=2,W=1,R=1,則若是讀的是備庫,就可能沒法讀取主庫已經更新過的數據,因此是弱一致性。
CAP理論
分佈式環境下(數據分佈)要任什麼時候刻保證數據一致性是不可能的,只能採起妥協的方案來保證數據最終一致性。這個也就是著名的CAP定理。
須要明確的一點是,對於一個分佈式系統而言,分區容錯性是一個最基本的要求。由於 既然是一個分佈式系統,那麼分佈式系統中的組件必然須要被部署到不一樣的節點,不然也就無所謂分佈式系統了,所以必然出現子網絡。而對於分佈式系統而言,網 絡問題又是一個一定會出現的異常狀況,所以分區容錯性也就成爲了一個分佈式系統必然須要面對和解決的問題。所以系統架構師每每須要把精力花在如何根據業務 特色在C(一致性)和A(可用性)之間尋求平衡。
BASE 理論
BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)三個短語的縮寫。BASE理論是對CAP中一致性和可用性權衡的結果,其來源於對大規模互聯網系統分佈式實踐的總結, 是基於CAP定理逐步演化而來的。BASE理論的核心思想是:即便沒法作到強一致性,但每一個應用均可以根據自身業務特色,採用適當的方式來使系統達到最終一致性。
BASE理論面向的是大型高可用可擴展的分佈式系統,和傳統的事物ACID特性是相反的,它徹底不一樣於ACID的強一致性模型,而是經過犧牲強一致性來得到可用性,並容許數據在一段時間內是不一致的,但最終達到一致狀態。但同時,在實際的分佈式場景中,不一樣業務單元和組件對數據一致性的要求是不一樣的,所以在具體的分佈式系統架構設計過程當中,ACID特性和BASE理論每每又會結合在一塊兒。
柔性事務
不一樣於ACID的剛性事務,在分佈式場景下基於BASE理論,就出現了柔性事務的概念。要想經過柔性事務來達到最終的一致性,就須要依賴於一些特性,這些特性在具體的方案中不必定都要知足,由於不一樣的方案要求不同;可是都不知足的話,是不可能作柔性事務的。
可見性(對外可查詢)
在分佈式事務執行過程當中,若是某一個步驟執行出錯,就須要明確的知道其餘幾個操做的處理狀況,這就須要其餘的服務都可以提供查詢接口,保證能夠經過查詢來判斷操做的處理狀況。
爲了保證操做的可查詢,須要對於每個服務的每一次調用都有一個全局惟一的標識,能夠是業務單據號(如訂單號)、也能夠是系統分配的操做流水號(如支付記錄流水號)。除此以外,操做的時間信息也要有完整的記錄。
冪等性,實際上是一個數學概念。冪等函數,或冪等方法,是指可使用相同參數重複執行,並能得到相同結果的函數。
f(f(x)) = f(x)
在編程中一個冪等操做的特色是其任意屢次執行所產生的影響均與一次執行的影響相同。也就是說,同一個方法,使用一樣的參數,調用屢次產生的業務結果與調用一次產生的業務結果相同。這一個要求其實也比較好理解,由於要保證數據的最終一致性,不少解決防範都會有不少重試的操做,若是一個方法不保證冪等,那麼將沒法被重試。冪等操做的實現方式有多種,如在系統中緩存全部的請求與處理結果、檢測到重複操做後,直接返回上一次的處理結果等。
業界方案
兩階段提交(2PC)
XA是X/Open CAE Specification (Distributed Transaction Processing)模型中定義的TM(Transaction Manager)與RM(Resource Manager)之間進行通訊的接口。
在XA規範中,數據庫充當RM角色,應用須要充當TM的角色,即生成全局的txId,調用XAResource接口,把多個本地事務協調爲全局統一的分佈式事務。
二階段提交是XA的標準實現。它將分佈式事務的提交拆分爲2個階段:prepare和commit/rollback。
2PC模型中,在prepare階段須要等待全部參與子事務的反饋,所以可能形成數據庫資源鎖定時間過長,不適合併發高以及子事務生命周長較長的業務場景。兩階段提交這種解決方案屬於犧牲了一部分可用性來換取的一致性。
saga
saga的提出,最先是爲了解決可能會長時間運行的分佈式事務(long-running process)的問題。所謂long-running的分佈式事務,是指那些企業業務流程,須要跨應用、跨企業來完成某個事務,甚至在事務流程中還須要有手工操做的參與,這類事務的完成時間可能以分計,以小時計,甚至可能以天計。這類事務若是按照事務的ACID的要求去設計,勢必形成系統的可用性大大的下降。試想一個由兩臺服務器一塊兒參與的事務,服務器A發起事務,服務器B參與事務,B的事務須要人工參與,因此處理時間可能很長。若是按照ACID的原則,要保持事務的隔離性、一致性,服務器A中發起的事務中使用到的事務資源將會被鎖定,不容許其餘應用訪問到事務過程當中的中間結果,直到整個事務被提交或者回滾。這就形成事務A中的資源被長時間鎖定,系統的可用性將不可接受。
而saga,則是一種基於補償的消息驅動的用於解決long-running process的一種解決方案。目標是爲了在確保系統高可用的前提下儘可能確保數據的一致性。仍是上面的例子,若是用saga來實現,那就是這樣的流程:服務器A的事務先執行,若是執行順利,那麼事務A就先行提交;若是提交成功,那麼就開始執行事務B,若是事務B也執行順利,則事務B也提交,整個事務就算完成。可是若是事務B執行失敗,那事務B自己須要回滾,這時由於事務A已經提交,因此須要執行一個補償操做,將已經提交的事務A執行的操做做反操做,恢復到未執行前事務A的狀態。這樣的基於消息驅動的實現思路,就是saga。咱們能夠看出,saga是犧牲了數據的強一致性,僅僅實現了最終一致性,可是提升了系統總體的可用性。
補償事務(TCC)
TCC 其實就是採用的補償機制,其核心思想是:針對每一個操做,都要註冊一個與其對應的確認和補償(撤銷)操做。TCC模型是把鎖的粒度徹底交給業務處理。它分爲三個階段:
Try 階段主要是對業務系統作檢測及資源預留
Confirm 階段主要是對業務系統作確認提交,Try階段執行成功並開始執行 Confirm階段時,默認 Confirm階段是不會出錯的。即:只要Try成功,Confirm必定成功。
Cancel 階段主要是在業務執行錯誤,須要回滾的狀態下執行的業務取消,預留資源釋放。
下面對TCC模式下,A帳戶往B帳戶匯款100元爲例子,對業務的改造進行詳細的分析:
匯款服務和收款服務分別須要實現,Try-Confirm-Cancel接口,並在業務初始化階段將其注入到TCC事務管理器中。、
[匯款服務] Try: 檢查A帳戶有效性,即查看A帳戶的狀態是否爲「轉賬中」或者「凍結」; 檢查A帳戶餘額是否充足; 從A帳戶中扣減100元,並將狀態置爲「轉帳中」; 預留扣減資源,將從A往B帳戶轉帳100元這個事件存入消息或者日誌中; Confirm: 不作任何操做; Cancel: A帳戶增長100元; 從日誌或者消息中,釋放扣減資源。 [收款服務] Try: 檢查B帳戶帳戶是否有效; Confirm: 讀取日誌或者消息,B帳戶增長100元; 從日誌或者消息中,釋放扣減資源; Cancel: 不作任何操做。
由此能夠看出,TCC模型對業務的侵入強,改造的難度大。
本地消息表(異步確保)
本地消息表這種實現方式應該是業界使用最多的,其核心思想是將分佈式事務拆分紅本地事務進行處理,這種思路是來源於ebay。咱們能夠從下面的流程圖中看出其中的一些細節:
基本思路就是:
消息生產方,須要額外建一個消息表,並記錄消息發送狀態。消息表和業務數據要在一個事務裏提交,也就是說他們要在一個數據庫裏面。而後消息會通過MQ發送到消息的消費方。若是消息發送失敗,會進行重試發送。
消息消費方,須要處理這個消息,並完成本身的業務邏輯。此時若是本地事務處理成功,代表已經處理成功了,若是處理失敗,那麼就會重試執行。若是是業務上面的失敗,能夠給生產方發送一個業務補償消息,通知生產方進行回滾等操做。
生產方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發送一遍。若是有靠譜的自動對帳補帳邏輯,這種方案仍是很是實用的。
事務消息
事務消息做爲一種異步確保型事務, 將兩個事務分支經過MQ進行異步解耦,事務消息的設計流程一樣借鑑了兩階段提交理論,總體交互流程以下圖所示:
事務發起方首先發送prepare消息到MQ。
在發送prepare消息成功後執行本地事務。
根據本地事務執行結果返回commit或者是rollback。
若是消息是rollback,MQ將刪除該prepare消息不進行下發,若是是commit消息,MQ將會把這個消息發送給consumer端。
若是執行本地事務過程當中,執行端掛掉,或者超時,MQ將會不停的詢問其同組的其它producer來獲取狀態。
Consumer端的消費成功機制有MQ保證。
有一些第三方的MQ是支持事務消息的,好比RocketMQ,可是市面上一些主流的MQ都是不支持事務消息的,好比 RabbitMQ 和 Kafka 都不支持。
盡最大努力通知
最大努力通知方案主要也是藉助MQ消息系統來進行事務控制,這一點與可靠消息最終一致方案同樣。看來MQ中間件確實在一個分佈式系統架構中,扮演者重要的角色。最大努力通知方案是比較簡單的分佈式事務方案,它本質上就是經過按期校對,實現數據一致性。
最大努力通知方案的實現
業務活動的主動方,在完成業務處理以後,向業務活動的被動方發送消息,容許消息丟失。
主動方能夠設置時間階梯型通知規則,在通知失敗後按規則重複通知,直到通知N次後再也不通知。
主動方提供校對查詢接口給被動方按需校對查詢,用於恢復丟失的業務消息。
業務活動的被動方若是正常接收了數據,就正常返回響應,並結束事務。
若是被動方沒有正常接收,根據定時策略,向業務活動主動方查詢,恢復丟失的業務消息
最大努力通知方案的特色
用到的服務模式:可查詢操做、冪等操做。
被動方的處理結果不影響主動方的處理結果;
適用於對業務最終一致性的時間敏感度低的系統;
適合跨企業的系統間的操做,或者企業內部比較獨立的系統間的操做,好比銀行通知、商戶通知等;
方案比較
別人是怎麼作的
alipay的分佈式事務服務DTS
分佈式事務服務(Distributed Transaction Service,簡稱 DTS)是一個分佈式事務框架,用來保障在大規模分佈式環境下事務的最終一致性。DTS 從架構上分爲 xts-client 和 xts-server 兩部分,前者是一個嵌入客戶端應用的 Jar 包,主要負責事務數據的寫入和處理;後者是一個獨立的系統,主要負責異常事務的恢復。
核心概念
在 DTS 內部,咱們將一個分佈式事務的關聯方,分爲發起方和參與者兩類:
發起方: 分佈式事務的發起方負責啓動分佈式事務,觸發建立相應的主事務記錄。發起方是分佈式事務的協調者,負責調用參與者的服務,並記錄相應的事務日誌,感知整個分佈式事務狀態來決定整個事務是 COMMIT 仍是 ROLLBACK。
參與者:參與者是分佈式事務中的一個原子單位,全部參與者都必須在一階段接口(Prepare)中標註(Annotation)參與者的標識,它定義了 prepare、commit、rollback 3個基本接口,業務系統須要實現這3個接口,並保證其業務數據的冪等性,也必須保證 prepare 中的數據操做可以被提交(COMMIT)或者回滾(ROLLBACK)。從存儲結構上,DTS 的事務狀態數據能夠分爲主事務記錄(Activity)和分支事務記錄(Action)兩類:
主事務記錄 Activity:主事務記錄是整個分佈式事務的主體,其最核心的數據結構是事務號(TX_ID)和事務狀態(STATE),它是在啓動分佈式事務的時候持久化寫入數據庫的,它的狀態決定了這筆分佈式事務的狀態。
分支事務記錄 Action:分支事務記錄是主事務記錄的一個子集,它記錄了一個參與者的信息,其中包括參與者的 NAME 名稱,DTS 經過這個 NAME 來惟必定位一個參與者。經過這個分支事務信息,咱們就能夠對參與者進行提交或者回滾操做。
這應該屬於咱們上面所說的TCC模式。
eBay 本地消息表
本地消息表這種實現方式的思路,實際上是源於ebay,後來經過支付寶等公司的佈道,在業內普遍使用。其基本的設計思想是將遠程分佈式事務拆分紅一系列的本地事務。若是不考慮性能及設計優雅,藉助關係型數據庫中的表便可實現。
舉個經典的跨行轉帳的例子來描述。第一步,扣款1W,經過本地事務保證了憑證消息插入到消息表中。第二步,通知對方銀行帳戶上加1W了。那問題來了,如何通知到對方呢?
一般採用兩種方式:
採用時效性高的MQ,由對方訂閱消息並監聽,有消息時自動觸發事件
採用定時輪詢掃描的方式,去檢查消息表的數據。
相似使用本地消息表+消息通知的還有去哪兒,蘑菇街
各類第三方支付回調
最大努力通知型。如支付寶、微信的支付回調接口方式,不斷回調直至成功,或直至調用次數衰減至失敗狀態。
咱們能夠怎麼來作
2PC/3PC須要資源管理器(mysql, redis)支持XA協議,且整個事務的執行期間須要鎖住事務資源,會下降性能。故先排除。
TCC的模式,須要事務接口提供try,confirm,cancel三個接口,提升了編程的複雜性。須要依賴於業務方來配合提供這樣的接口。推行難度大,暫時排除。
最大努力通知型,應用於異構或者服務平臺當中
能夠看到ebay的經典模式中,分佈式的事務,是經過本地事務+可靠消息,來達到事務的最終一致性的。可是出現了事務消息,就把本地事務的工做給涵蓋在事務消息當中了。因此,接下來要基於事務消息來套咱們的應用場景,看起是否知足咱們對分佈式事務產品的要求。