為什么說(shuō)傳統(tǒng)分布式事務(wù)不再適用于微服務(wù)架構(gòu)?
傳統(tǒng)應(yīng)用使用本地事務(wù)和分布式事務(wù)保證數(shù)據(jù)一致性,但是在微服務(wù)架構(gòu)中數(shù)據(jù)都是服務(wù)私有的,需要通過(guò)服務(wù)提供的API來(lái)訪問(wèn),所以分布式事務(wù)不再適用微服務(wù)架構(gòu)。那么微服務(wù)架構(gòu)又該如何保證數(shù)據(jù)一致性呢?本文就來(lái)談?wù)勥@個(gè)話題。
- 傳統(tǒng)分布式事務(wù)不是微服務(wù)中數(shù)據(jù)一致性的***選擇
- 微服務(wù)架構(gòu)中應(yīng)滿足數(shù)據(jù)最終一致性原則
- 微服務(wù)架構(gòu)實(shí)現(xiàn)最終一致性的三種模式
- 對(duì)賬是***的***防線
傳統(tǒng)分布式事務(wù)
我們先來(lái)看下***部分,傳統(tǒng)使用本地事務(wù)和分布式事務(wù)保證一致性。
傳統(tǒng)單機(jī)應(yīng)用一般都會(huì)使用一個(gè)關(guān)系型數(shù)據(jù)庫(kù),好處是應(yīng)用可以使用ACID。為保證一致性我們只需要:開(kāi)始一個(gè)事務(wù),改變(插入,刪除,更新)很多行,然后提交事務(wù)(如果有異常時(shí)回滾事務(wù))。更進(jìn)一步,借助開(kāi)發(fā)平臺(tái)中的數(shù)據(jù)訪問(wèn)技術(shù)和框架(如 Spring),我們需要做的事情更少,只需要關(guān)注數(shù)據(jù)本身的改變。
隨著組織規(guī)模不斷擴(kuò)大,業(yè)務(wù)量不斷增長(zhǎng),單機(jī)應(yīng)用和數(shù)據(jù)庫(kù)已經(jīng)不足以支持龐大的業(yè)務(wù)量和數(shù)據(jù)量,這個(gè)時(shí)候需要對(duì)應(yīng)用和數(shù)據(jù)庫(kù)進(jìn)行拆分,這就出現(xiàn)了一個(gè)應(yīng)用需要同時(shí)訪問(wèn)兩個(gè)或兩個(gè)以上的數(shù)據(jù)庫(kù)情況。開(kāi)始我們用分布式事務(wù)來(lái)保證一致性,也就是我們常說(shuō)的兩階段提交協(xié)議(2PC)。
本地事務(wù)和分布式事務(wù)現(xiàn)在已經(jīng)非常成熟,相關(guān)介紹很豐富,此處不再討論。我們下面來(lái)談?wù)劄槭裁捶植际绞聞?wù)不適用于微服務(wù)架構(gòu)。
首先,對(duì)于微服務(wù)架構(gòu)來(lái)說(shuō),數(shù)據(jù)訪問(wèn)變得更加復(fù)雜,這是因?yàn)閿?shù)據(jù)都是微服務(wù)私有的,唯一可訪問(wèn)的方式就是通過(guò) API。這種打包數(shù)據(jù)訪問(wèn)方式使得微服務(wù)之間松耦合,并且彼此之間獨(dú)立,更容易進(jìn)行性能擴(kuò)展。
其次,不同的微服務(wù)經(jīng)常使用不同的數(shù)據(jù)庫(kù)。應(yīng)用會(huì)產(chǎn)生各種不同類型的數(shù)據(jù),關(guān)系型數(shù)據(jù)庫(kù)并不一定是***選擇。
例如,某個(gè)產(chǎn)生和查詢字符串的應(yīng)用采用 Elasticsearch 的字符搜索引擎;某個(gè)產(chǎn)生社交圖片數(shù)據(jù)的應(yīng)用可以采用圖數(shù)據(jù)庫(kù),例如Neo4j。
基于微服務(wù)的應(yīng)用一般都使用 SQL 和 NoSQL 結(jié)合的模式。但是這些非關(guān)系型數(shù)據(jù)大多數(shù)并不支持 2PC。
可見(jiàn)在微服務(wù)架構(gòu)中已經(jīng)不能選擇分布式事務(wù)了。
最終一致性原則
依據(jù) CAP 理論,必須在可用性(availability)和一致性(consistency)之間做出選擇。如果選擇提供一致性需要付出在滿足一致性之前阻塞其他并發(fā)訪問(wèn)的代價(jià)。這可能持續(xù)一個(gè)不確定的時(shí)間,尤其是在系統(tǒng)已經(jīng)表現(xiàn)出高延遲時(shí)或者網(wǎng)絡(luò)故障導(dǎo)致失去連接時(shí)。
依據(jù)目前的成功經(jīng)驗(yàn),可用性一般是更好的選擇,但是在服務(wù)和數(shù)據(jù)庫(kù)之間維護(hù)數(shù)據(jù)一致性是非常根本的需求,微服務(wù)架構(gòu)中應(yīng)選擇滿足最終一致性。
最終一致性是指系統(tǒng)中的所有數(shù)據(jù)副本經(jīng)過(guò)一定時(shí)間后,最終能夠達(dá)到一致的狀態(tài)。
當(dāng)然選擇了最終一致性,就要保證到最終的這段時(shí)間要在用戶可接受的范圍之內(nèi)。那么我們?cè)趺磳?shí)現(xiàn)最終一致性呢?
從一致性的本質(zhì)來(lái)看,是要保證在一個(gè)業(yè)務(wù)邏輯中包含的服務(wù)要么都成功,要么都失敗。那我們?cè)趺催x擇方向呢?保證成功還是保證失敗呢?
我們說(shuō)業(yè)務(wù)模式?jīng)Q定了我們的選擇。實(shí)現(xiàn)最終一致性有三種模式:可靠事件模式、業(yè)務(wù)補(bǔ)償模式、TCC 模式。
可靠事件模式
可靠事件模式屬于事件驅(qū)動(dòng)架構(gòu),當(dāng)某件重要事情發(fā)生時(shí),例如更新一個(gè)業(yè)務(wù)實(shí)體,微服務(wù)會(huì)向消息代理發(fā)布一個(gè)事件。消息代理會(huì)向訂閱事件的微服務(wù)推送事件,當(dāng)訂閱這些事件的微服務(wù)接收此事件時(shí),就可以完成自己的業(yè)務(wù),也可能會(huì)引發(fā)更多的事件發(fā)布。
1. 如訂單服務(wù)創(chuàng)建一個(gè)待支付的訂單,發(fā)布一個(gè)“創(chuàng)建訂單”的事件。
2. 支付服務(wù)消費(fèi)“創(chuàng)建訂單”事件,支付完成后發(fā)布一個(gè)“支付完成”事件。
3. 訂單服務(wù)消費(fèi)“支付完成”事件,訂單狀態(tài)更新為待出庫(kù)。
從而就實(shí)現(xiàn)了完成的業(yè)務(wù)流程。但是這并不是一個(gè)***的流程。
這個(gè)過(guò)程可能導(dǎo)致出現(xiàn)不一致的地方在于:某個(gè)微服務(wù)在更新了業(yè)務(wù)實(shí)體后發(fā)布事件卻失敗;雖然微服務(wù)發(fā)布事件成功,但是消息代理未能正確推送事件到訂閱的微服務(wù);接受事件的微服務(wù)重復(fù)消費(fèi)了事件。
可靠事件模式在于保證可靠事件投遞和避免重復(fù)消費(fèi),可靠事件投遞定義為:
- 每個(gè)服務(wù)原子性的業(yè)務(wù)操作和發(fā)布事件。
- 消息代理確保事件傳遞至少一次。
避免重復(fù)消費(fèi)要求服務(wù)實(shí)現(xiàn)冪等性,如支付服務(wù)不能因?yàn)橹貜?fù)收到事件而多次支付。
因?yàn)楝F(xiàn)在流行的消息隊(duì)列都實(shí)現(xiàn)了事件的持久化和 at least once 的投遞模式,『消息代理確保事件投遞至少一次』已經(jīng)滿足,今天不做展開(kāi)。
下面分享的內(nèi)容主要從可靠事件投遞和實(shí)現(xiàn)冪等性兩方面來(lái)討論,我們先來(lái)看可靠事件投遞。
首先我們來(lái)看一個(gè)實(shí)現(xiàn)的代碼片段,這是從某生產(chǎn)系統(tǒng)上截取下來(lái)的。
根據(jù)上述代碼及注釋,初看可能出現(xiàn) 3 種情況:
- 操作數(shù)據(jù)庫(kù)成功,向消息代理投遞事件也成功。
- 操作數(shù)據(jù)庫(kù)失敗,不會(huì)向消息代理中投遞事件了。
- 操作數(shù)據(jù)庫(kù)成功,但是向消息代理中投遞事件時(shí)失敗,向外拋出了異常,剛剛執(zhí)行的更新數(shù)據(jù)庫(kù)的操作將被回滾。
從上面分析的幾種情況來(lái)看,貌似沒(méi)有問(wèn)題。但是仔細(xì)分析不難發(fā)現(xiàn)缺陷所在,在上面的處理過(guò)程中存在一段隱患時(shí)間窗口。
微服務(wù) A 投遞事件的時(shí)候可能消息代理已經(jīng)處理成功,但是返回響應(yīng)的時(shí)候網(wǎng)絡(luò)異常,導(dǎo)致 append 操作拋出異常。最終結(jié)果是事件被投遞,數(shù)據(jù)庫(kù)卻被回滾。
在投遞完成后到數(shù)據(jù)庫(kù) commit 操作之間如果微服務(wù) A 宕機(jī)也將造成數(shù)據(jù)庫(kù)操作因?yàn)檫B接異常關(guān)閉而被回滾。最終結(jié)果還是事件被投遞,數(shù)據(jù)庫(kù)卻被回滾。這個(gè)實(shí)現(xiàn)往往運(yùn)行很長(zhǎng)時(shí)間都沒(méi)有出過(guò)問(wèn)題,但是一旦出現(xiàn)了將會(huì)讓人感覺(jué)莫名,很難發(fā)現(xiàn)問(wèn)題所在。
下面給出兩種可靠事件投遞的實(shí)現(xiàn)方式。
1. 本地事件表
本地事件表方法將事件和業(yè)務(wù)數(shù)據(jù)保存在同一個(gè)數(shù)據(jù)庫(kù)中,使用一個(gè)額外的“事件恢復(fù)”服務(wù)來(lái)恢復(fù)事件,由本地事務(wù)保證更新業(yè)務(wù)和發(fā)布事件的原子性。考慮到事件恢復(fù)可能會(huì)有一定的延時(shí),服務(wù)在完成本地事務(wù)后可立即向消息代理發(fā)布一個(gè)事件。
- 微服務(wù)在同一個(gè)本地事務(wù)中記錄業(yè)務(wù)數(shù)據(jù)和事件。
- 微服務(wù)實(shí)時(shí)發(fā)布一個(gè)事件立即通知關(guān)聯(lián)的業(yè)務(wù)服務(wù),如果事件發(fā)布成功立即刪除記錄的事件。
- 事件恢復(fù)服務(wù)定時(shí)從事件表中恢復(fù)未發(fā)布成功的事件,重新發(fā)布,重新發(fā)布成功才刪除記錄的事件。
其中第Ⅱ條的操作主要是為了增加發(fā)布事件的實(shí)時(shí)性,由第三條保證事件一定被發(fā)布。
本地事件表方式業(yè)務(wù)系統(tǒng)和事件系統(tǒng)耦合比較緊密,額外的事件數(shù)據(jù)庫(kù)操作也會(huì)給數(shù)據(jù)庫(kù)帶來(lái)額外的壓力,可能成為瓶頸。
2. 外部事件表
外部事件表方法將事件持久化到外部的事件系統(tǒng),事件系統(tǒng)需提供實(shí)時(shí)事件服務(wù)以接受微服務(wù)發(fā)布事件,同時(shí)事件系統(tǒng)還需要提供事件恢復(fù)服務(wù)來(lái)確認(rèn)和恢復(fù)事件。
- 業(yè)務(wù)服務(wù)在事務(wù)提交前,通過(guò)實(shí)時(shí)事件服務(wù)向事件系統(tǒng)請(qǐng)求發(fā)送事件,事件系統(tǒng)只記錄事件并不真正發(fā)送。
- 業(yè)務(wù)服務(wù)在提交后,通過(guò)實(shí)時(shí)事件服務(wù)向事件系統(tǒng)確認(rèn)發(fā)送,事件得到確認(rèn)后事件系統(tǒng)才真正發(fā)布事件到消息代理。
- 業(yè)務(wù)服務(wù)在業(yè)務(wù)回滾時(shí),通過(guò)實(shí)時(shí)事件向事件系統(tǒng)取消事件。
- 如果業(yè)務(wù)服務(wù)在發(fā)送確認(rèn)或取消之前停止服務(wù)了怎么辦呢?事件系統(tǒng)的事件恢復(fù)服務(wù)會(huì)定期找到未確認(rèn)發(fā)送的事件向業(yè)務(wù)服務(wù)查詢狀態(tài),根據(jù)業(yè)務(wù)服務(wù)返回的狀態(tài)決定事件是要發(fā)布還是取消。
該方式將業(yè)務(wù)系統(tǒng)和事件系統(tǒng)獨(dú)立解耦,都可以獨(dú)立伸縮。但是這種方式需要一次額外的發(fā)送操作,并且需要發(fā)布者提供額外的查詢接口。
介紹完了可靠事件投遞再來(lái)說(shuō)一說(shuō)冪等性的實(shí)現(xiàn),有些事件本身是冪等的,有些事件卻不是。
本身具有冪等性的事件需要考慮執(zhí)行順序
如果事件本身描述的是某個(gè)時(shí)間點(diǎn)的固定值(如賬戶余額為 100),而不是描述一條轉(zhuǎn)換指令(如余額增加 10),那么這個(gè)事件是冪等的。
我們要意識(shí)到事件可能出現(xiàn)的次數(shù)和順序是不可預(yù)測(cè)的,需要保證冪等事件的順序執(zhí)行,否則結(jié)果往往不是我們想要的。
如果我們先后收到兩條事件,(1)賬戶余額更新為100,(2)賬戶余額更新為120。
1. 微服務(wù)收到事件(1)
2. 微服務(wù)收到事件(2)
3. 微服務(wù)再次收到事件(1)
顯然結(jié)果是錯(cuò)誤的,所以我們需要保證事件(2)一旦執(zhí)行事件(1)就不能再處理,否則賬戶余額仍不是我們想要的結(jié)果。
為保證事件的順序一個(gè)簡(jiǎn)單的做法是在事件中添加時(shí)間戳,微服務(wù)記錄每類型的事件***處理的時(shí)間戳,如果收到的事件的時(shí)間戳早于我們記錄的,丟棄該事件。如果事件不是在同一個(gè)服務(wù)器上發(fā)出的,那么服務(wù)器之間的時(shí)間同步是個(gè)難題,更穩(wěn)妥的做法是使用一個(gè)全局遞增序列號(hào)替換時(shí)間戳。
對(duì)于本身不具有冪等性的操作,主要思想是為每條事件存儲(chǔ)執(zhí)行結(jié)果,當(dāng)收到一條事件時(shí)我們需要根據(jù)事件的 ID 查詢?cè)撌录欠褚呀?jīng)執(zhí)行過(guò),如果執(zhí)行過(guò)直接返回上一次的執(zhí)行結(jié)果,否則調(diào)度執(zhí)行事件。
在這個(gè)思想下我們需要考慮重復(fù)執(zhí)行一條事件和查詢存儲(chǔ)結(jié)果的開(kāi)銷。
重復(fù)處理開(kāi)銷小的事件
如果重復(fù)處理一條事件開(kāi)銷很小,或者可預(yù)見(jiàn)只有非常少的事件會(huì)被重復(fù)接收,可以選擇重復(fù)處理一次事件,在將事件數(shù)據(jù)持久化時(shí)由數(shù)據(jù)庫(kù)拋出唯一性約束異常。
重復(fù)處理開(kāi)銷大事件使用事件存儲(chǔ)過(guò)濾重復(fù)事件
如果重復(fù)處理一條事件的開(kāi)銷相比額外一次查詢的開(kāi)銷要高很多,使用一個(gè)過(guò)濾服務(wù)來(lái)過(guò)濾重復(fù)的事件,過(guò)濾服務(wù)使用事件存儲(chǔ)存儲(chǔ)已經(jīng)處理過(guò)的事件和結(jié)果。
當(dāng)收到一條事件時(shí),過(guò)濾服務(wù)首先查詢事件存儲(chǔ),確定該條事件是否已經(jīng)被處理過(guò),如果事件已經(jīng)被處理過(guò),直接返回存儲(chǔ)的結(jié)果;否則調(diào)度業(yè)務(wù)服務(wù)執(zhí)行處理,并將處理完的結(jié)果存儲(chǔ)到事件存儲(chǔ)中。
一般情況下上面的方法能夠運(yùn)行得很好,如果我們的微服務(wù)是 RPC 類的服務(wù)我們需要更加小心,可能出現(xiàn)的問(wèn)題在于,(1)過(guò)濾服務(wù)在業(yè)務(wù)處理完成后才將事件結(jié)果存儲(chǔ)到事件存儲(chǔ)中,但是在業(yè)務(wù)處理完成前有可能就已經(jīng)收到重復(fù)事件,由于是 RPC 服務(wù)也不能依賴數(shù)據(jù)庫(kù)的唯一性約束;(2)業(yè)務(wù)服務(wù)的處理結(jié)果可能出現(xiàn)位置狀態(tài),一般出現(xiàn)在正常提交請(qǐng)求但是沒(méi)有收到響應(yīng)的時(shí)候。
對(duì)于問(wèn)題(1)可以按步驟記錄事件處理過(guò)程,比如事件的記錄事件的處理過(guò)程為“接收”、“發(fā)送請(qǐng)求”、“收到應(yīng)答”、“處理完成”。好處是過(guò)濾服務(wù)能及時(shí)的發(fā)現(xiàn)重復(fù)事件,進(jìn)一步還能根據(jù)事件狀態(tài)作不同的處理。
對(duì)于問(wèn)題(2)可以通過(guò)一次額外的查詢請(qǐng)求來(lái)確定事件的實(shí)際處理狀態(tài),要注意額外的查詢會(huì)帶來(lái)更長(zhǎng)時(shí)間的延時(shí),更進(jìn)一步可能某些 RPC 服務(wù)根本不提供查詢接口。此時(shí)只能選擇接收暫時(shí)的不一致,時(shí)候采用對(duì)賬和人工接入的方式來(lái)保證一致性。
補(bǔ)償模式
為了描述方便,這里先定義兩個(gè)概念:
- 業(yè)務(wù)異常:業(yè)務(wù)邏輯產(chǎn)生錯(cuò)誤的情況,比如賬戶余額不足、商品庫(kù)存不足等。
- 技術(shù)異常:非業(yè)務(wù)邏輯產(chǎn)生的異常,如網(wǎng)絡(luò)連接異常、網(wǎng)絡(luò)超時(shí)等。
補(bǔ)償模式使用一個(gè)額外的協(xié)調(diào)服務(wù)來(lái)協(xié)調(diào)各個(gè)需要保證一致性的微服務(wù),協(xié)調(diào)服務(wù)按順序調(diào)用各個(gè)微服務(wù),如果某個(gè)微服務(wù)調(diào)用異常(包括業(yè)務(wù)異常和技術(shù)異常)就取消之前所有已經(jīng)調(diào)用成功的微服務(wù)。
補(bǔ)償模式建議僅用于不能避免出現(xiàn)業(yè)務(wù)異常的情況,如果有可能應(yīng)該優(yōu)化業(yè)務(wù)模式,以避免要求補(bǔ)償事務(wù)。如賬戶余額不足的業(yè)務(wù)異常可通過(guò)預(yù)先凍結(jié)金額的方式避免,商品庫(kù)存不足可要求商家準(zhǔn)備額外的庫(kù)存等。
我們通過(guò)一個(gè)實(shí)例來(lái)說(shuō)明補(bǔ)償模式,一家旅行公司提供預(yù)訂行程的業(yè)務(wù),可以通過(guò)公司的網(wǎng)站提前預(yù)訂飛機(jī)票、火車(chē)票、酒店等。
假設(shè)一位客戶規(guī)劃的行程是:
上海-北京6月19日9點(diǎn)的某某航班。
某某酒店住宿3晚。
北京-上海6月22日17點(diǎn)火車(chē)。
在客戶提交行程后,旅行公司的預(yù)訂行程業(yè)務(wù)按順序串行的調(diào)用航班預(yù)訂服務(wù)、酒店預(yù)訂服務(wù)、火車(chē)預(yù)訂服務(wù)。***的火車(chē)預(yù)訂服務(wù)成功后整個(gè)預(yù)訂業(yè)務(wù)才算完成。
如果火車(chē)票預(yù)訂服務(wù)沒(méi)有調(diào)用成功,那么之前預(yù)訂的航班、酒店都得取消。取消之前預(yù)訂的酒店、航班即為補(bǔ)償過(guò)程。
為了降低開(kāi)發(fā)的復(fù)雜性和提高效率,協(xié)調(diào)服務(wù)實(shí)現(xiàn)為一個(gè)通用的補(bǔ)償框架。補(bǔ)償框架提供服務(wù)編排和自動(dòng)完成補(bǔ)償?shù)哪芰Α?/p>
要實(shí)現(xiàn)補(bǔ)償過(guò)程,我們需要做到兩點(diǎn):
首先要確定失敗的步驟和狀態(tài),從而確定需要補(bǔ)償?shù)姆秶?/p>
在上面的例子中我們不僅要知道第 3 個(gè)步驟(預(yù)訂火車(chē))失敗,還要知道失敗的原因。如果是因?yàn)轭A(yù)訂火車(chē)服務(wù)返回?zé)o票,那么補(bǔ)償過(guò)程只需要取消前兩個(gè)步驟就可以了;但是如果失敗的原因是因?yàn)榫W(wǎng)絡(luò)超時(shí),那么補(bǔ)償過(guò)程除前兩個(gè)步驟之外還需要包括第 3 個(gè)步驟。
其次要能提供補(bǔ)償操作使用到的業(yè)務(wù)數(shù)據(jù)。
比如一個(gè)支付微服務(wù)的補(bǔ)償操作要求參數(shù)包括支付時(shí)的業(yè)務(wù)流水 id、賬號(hào)和金額。理論上說(shuō)實(shí)際完成補(bǔ)償操作可以根據(jù)唯一的業(yè)務(wù)流水 id 就可以,但是提供更多的要素有益于微服務(wù)的健壯性,微服務(wù)在收到補(bǔ)償操作的時(shí)候可以做業(yè)務(wù)的檢查,比如檢查賬戶是否相等,金額是否一致等等。
實(shí)現(xiàn)補(bǔ)償模式的關(guān)鍵在于業(yè)務(wù)流水的記錄
做到上面兩點(diǎn)的辦法是記錄完整的業(yè)務(wù)流水,可以通過(guò)業(yè)務(wù)流水的狀態(tài)來(lái)確定需要補(bǔ)償?shù)牟襟E,同時(shí)業(yè)務(wù)流水為補(bǔ)償操作提供需要的業(yè)務(wù)數(shù)據(jù)。
當(dāng)客戶的一個(gè)預(yù)訂請(qǐng)求達(dá)到時(shí),協(xié)調(diào)服務(wù)(補(bǔ)償框架)為請(qǐng)求生成一個(gè)全局唯一的業(yè)務(wù)流水號(hào),并在調(diào)用各個(gè)工作服務(wù)的同時(shí)記錄完整的狀態(tài)。
- 記錄調(diào)用 bookFlight 的業(yè)務(wù)流水,調(diào)用 bookFlight 服務(wù),更新業(yè)務(wù)流水狀態(tài)。
- 記錄調(diào)用 bookHotel 的業(yè)務(wù)流水,調(diào)用 bookHotel 服務(wù),更新業(yè)務(wù)流水狀態(tài)。
- 記錄調(diào)用 bookTrain 的業(yè)務(wù)流水,調(diào)用 bookTrain 服務(wù),更新業(yè)務(wù)流水狀態(tài)。
當(dāng)調(diào)用某個(gè)服務(wù)出現(xiàn)異常時(shí),比如第 3 步驟(預(yù)訂火車(chē))異常。
協(xié)調(diào)服務(wù)(補(bǔ)償框架)同樣會(huì)記錄第 3 步的狀態(tài),同時(shí)會(huì)另外記錄一條事件,說(shuō)明業(yè)務(wù)出現(xiàn)了異常。然后就是執(zhí)行補(bǔ)償過(guò)程了,可以從業(yè)務(wù)流水的狀態(tài)中知道補(bǔ)償?shù)姆秶a(bǔ)償過(guò)程中需要的業(yè)務(wù)數(shù)據(jù)從記錄的業(yè)務(wù)流水中獲取。
對(duì)于一個(gè)通用的補(bǔ)償框架來(lái)說(shuō),預(yù)先知道微服務(wù)需要記錄的業(yè)務(wù)要素是不可能的。那么就需要一種方法來(lái)保證業(yè)務(wù)流水的可擴(kuò)展性,這里介紹兩種方法:大表和關(guān)聯(lián)表。
大表顧明思議就是設(shè)計(jì)時(shí)除必須的字段外,還需要預(yù)留大量的備用字段,框架可以提供輔助工具來(lái)幫助將業(yè)務(wù)數(shù)據(jù)映射到備用字段中。
關(guān)聯(lián)表,分為框架表和業(yè)務(wù)表,技術(shù)表中保存為實(shí)現(xiàn)補(bǔ)償操作所需要的技術(shù)數(shù)據(jù),業(yè)務(wù)表保存業(yè)務(wù)數(shù)據(jù),通過(guò)在技術(shù)表中增加業(yè)務(wù)表名和業(yè)務(wù)表主鍵來(lái)建立和業(yè)務(wù)數(shù)據(jù)的關(guān)聯(lián)。
大表對(duì)于框架層實(shí)現(xiàn)起來(lái)簡(jiǎn)單,但是也有一些難點(diǎn),比如預(yù)留多少字段合適,每個(gè)字段又需要預(yù)留多少長(zhǎng)度。另外一個(gè)難點(diǎn)是如果向從數(shù)據(jù)層面來(lái)查詢數(shù)據(jù),很難看出備用字段的業(yè)務(wù)含義,維護(hù)過(guò)程不友好。
關(guān)聯(lián)表在業(yè)務(wù)要素上更靈活,能支持不同的業(yè)務(wù)類型記錄不同的業(yè)務(wù)要素;但是對(duì)于框架實(shí)現(xiàn)上難度更高,另外每次查詢都需要復(fù)雜的關(guān)聯(lián)動(dòng)作,性能方面會(huì)受影響。
有了上面的完整的流水記錄,協(xié)調(diào)服務(wù)就可以根據(jù)工作服務(wù)的狀態(tài)在異常時(shí)完成補(bǔ)償過(guò)程。但是補(bǔ)償由于網(wǎng)絡(luò)等原因,補(bǔ)償操作并不一定能保證 100%成功,這時(shí)候我們還要做更多一點(diǎn)。
通過(guò)重試保證補(bǔ)償過(guò)程的完整,從而滿足最終一致性
補(bǔ)償過(guò)程作為一個(gè)服務(wù)調(diào)用過(guò)程同樣存在調(diào)用不成功的情況,這個(gè)時(shí)候需要通過(guò)重試的機(jī)制來(lái)保證補(bǔ)償?shù)某晒β省.?dāng)然這也就要求補(bǔ)償操作本身具備冪等性。
關(guān)于冪等性的實(shí)現(xiàn)在前面做過(guò)討論。
如果只是一味的失敗就立即重試會(huì)給工作服務(wù)造成不必要的壓力,我們要根據(jù)服務(wù)執(zhí)行失敗的原因來(lái)選擇不同的重試策略。
1) 如果失敗的原因不是暫時(shí)性的,由于業(yè)務(wù)因素導(dǎo)致(如業(yè)務(wù)要素檢查失敗)的業(yè)務(wù)錯(cuò)誤,這類錯(cuò)誤是不會(huì)重發(fā)就能自動(dòng)恢復(fù)的,那么應(yīng)該立即終止重試。
2) 如果錯(cuò)誤的原因是一些罕見(jiàn)的異常,比如因?yàn)榫W(wǎng)絡(luò)傳輸過(guò)程出現(xiàn)數(shù)據(jù)丟失或者錯(cuò)誤,應(yīng)該立即再次重試,因?yàn)轭愃频腻e(cuò)誤一般很少會(huì)再次發(fā)生。
3) 如果錯(cuò)誤的原因是系統(tǒng)繁忙(比如 HTTP 協(xié)議返回的 500 或者另外約定的返回碼)或者超時(shí),這個(gè)時(shí)候需要等待一些時(shí)間再重試。
重試操作一般會(huì)指定重試次數(shù)上線,如果重試次數(shù)達(dá)到了上限就不再進(jìn)行重試了。這個(gè)時(shí)候應(yīng)該通過(guò)一種手段通知相關(guān)人員進(jìn)行處理。
對(duì)于等待重試的策略如果重試時(shí)仍然錯(cuò)誤,可逐漸增加等待的時(shí)間,直到達(dá)到一個(gè)上限后,以上限作為等待時(shí)間。
如果某個(gè)時(shí)刻聚集了大量需要重試的操作,補(bǔ)償框架需要控制請(qǐng)求的流量,以防止對(duì)工作服務(wù)造成過(guò)大的壓力。
另外關(guān)于補(bǔ)償模式還有幾點(diǎn)補(bǔ)充說(shuō)明。
微服務(wù)實(shí)現(xiàn)補(bǔ)償操作不是簡(jiǎn)單的回退到業(yè)務(wù)發(fā)生時(shí)的狀態(tài),因?yàn)榭赡苓€有其他的并發(fā)的請(qǐng)求同時(shí)更改了狀態(tài)。一般都使用逆操作的方式完成補(bǔ)償。
補(bǔ)償過(guò)程不需要嚴(yán)格按照與業(yè)務(wù)發(fā)生的相反順序執(zhí)行,可以依據(jù)工作服務(wù)的重用程度優(yōu)先執(zhí)行,甚至是可以并發(fā)的執(zhí)行。
有些服務(wù)的補(bǔ)償過(guò)程是有依賴關(guān)系的,被依賴服務(wù)的補(bǔ)償操作沒(méi)有成功就要及時(shí)終止補(bǔ)償過(guò)程。
如果在一個(gè)業(yè)務(wù)中包含的工作服務(wù)不是都提供了補(bǔ)償操作,那我們編排服務(wù)時(shí)應(yīng)該把提供補(bǔ)償操作的服務(wù)放在前面,這樣當(dāng)后面的工作服務(wù)錯(cuò)誤時(shí)還有機(jī)會(huì)補(bǔ)償。
設(shè)計(jì)工作服務(wù)的補(bǔ)償接口時(shí)應(yīng)該以協(xié)調(diào)服務(wù)請(qǐng)求的業(yè)務(wù)要素作為條件,不要以工作服務(wù)的應(yīng)答要素作為條件。因?yàn)檫€存在超時(shí)需要補(bǔ)償?shù)那闆r,這時(shí)補(bǔ)償框架就沒(méi)法提供補(bǔ)償需要的業(yè)務(wù)要素。
TCC模式(Try-Confirm-Cancel)
一個(gè)完整的 TCC 業(yè)務(wù)由一個(gè)主業(yè)務(wù)服務(wù)和若干個(gè)從業(yè)務(wù)服務(wù)組成,主業(yè)務(wù)服務(wù)發(fā)起并完成整個(gè)業(yè)務(wù)活動(dòng),TCC 模式要求從服務(wù)提供三個(gè)接口:Try、Confirm、Cancel。
1. Try
完成所有業(yè)務(wù)檢查
預(yù)留必須業(yè)務(wù)資源
2. Confirm
真正執(zhí)行業(yè)務(wù)
不作任何業(yè)務(wù)檢查
只使用 Try 階段預(yù)留的業(yè)務(wù)資源
Confirm 操作滿足冪等性
3. Cancel:
釋放 Try 階段預(yù)留的業(yè)務(wù)資源
Cancel 操作滿足冪等性
整個(gè) TCC 業(yè)務(wù)分成兩個(gè)階段完成。
***階段:主業(yè)務(wù)服務(wù)分別調(diào)用所有從業(yè)務(wù)的 try 操作,并在活動(dòng)管理器中登記所有從業(yè)務(wù)服務(wù)。當(dāng)所有從業(yè)務(wù)服務(wù)的 try 操作都調(diào)用成功或者某個(gè)從業(yè)務(wù)服務(wù)的 try 操作失敗,進(jìn)入第二階段。
第二階段:活動(dòng)管理器根據(jù)***階段的執(zhí)行結(jié)果來(lái)執(zhí)行 confirm 或 cancel 操作。
如果***階段所有 try 操作都成功,則活動(dòng)管理器調(diào)用所有從業(yè)務(wù)活動(dòng)的 confirm操作。否則調(diào)用所有從業(yè)務(wù)服務(wù)的 cancel 操作。
需要注意的是第二階段 confirm 或 cancel 操作本身也是滿足最終一致性的過(guò)程,在調(diào)用 confirm 或 cancel 的時(shí)候也可能因?yàn)槟撤N原因(比如網(wǎng)絡(luò))導(dǎo)致調(diào)用失敗,所以需要活動(dòng)管理支持重試的能力,同時(shí)這也就要求 confirm 和 cancel 操作具有冪等性。
在補(bǔ)償模式中一個(gè)比較明顯的缺陷是,沒(méi)有隔離性。從***個(gè)工作服務(wù)步驟開(kāi)始一直到所有工作服務(wù)完成(或者補(bǔ)償過(guò)程完成),不一致是對(duì)其他服務(wù)可見(jiàn)的。另外最終一致性的保證還充分的依賴了協(xié)調(diào)服務(wù)的健壯性,如果協(xié)調(diào)服務(wù)異常,就沒(méi)法達(dá)到一致性。
TCC模式在一定程度上彌補(bǔ)了上述的缺陷,在TCC模式中直到明確的confirm動(dòng)作,所有的業(yè)務(wù)操作都是隔離的(由業(yè)務(wù)層面保證)。另外工作服務(wù)可以通過(guò)指定 try 操作的超時(shí)時(shí)間,主動(dòng)的 cancel 預(yù)留的業(yè)務(wù)資源,從而實(shí)現(xiàn)自治的微服務(wù)。
TCC模式和補(bǔ)償模式一樣需要需要有協(xié)調(diào)服務(wù)和工作服務(wù),協(xié)調(diào)服務(wù)也可以作為通用服務(wù)一般實(shí)現(xiàn)為框架。與補(bǔ)償模式不同的是 TCC 服務(wù)框架不需要記錄詳細(xì)的業(yè)務(wù)流水,完成 confirm 和 cancel 操作的業(yè)務(wù)要素由業(yè)務(wù)服務(wù)提供。
在第4步確認(rèn)預(yù)訂之前,訂單只是pending狀態(tài),只有等到明確的confirm之后訂單才生效。
如果3個(gè)服務(wù)中某個(gè)服務(wù)try操作失敗,那么可以向TCC服務(wù)框架提交cancel,或者什么也不做由工作服務(wù)自己超時(shí)處理。
TCC 模式也不能***保證一致性,如果業(yè)務(wù)服務(wù)向 TCC 服務(wù)框架提交 confirm后,TCC 服務(wù)框架向某個(gè)工作服務(wù)提交 confirm 失敗(比如網(wǎng)絡(luò)故障),那么就會(huì)出現(xiàn)不一致,一般稱為 heuristic exception。
需要說(shuō)明的是為保證業(yè)務(wù)成功率,業(yè)務(wù)服務(wù)向 TCC 服務(wù)框架提交 confirm 以及TCC 服務(wù)框架向工作服務(wù)提交 confirm/cancel 時(shí)都要支持重試,這也就要confirm/cancel 的實(shí)現(xiàn)必須具有冪等性。如果業(yè)務(wù)服務(wù)向 TCC 服務(wù)框架提交confirm/cancel 失敗,不會(huì)導(dǎo)致不一致,因?yàn)榉?wù)***都會(huì)超時(shí)而取消。
另外 heuristic exception 是不可杜絕的,但是可以通過(guò)設(shè)置合適的超時(shí)時(shí)間,以及重試頻率和監(jiān)控措施使得出現(xiàn)這個(gè)異常的可能性降低到很小。如果出現(xiàn)了heuristic exception 是可以通過(guò)人工的手段補(bǔ)救的。
對(duì)賬是***的***防線
如果有些業(yè)務(wù)由于瞬時(shí)的網(wǎng)絡(luò)故障或調(diào)用超時(shí)等問(wèn)題,通過(guò)上文所講的 3 種模式一般都能得到很好的解決。但是在當(dāng)今云計(jì)算環(huán)境下,很多服務(wù)是依賴于外部系統(tǒng)的可用性情況,在一些重要的業(yè)務(wù)場(chǎng)景下還需要周期性的對(duì)賬來(lái)保證真實(shí)的一致性。比如支付系統(tǒng)和銀行之間每天日終是都會(huì)有對(duì)賬過(guò)程。