分布式事务解决方案以及 .Net Core 下的实现(上)
數據一致性是構建業務系統需要考慮的重要問題 , 以往我們是依靠數據庫來保證數據的一致性。但是在微服務架構以及分布式環境下實現數據一致性是一個很有挑戰的的問題。最近在研究分布式事物,分布式的解決方案有很多解決方案,也讓我在研究的同時也引發了很多思考。今天我想講的是分布式事物解決方案是和saga有關。
原文地址:微服務場景下的數據一致性解決方案
incubator-servicecomb-saga地址:incubator-servicecomb-saga
servicecomb-saga-csharp(servicecomb-saga netcore sdk)地址:servicecomb-saga-csharp
根據原文做一些解釋性的地方 方便更加理解
0|1單體應用的數據一致性
我就給大家講一個國外經常用到的例子吧,就是假如有一家大型的企業,下屬有航空公司、租車公司、和連鎖酒店。這個大公司為客戶提供一站式的旅游行程規劃服務,這樣客戶只需要提供出行目的地, 這個大公司能幫助客戶預訂機票、租車、以及預訂酒店。從業務的角度,我們必須保證上述三個服務的預訂都完成才能滿足一個成功的旅游行程,否則不能成行。
我們的單體應用要滿足這個需求非常簡單,只需將這個三個服務請求放到同一個數據庫事務中,數據庫會幫我們保證全部成功或者全部回滾。
這三個服務上線公司滿意,客戶也很滿意
0|1微服務場景下的數據一致性
隨之時間的推移,這個大企業的行程規劃服務非常成功,用戶量劇增上百倍。企業的下屬航空公司、租車公司、和連鎖酒店也相繼推出了更多服務以滿足客戶需求, 我們的應用和開發團隊也因此日漸龐大。如今我們的單體應用已變得如此復雜,以至于沒人了解整個應用是怎么運作的。更糟的是新功能的上線現在需要所有研發團隊合作, 日夜奮戰數周才能完成??粗袌稣加新拭繘r愈下,公司高層對研發部門越來越不滿意。
經過數輪討論,領導最終決定將龐大的單體應用一分為四:機票預訂服務、租車服務、酒店預訂服務、和支付服務。服務各自使用自己的數據庫,并通過HTTP協議通信。 負責各服務的團隊根據市場需求按照自己的開發節奏發版上線。如今我們面臨新的挑戰:如何保證最初三個服務的預訂都完成才能滿足一個成功的旅游行程, 否則不能成行的業務規則?現在服務有各自的邊界,而且數據庫選型也不盡相同,通過數據庫保證數據一致性的方案已不可行。
0|1Sagas
經過一段時間的查找,我發現了一篇論文,1987年Hector & Kenneth 發表論文 Sagas論文地址
Saga是一個長活事務(Long Live Transaction (LLT)),可被分解成可以交錯運行的子事務集合。其中每個子事務都是一個保持數據庫一致性的真實事務(LLT = T1 + T2 + T3 + ... + Tn)。每個本地事務Tx 有對應的補償 Cx。
在大企業的業務場景下,一個行程規劃的事務就是一個Saga,其中包含四個子事務:機票預訂、租車、酒店預訂、和支付。
根據上面提到的公式
當每個saga子事務 T1, T2, …, Tn 都有對應的補償定義 C1, C2, …, Cn-1, 那么saga系統可以保證 [1]子事務序列 T1, T2, …, Tn得以完成 (最佳情況)或者序列
T1, T2, …, Tj, Cj, …,
C2, C1, 0 < j < n,
得以完成
換句話說,通過上述定義的事務/補償,saga保證滿足以下業務規則:
所有的預訂都被執行成功,如果任何一個失敗,都會被取消
如果最后一步付款失敗,所有預訂也將被取消,這些取消就是所謂的補償。
0|1Saga的恢復方式
原論文中描述了兩種類型的Saga恢復方式:
向后恢復 補償所有已完成的事務,如果任一子事務失敗。向前恢復 重試失敗的事務,假設每個子事務最終都會成功
顯然,向前恢復沒有必要提供補償事務,如果你的業務中,子事務(最終)總會成功,或補償事務難以定義或不可能,向前恢復更符合你的需求。
理論上補償事務永不失敗,然而,在分布式世界中,我們來想想極端的情況,無非就是往三種可能去考慮,成功,失敗,超時(有可能成功,也有可能失敗)。那么服務器可能會宕機,網絡可能會失敗,甚至數據中心也可能會停電。在這種情況下我們能做些什么? 最后的手段是提供回退措施,比如人工干預。
補充說明:ACID與SAGA
- 原子性(Atomicity):Saga只提供ACD保證,原子性(通過Saga協調器實現) 
- 一致性(Consistency):本地事務 + Saga log 
- 隔離性(Isolation):Saga不保證 
- 持久性(Durability):Saga log 提供 
有很多朋友會說怎么不提供隔離性啊?
例子地址:地址
- 兩個Saga事務同時操作一個資源會出現數據語義不一致的的情況 
- 兩個Saga事務同時操作一個訂單 ,彼此操作會覆蓋對方(更新丟失) 
- 兩個Saga事務同時訪問扣款賬號,無法看到退款 (臟讀取問題) 
- 在一個Saga事務內,數據被其他事務修改前后的讀取值不一致(模糊讀取問題) 
面對以上問題我們應該如何應對隔離性問題呢?
下面給出對應的解決方案
- 隔離的本質是控制并發,防止并發事務操作相同資源而引起結果錯亂 
- 在應用層面加入邏輯鎖的邏輯。 
- 業務層面采用預先凍結資金的方式隔離此部分資金。 
- 業務操作過程中通過及時讀取當前狀態的方式獲取更新。 
0|1使用Saga的條件
Saga看起來很有希望滿足我們的需求。所有長活事務都可以這樣做嗎?這里有一些限制:
Saga只允許兩個層次的嵌套,頂級的Saga和簡單子事務 [1]
在外層,全原子性不能得到滿足。也就是說,sagas可能會看到其他sagas的部分結果 [1]
每個子事務應該是獨立的原子行為 [2]
在我們的業務場景下,航班預訂、租車、酒店預訂和付款是自然獨立的行為,而且每個事務都可以用對應服務的數據庫保證原子操作。
我們在行程規劃事務層面也不需要原子性。一個用戶可以預訂最后一張機票,而后由于信用卡余額不足而被取消。同時另一個用戶可能開始會看到已無余票, 接著由于前者預訂被取消,最后一張機票被釋放,而搶到最后一個座位并完成行程規劃。
補償也有需考慮的事項:
補償事務從語義角度撤消了事務Ti的行為,但未必能將數據庫返回到執行Ti時的狀態。(例如,如果事務觸發導彈發射, 則可能無法撤消此操作)
但這對我們的業務來說不是問題。其實難以撤消的行為也有可能被補償。例如,發送電郵的事務可以通過發送解釋問題的另一封電郵來補償。
現在我們有了通過Saga來解決數據一致性問題的方案。它允許我們成功地執行所有事務,或在任何事務失敗的情況下,補償已成功的事務。 雖然Saga不提供ACID保證,但仍適用于許多數據最終一致性的場景。那我們如何設計一個Saga系統?
0|1Saga Log
Saga保證所有的子事務都得以完成或補償,但Saga系統本身也可能會崩潰。Saga崩潰時可能處于以下幾個狀態:
- Saga收到事務請求,但尚未開始。因子事務對應的微服務狀態未被Saga修改,我們什么也不需要做。 
- 一些子事務已經完成。重啟后,Saga必須接著上次完成的事務恢復。 
- 子事務已開始,但尚未完成。由于遠程服務可能已完成事務,也可能事務失敗,甚至服務請求超時,saga只能重新發起之前未確認完成的子事務。這意味著子事務必須冪等。 
- 子事務失敗,其補償事務尚未開始。Saga必須在重啟后執行對應補償事務。 
補償事務已開始但尚未完成。解決方案與上一個相同。這意味著補償事務也必須是冪等的。
所有子事務或補償事務均已完成,與第一種情況相同。
為了恢復到上述狀態,我們必須追蹤子事務及補償事務的每一步。我們決定通過事件的方式達到以上要求,并將以下事件保存在名為saga log的持久存儲中:
- Saga started event 保存整個saga請求,其中包括多個事務/補償請求 
- Transaction started event 保存對應事務請求 
- Transaction ended event 保存對應事務請求及其回復 
- Transaction aborted event 保存對應事務請求和失敗的原因 
- Transaction compensated event 保存對應補償請求及其回復 
- Saga ended event 標志著saga事務請求的結束,不需要保存任何內容 
通過將這些事件持久化在saga log中,我們可以將saga恢復到上述任何狀態。
由于Saga只需要做事件的持久化,而事件內容以JSON的形式存儲,Saga log的實現非常靈活,數據庫(SQL或NoSQL),持久消息隊列,甚至普通文件可以用作事件存儲, 當然有些能更快得幫saga恢復狀態。
0|1Saga請求的數據結構
在我們的業務場景下,航班預訂、租車、和酒店預訂沒有依賴關系,可以并行處理,但對于我們的客戶來說,只在所有預訂成功后一次付費更加友好。 那么這四個服務的事務關系可以用下圖表示:
將行程規劃請求的數據結構實現為有向非循環圖恰好合適。 圖的根是saga啟動任務,葉是saga結束任務。
0|1Parallel Saga
如上所述,航班預訂,租車和酒店預訂可以并行處理。但是這樣做會造成另一個問題:如果航班預訂失敗,而租車正在處理怎么辦?我們不能一直等待租車服務回應, 因為不知道需要等多久。
最好的辦法是再次發送租車請求,獲得回應,以便我們能夠繼續補償操作。但如果租車服務永不回應,我們可能需要采取回退措施,比如手動干預。
超時的預訂請求可能最后仍被租車服務收到,這時服務已經處理了相同的預訂和取消請求。
因此,服務的實現必須保證補償請求執行以后,再次收到的對應事務請求無效。 Caitie McCaffrey在她的演講Distributed Sagas: A Protocol for Coordinating Microservices中把這個稱為可交換的補償請求 (commutative compensating request)。
0|1分布式saga架構
分布式的Saga借鑒了zipkin的思想,Omega就是類似探針的形式,上報saga事件,然后Alpha是屬于Saga的ProcessManager.也就是協調器的東西。
- alpha充當協調者的角色,主要負責對事務的事件進行持久化存儲以及協調子事務的狀態,使其得以最終與全局事務的狀態保持一致。 
- omega是微服務中內嵌的一個agent,負責對網絡請求進行攔截并向alpha上報事務事件,并在異常情況下根據alpha下發的指令執行相應的補償操作。 
接下來我們看下Omega的內部實現
omega是微服務中內嵌的一個agent。當服務收到請求時,omega會將其攔截并從中提取請求信息中的全局事務id作為其自身的全局事務id(即Saga事件id),并提取本地事務id作為其父事務id。在預處理階段,alpha會記錄事務開始的事件;在后處理階段,alpha會記錄事務結束的事件。因此,每個成功的子事務都有一一對應的開始及結束事件。
我們再看下 他們是如何通信的
服務間通信的流程與Zipkin的類似。在服務生產方,omega會攔截請求中事務相關的id來提取事務的上下文。在服務消費方,omega會在請求中注入事務相關的id來傳遞事務的上下文。通過服務提供方和服務消費方的這種協作處理,子事務能連接起來形成一個完整的全局事務。
借助zipkin的思想就可以讓整一個事務組形成一個鏈式結構。
0|1Saga 具體處理流程
Saga處理場景是要求相關的子事務提供事務處理函數同時也提供補償函數。Saga協調器alpha會根據事務的執行情況向omega發送相關的指令,確定是否向前重試或者向后恢復。
成功場景
成功場景下,每個事務都會有開始和有對應的結束事件。
異常場景
異常場景下,omega會向alpha上報中斷事件,然后alpha會向該全局事務的其它已完成的子事務發送補償指令,確保最終所有的子事務要么都成功,要么都回滾。
超時場景 (需要調整)
超時場景下,已超時的事件會被alpha的定期掃描器檢測出來,與此同時,該超時事務對應的全局事務也會被中斷。
以上都是介紹完incubator-servicecomb-saga?總體架構。我覺得它的idea很nice,所以我和水哥,還有老杜做了一個很有趣的事情。什么事情呢?就是實現了Omega這個客戶端,github地址在這里:servicecomb-saga-csharp,目前實現上面的三種場景。
下篇結合實際的sample和大家講解下netcore下的實現,這篇文章讓大家整體的了解什么是saga。
原文地址:?https://www.cnblogs.com/WithLin/p/9556413.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com 
總結
以上是生活随笔為你收集整理的分布式事务解决方案以及 .Net Core 下的实现(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: RabbitMQ一个简单可靠的方案(.N
- 下一篇: 微软MVP张善友告诉你,微服务选型要注意
