微服務架構下的資料一致性保證(二):可靠事件模式
第一篇分享中講到實現可靠事件模式的關鍵在於:可靠事件投遞和避免事件重複消費,其中避免事件重複消費需要微服務滿足冪等性。那麼又該如何實現可靠事件投遞?又該如何保證服務滿足冪等性?
轉載本文需註明出處:EAII企業架構創新研究院,違者必究。如需加入微信群參與微課堂、架構設計與討論直播請直接回復公眾號:“EAII企業架構創新研究院”。(微訊號:eaworld)
· · ·
大家好,今天是第二次在這裡給大家分享資料一致性的話題,在第一篇分享中我們介紹了微服務架構下應該滿足資料最終一致性原則,並介紹實現最終一致性3種模式。
本文是系列分享的第二篇,講述可靠事件模式的實現方法。
在第一篇分享中我們介紹了可靠事件模式屬於事件驅動架構,微服務完成業務操作後向訊息代理髮布事件,關聯的微服務從訊息代理訂閱到該事件從而完成相應的業務操作。
我們還強調了實現可靠事件模式的關鍵在於:可靠事件投遞和避免事件重複消費。
可靠事件投遞定義為(a)每個服務原子性的完成業務操作和釋出事件(b)訊息代理確保事件投遞至少一次。
避免重複消費要求消費事件的服務實現冪等性。
因為現在流行的訊息佇列都實現了事件的持久化和at leastonce的投遞模式,(b)特性(訊息代理確保事件投遞至少一次)已經滿足,今天不做展開。
下面分享的內容主要從可靠事件投遞和實現冪等性兩方面來討論,我們先來看可靠事件投遞。
首先我們來看一個實現的程式碼片段,這是從某生產系統上擷取下來的。
根據上述程式碼及註釋,初看可能出現3種情況:
1.操作資料庫成功,向訊息代理投遞事件也成功
2.操作資料庫失敗,不會向訊息代理中投遞事件了
3.操作資料庫成功,但是向訊息代理中投遞事件時失敗,向外丟擲了異常,剛剛執行的更新資料庫的操作將被回滾。
從上面分析的幾種情況來看,貌似沒有問題。但是仔細分析不難發現缺陷所在,在上面的處理過程中存在一段隱患時間視窗。
1) 微服務A投遞事件的時候可能訊息代理已經處理成功,但是返回響應的時候網路異常,
導致append操作丟擲異常。最終結果是事件被投遞,資料庫確被回滾。
2) 在投遞完成後到資料庫commit操作之間如果微服務A宕機也將造成資料庫操作因為連線異常關閉而被回滾。最終結果還是事件被投遞,資料庫卻被回滾。
這個實現往往執行很長時間都沒有出過問題,但是一旦出現了將會讓人感覺莫名很難發現問題所在。
· · ·
下面給出兩種可靠事件投遞的實現方式
本地事件表
本地事件表方法將事件和業務資料儲存在同一個資料庫中,使用一個額外的“事件恢復”服務來恢復事件,由本地事務保證更新業務和釋出事件的原子性。考慮到事件恢復可能會有一定的延時,服務在完成本地事務後可立即向訊息代理髮佈一個事件。
1. 微服務在同一個本地事務中記錄業務資料和事件。
2. 微服務實時釋出一個事件立即通知關聯的業務服務,如果事件釋出成功立即刪除記錄的事件。
3. 事件恢復服務定時從事件表中恢復未釋出成功的事件,重新發布,重新發布成功才刪除記錄的事件。
其中第2條的操作主要是為了增加發布事件的實時性,由第三條保證事件一定被髮布。
本地事件表方式業務系統和事件系統耦合比較緊密,額外的事件資料庫操作也會給資料庫帶來額外的壓力,可能成為瓶頸。
外部事件表
外部事件表方法將事件持久化到外部的事件系統,事件系統需提供實時事件服務以接受微服務釋出事件,同時事件系統還需要提供事件恢復服務來確認和恢復事件。
1.業務服務在事務提交前,通過實時事件服務向事件系統請求傳送事件,事件系統只記錄事件並不真正傳送。
2.業務服務在提交後,通過實時事件服務向事件系統確認傳送,事件得到確認後事件系統才真正釋出事件到訊息代理。
3.業務服務在業務回滾時,通過實時事件向事件系統取消事件。
4.如果業務服務在傳送確認或取消之前停止服務了怎麼辦呢?事件系統的事件恢復服務會定期找到未確認傳送的事件向業務服務查詢狀態,根據業務服務返回的狀態決定事件是要釋出還是取消。
該方式將業務系統和事件系統獨立解耦,都可以獨立伸縮。但是這種方式需要一次額外的傳送操作,並且需要釋出者提供額外的查詢介面。
· · ·
介紹完了可靠事件投遞再來說一說冪等性的實現,有些事件本身是冪等的,有些事件卻不是。
本身具有冪等性的事件需要考慮執行順序
如果事件本身描述的是某個時間點的固定值(如賬戶餘額為100),而不是描述一條轉換指令(如餘額增加10),那麼這個事件是冪等的。
我們要意識到事件可能出現的次數和順序是不可預測的,需要保證冪等事件的順序執行,否則結果往往不是我們想要的。
如果我們先後收到兩條事件,(1)賬戶餘額更新為100,(2)賬戶餘額更新為120。
1.微服務收到事件(1)
2. 微服務收到事件(2)
3. 微服務再次收到事件1
顯然結果是錯誤的,所以我們需要保證事件(2)一旦執行事件(1)就不能再處理,否則賬戶餘額仍不是我們想要的結果。
為保證事件的順序一個簡單的做法是在事件中新增時間戳,微服務記錄每型別的事件最後處理的時間戳,如果收到的事件的時間戳早於我們記錄的,丟棄該事件。
如果事件不是在同一個伺服器上發出的,那麼伺服器之間的時間同步是個難題,更穩妥的做法是使用一個全域性遞增序列號替換時間戳。
對於本身不具有冪等性的操作,主要思想是為每條事件儲存執行結果,當收到一條事件時我們需要根據事件的id查詢該事件是否已經執行過,如果執行過直接返回上一次的執行結果,否則排程執行事件。
在這個思想下我們需要考慮重複執行一條事件和查詢儲存結果的開銷。
· · ·
重複處理開銷小的事件重複處理
如果重複處理一條事件的開銷相比額外一次查詢的開銷要高很多,使用一個過濾服務來過濾重複的事件,過濾服務使用事件儲存儲存已經處理過的事件和結果。
當收到一條事件時,過濾服務首先查詢事件儲存,確定該條事件是否已經被處理過,如果事件已經被處理過,直接返回儲存的結果;否則排程業務服務執行處理,並將處理完的結果儲存到事件儲存中。
一般情況下上面的方法能夠執行得很好,如果我們的微服務是RPC類的服務我們需要更加小心,可能出現的問題在於,(1)過濾服務在業務處理完成後才將事件結果儲存到事件儲存中,但是在業務處理完成前有可能就已經收到重複事件,由於是RPC服務也不能依賴資料庫的唯一性約束;(2)業務服務的處理結果可能出現位置狀態,一般出現在正常提交請求但是沒有收到響應的時候。
對於問題(1)可以按步驟記錄事件處理過程,比如事件的記錄事件的處理過程為“接收”、“傳送請求”、“收到應答”、“處理完成”。好處是過濾服務能及時的發現重複事件,進一步還能根據事件狀態作不同的處理。
對於問題(2)可以通過一次額外的查詢請求來確定事件的實際處理狀態,要注意額外的查詢會帶來更長時間的延時,更進一步可能某些RPC服務根本不提供查詢介面。此時只能選擇接收暫時的不一致,時候採用對賬和人工接入的方式來保證一致性。
需要注意的是上面的冪等處理方法要求事件必須有唯一的ID(這個ID一般是業務相關的),比如用ID來保證資料庫的唯一性約束;使用事件ID來確認事件是否已經被處理;使用事
件ID來查詢RPC服務的事件處理結果。