微服務(wù)之Saga事務(wù)管理

Saga是一種在微服務(wù)架構(gòu)中維護(hù)數(shù)據(jù)一致性的機(jī)制,一個(gè)Saga表示需要更新多個(gè)服務(wù)中數(shù)據(jù)的一個(gè)系統(tǒng)操作。Saga由一連串的本地事務(wù)組成,每一個(gè)本地事務(wù)負(fù)責(zé)更新自己的數(shù)據(jù)庫,這些操作滿足于ACID要求。通常Saga是通過異步消息的方式來協(xié)調(diào)各個(gè)本地事務(wù)執(zhí)行的。即:通過使用一部消息來協(xié)調(diào)一系列本地事務(wù),從而維護(hù)多個(gè)服務(wù)之間的數(shù)據(jù)一致性。
Saga的一個(gè)挑戰(zhàn)在于只滿足了ACD(原子性、一致性和持久性)特性,缺少了隔離性。因此,應(yīng)用程序必須通過一些對策來防止或是減少由于缺乏隔離性而導(dǎo)致的并發(fā)異常。
傳統(tǒng)的本地事務(wù)實(shí)現(xiàn)方式簡單,可以通過各種開發(fā)框架直接引入,例如spring提供的@Transactional。在分布式系統(tǒng)下理論上是可以通過一個(gè)單點(diǎn)服務(wù)把所有操作參與服務(wù)方的調(diào)用放到一個(gè)本地事務(wù)里管理,并通過查詢事務(wù)API確認(rèn)事務(wù)的執(zhí)行情況,以及通過補(bǔ)充事務(wù)來回滾所作出的改變。但是這種實(shí)現(xiàn)方式的成本較高,而且耦合度太高不易于維護(hù),最致命的是系統(tǒng)性能將成為瓶頸。
如何使用Saga來維護(hù)數(shù)據(jù)一致性Saga是通過使用補(bǔ)償事務(wù)來回滾所做出的改變,與傳統(tǒng)的ACID事務(wù)不同,傳統(tǒng)事務(wù)在檢測到范圍業(yè)務(wù)規(guī)則(通常是識(shí)別異常),可以通過執(zhí)行ROLLBACK語句,輕松回滾事務(wù)。而Saga無法做到自動(dòng)回滾,只能通過編寫配套的補(bǔ)償事務(wù)進(jìn)行回滾。假設(shè)一個(gè)Saga的第n+1個(gè)事務(wù)失敗了,必須撤銷前n個(gè)事務(wù)的影響。從概念上講每個(gè)步驟Ti都有一個(gè)相應(yīng)的補(bǔ)償事務(wù)Ci,它可以撤銷Ti的影響。要撤銷前n個(gè)步驟的影響,Saga必須以相反的順序執(zhí)行每個(gè)Ci。

Saga的解構(gòu)這里需要注意的一點(diǎn)就是并非所有事物都需要補(bǔ)償事務(wù),有些事務(wù)可能只是記錄了一條歷史,或者發(fā)送一條通知消息等。
通常一個(gè)saga包含三種類型的事務(wù)
可補(bǔ)償性事務(wù):可以使用補(bǔ)償事務(wù)回滾的事務(wù)。
關(guān)鍵性事務(wù):saga執(zhí)行過程的關(guān)鍵點(diǎn)。如果關(guān)鍵性事務(wù)執(zhí)行成功,則saga將一直運(yùn)行到完成。關(guān)鍵性事務(wù)不見得是一個(gè)可補(bǔ)償查詢的事務(wù),或者可重復(fù)性事務(wù),但是它可以是最后一個(gè)可補(bǔ)償事務(wù)或第一個(gè)可重復(fù)的事務(wù)。
可重復(fù)性事務(wù):在關(guān)鍵事務(wù)之后的事務(wù),它不需要回滾并保證能夠完成。
Saga的兩種協(xié)調(diào)方式eg:下單 → 支付 → 通知。下單時(shí)可以創(chuàng)建訂單、鎖定庫存,這兩個(gè)事務(wù)都屬于可補(bǔ)償性事務(wù)。任何一步失敗都可以回滾訂單狀態(tài)和恢復(fù)庫存。而支付事務(wù)則屬于關(guān)鍵性事務(wù),只要支付成功那么整個(gè)saga從理論上就已經(jīng)算是成功了,即使支付失敗,也不需要回滾支付事務(wù)本身。最后的通知事務(wù),這就是一個(gè)不需要冪等的可重復(fù)性事務(wù),即使執(zhí)行失敗,也可以重試。

協(xié)同式(分布式實(shí)現(xiàn)方式)
把Saga的決策和執(zhí)行順序邏輯分布在Saga的每一個(gè)參與方中,他們通過交換事件的方式來進(jìn)行溝通。使用協(xié)同式實(shí)現(xiàn)Saga時(shí),沒有一個(gè)中央?yún)f(xié)調(diào)器會(huì)告訴Saga參與方該做什么。相反,Saga的參與方訂閱彼此的事件并作出相應(yīng)的響應(yīng)。在具體的實(shí)現(xiàn)過程中,必須保證Saga的參與方將更新其本地?cái)?shù)據(jù)庫和發(fā)布事件作為數(shù)據(jù)庫事務(wù)的一部分,基于協(xié)同式Saga的每一步都會(huì)更新數(shù)據(jù)并發(fā)一個(gè)事件,且確保能夠?qū)⒔邮盏降拿總€(gè)事件映射到自己的數(shù)據(jù)上。

優(yōu)點(diǎn) | 缺點(diǎn) |
松耦合:采用事件源的方式降低系統(tǒng)復(fù)雜程度,提升系統(tǒng)擴(kuò)展性, 處理模塊通過訂閱事件的方式降低系統(tǒng)的耦合程度 | 更難理解:協(xié)同式saga的邏輯分布在每個(gè)服務(wù)的實(shí)現(xiàn)中,因此開發(fā)人員很難理解特定的saga是如何工作的。 |
實(shí)現(xiàn)方式簡單:服務(wù)在創(chuàng)建、更新或刪除業(yè)務(wù)對象時(shí)發(fā)布事件 | 服務(wù)之間的循環(huán)依賴:saga的參與方訂閱彼此的時(shí)間,這通常會(huì)導(dǎo)致循環(huán)依賴關(guān)系。雖然這并不一定會(huì)有什么影響,但循環(huán)依賴被認(rèn)為是一種不好的設(shè)計(jì)風(fēng)格。 |
編排式(集中式實(shí)現(xiàn)方式)
當(dāng)使用編排式saga時(shí),開發(fā)人員定義一個(gè)編排器類,這個(gè)類的唯一職責(zé)就是告訴saga的參與方該做什么事情。相當(dāng)于把所有參與方集中管理起來。編排器使用命令/異步響應(yīng)方式與saga的參與方服務(wù)通信。為了完成saga中的每一個(gè)環(huán)節(jié),編排器對某個(gè)參與方發(fā)出一個(gè)命令式的消息,告訴這個(gè)參與方該做什么操作。當(dāng)參與方服務(wù)完成操作后,會(huì)給編排器發(fā)出一個(gè)答復(fù)消息。編排器處理這個(gè)消息后,并決定下一步操作是什么。

對于編排器比較形象的一種呈現(xiàn)方式就是,將其看做是一個(gè)狀態(tài)機(jī)。狀態(tài)機(jī)都是通過觸發(fā)事件來維護(hù)一組狀態(tài)的轉(zhuǎn)換。對于saga來說,觸發(fā)事件就是對每個(gè)參與方的調(diào)用,而狀態(tài)的轉(zhuǎn)換就是參與方對調(diào)用事件的執(zhí)行結(jié)果,也就是在參與方執(zhí)行完本地事務(wù)后觸發(fā)狀態(tài)的更改。對于狀態(tài)機(jī)是有高效的測試策略的,因此,使用狀態(tài)機(jī)模型可以更輕松地設(shè)計(jì)、實(shí)現(xiàn)和測試saga。
優(yōu)點(diǎn) | 缺點(diǎn) |
高更簡單的依賴關(guān)系:編排的一個(gè)好處就是它不會(huì)引入循環(huán)依賴關(guān)系。編排器調(diào)用參與方,但參與方不會(huì)調(diào)用編排器。 | 業(yè)務(wù)耦合程度較高:由于在編排器中存在集中過多業(yè)務(wù)邏輯實(shí)現(xiàn),對于可擴(kuò)展性存在一些挑戰(zhàn)。 |
較少的耦合:參與方之間是松耦合的,每個(gè)參與方只需要向編排器提供調(diào)用的PAI就行,參與方之間不需要相互感知。 | 連續(xù)性保障:要有編排器故障的恢復(fù)策略,可以根據(jù)狀態(tài)進(jìn)行補(bǔ)償。 |
核心協(xié)調(diào)邏輯本地化在編排器中,領(lǐng)域?qū)ο蟾雍唵吻逦恍枰私鈪⑴c方的具體實(shí)現(xiàn)。 |
ACID中的隔離屬性可確保同時(shí)執(zhí)行多個(gè)事務(wù)的結(jié)果與順序執(zhí)行它們的結(jié)果相同。因?yàn)閟aga事務(wù)沒有準(zhǔn)備階段,事務(wù)沒有隔離,如果兩個(gè)saga事務(wù)同時(shí)操作同一資源就會(huì)遇到我們操作多線程臨界資源的的情況。因此會(huì)產(chǎn)生更新丟失,臟數(shù)據(jù)讀取、不可重復(fù)讀等問題。
更新丟失:一個(gè)saga沒有讀取更新,而是直接覆蓋了另一個(gè)saga所做的更改。
臟讀:一個(gè)事務(wù)或一個(gè)saga讀取了尚未完成的saga所做出的更新。
不可重復(fù)讀:一個(gè)saga的兩個(gè)不同步驟讀取相同的數(shù)據(jù)卻獲得了不同結(jié)果,因?yàn)橛辛硗庖粋€(gè)saga已經(jīng)進(jìn)行了更新。

隔離的本質(zhì)是為了控制并發(fā),防止并發(fā)事務(wù)操作相同資源而引起的結(jié)果錯(cuò)亂。saga模式下解決由于缺少隔離性帶來弊端的對策有:
語義鎖
所謂的語義鎖就是一個(gè)標(biāo)記性狀態(tài)。在使用語義鎖時(shí),saga的可補(bǔ)充性事務(wù)會(huì)在其創(chuàng)建或更新的任何記錄中設(shè)置標(biāo)志,該標(biāo)志表示記錄未提交且可能發(fā)生更改。該標(biāo)志可以阻止其他事務(wù)訪問記錄的鎖,也可以是指示其他事物應(yīng)該謹(jǐn)慎地處理該記錄的一個(gè)警告。這個(gè)標(biāo)志會(huì)被一個(gè)可重復(fù)的事務(wù)清除,這表示saga成功完成;或通過補(bǔ)償事務(wù)清除,這表示saga發(fā)生了回滾。
eg:鎖定庫存時(shí),可以給商品打上一個(gè)出貨中的標(biāo)志,當(dāng)有其他saga獲取這個(gè)商品時(shí)發(fā)現(xiàn)是在出貨中裝填就不會(huì)再獲取這個(gè)商品了。如果訂單正常完成,可以通過一個(gè)可重復(fù)事務(wù)清除這個(gè)狀態(tài)(或者理解為通過一個(gè)定時(shí)任務(wù)將狀態(tài)更新為已出貨);如果訂單執(zhí)行失敗,那么可以通過鎖定庫存的補(bǔ)償事務(wù)回滾,將狀態(tài)更改為可出貨。
使用語義鎖的好處是他們實(shí)質(zhì)上重建了ACID事務(wù)提供的隔離性,更新相同數(shù)據(jù)的saga被序列化了,這顯著減少了編程工作量。另一個(gè)好處是它消除了客戶端重試的負(fù)擔(dān)。缺點(diǎn)是應(yīng)用程序必須管理鎖,要實(shí)現(xiàn)死鎖檢測和兜底機(jī)制。
交換式更新
一個(gè)簡單的對策是將更新操作設(shè)計(jì)為可交換的。所謂可交換就是可以按任何順序執(zhí)行,則操作是可交換的。這種對策很有用,因?yàn)樗梢员苊飧碌膩G失。
悲觀視圖
處理缺乏隔離性的另一種方法是悲觀視圖。它重新排序Saga的步驟,以最大限度地降低由于臟讀而導(dǎo)致的業(yè)務(wù)風(fēng)險(xiǎn)。
重讀值
重讀值對策可防止丟失更新。使用此計(jì)數(shù)器的Saga在更新之前重新讀取記錄,驗(yàn)證它是否未更改,然后更新記錄。如果記錄已更改,則Saga將中止并可能重新啟動(dòng)。此對策是樂觀脫機(jī)鎖模式的一種形式。
版本文件
版本文件對策之所以如此命名,是因?yàn)樗涗浟藢?shù)據(jù)執(zhí)行的操作,以便可以對它們進(jìn)行重新排序。這是將不可交換操作轉(zhuǎn)換為可交換操作的一種方法。
業(yè)務(wù)風(fēng)險(xiǎn)評級
最終的對策是基于價(jià)值(業(yè)務(wù)風(fēng)險(xiǎn))對策。這是一種基于業(yè)務(wù)風(fēng)險(xiǎn)選擇并發(fā)機(jī)制的策略。使用此對策的應(yīng)用程序使用每個(gè)請求的屬性來決定使用Saga和分布式事務(wù)。它使用Saga執(zhí)行低風(fēng)險(xiǎn)請求,可能會(huì)應(yīng)用前幾節(jié)中描述的對策。但它使用分布式事務(wù)來執(zhí)行高風(fēng)險(xiǎn)請求(例如涉及大量資金)。此對策使應(yīng)用程序能夠動(dòng)態(tài)地對業(yè)務(wù)風(fēng)險(xiǎn)、可用性和可伸縮性進(jìn)行權(quán)衡。
