數據一致性
前面總結了微服務的9個痛點,有些痛點沒有好的解決方案,而有些痛點是有對策的,從本章開始,就來講解某些痛點對應的解決方案。
這一章先解決數據一致性的問題,先來看一個實際的業務場景。
業務場景:下游服務失敗后上游服務如何獨善其身
前面講過,使用微服務時,很多時候需要跨多個服務去更新多個數據庫的數據,架構如圖13-1所示。
• 圖13-1 微服務上下游示意圖
如圖13-1所示,如果業務正常運轉,3個服務的數據應該分別變為a2、b2、c2,此時數據才一致。但是如果出現網絡抖動、服務超負荷或者數據庫超負荷等情況,整個處理鏈條有可能在步驟2失敗,這時數據就會變成a2、b1、c1;當然也有可能在步驟3失敗,最終數據就會變成a2、b2、c1。這樣數據就出錯了,即數據不一致。
在本章所討論的項目開始之前,因為之前的改造項目時間很緊,所以開發人員完全沒有精力處理系統數據一致性的問題,最終業務系統出現了很多錯誤數據,業務部門發工單告知IT部門數據有問題,經過一番檢查后,IT部門發現是因為分布式更新的原因導致了數據不一致。
此時,IT部門不得不抽出時間針對數據一致性問題給出一個可靠的解決方案。通過討論,IT部門把數據一致性的問題歸類為以下兩種情況。
1.實時數據不一致可以接受,但要保證數據的最終一致性
因為一些服務出現錯誤,導致圖13-1中的步驟3失敗,此時處理完請求后,數據就變成了a2、b2、c1,不過沒關系,只需保證最終數據是a2、b2、c2即可。
在以往的一個項目中,業務場景是這樣的(示例有所簡化):零售下單時,一般需要實現在商品服務中扣除商品的庫存、在訂單服務中生成一個訂單、在交易服務中生成一個交易單這3個步驟。假設交易單生成失敗,就會出現庫存扣除、訂單生成,但交易單沒有生成的情況,此時只需保證最終交易單成功生成即可,這就是最終一致性。
2.必須保證實時一致性
如果圖13-1中的步驟2和步驟3成功了,數據就會變成b2、c2,但是如果步驟3失敗,那么步驟1和步驟2會立即回滾,保證數據變回a1、b1。
在以往的一個項目中,業務場景類似這樣:用戶使用積分兌換折扣券時,需要實現扣除用戶積分、生成一張折扣券給用戶這兩個步驟。如果還是使用最終一致性方案的話,有可能出現用戶積分扣除而折扣券還未生成的情況,此時用戶進入賬戶發現積分沒有了,也沒有折扣券,就會馬上投訴。
那怎么辦呢?直接將前面的步驟回滾,并告知用戶處理失敗請繼續重試即可,這就是實時一致性。
針對以上兩種情況,具體解決方案是什么呢?下面一起來看看。
最終一致性方案
對于數據要求最終一致性的場景,實現思路是這樣的。
1)每個步驟完成后,生產一條消息給MQ,告知下一步處理接下來的數據。
2)消費者收到這條消息,將數據處理完成后,與步驟1)一樣觸發下一步。
3)消費者收到這條消息后,如果數據處理失敗,這條消息應該保留,直到消費者下次重試。
將3個服務的整個調用流程走下來,邏輯還是比較復雜的,整體流程如圖13-2所示。
• 圖13-2 服務調用流程
詳細的實現邏輯如下。
1)調用端調用Service A。
2)Service A將數據庫中的a1改為a2。
3)Service A生成一條步驟2(暫且命名為Step2)的消息給MQ。
4)Service A返回成功信息給調用端。
5)Service B監聽Step2的消息,獲得一條消息。
6)Service B將數據庫中的b1改為b2。
7)Service B生成一條步驟3(暫且命名為Step3)的消息給MQ。
8)Service B將Step2的消息設置為已消費。
9)Service C監聽Step3的消息,獲得一條消息。
10)Service C將數據庫中的c1改為c2。
11)Service C將Step3的消息設置為已消費。
接下來要考慮,如果每個步驟失敗了該怎么辦?
1)調用端調用Service A。
解決方案:直接返回失敗信息給用戶,用戶數據不受影響。
2)Service A將數據庫中的a1改為a2。
解決方案:如果這一步失敗,就利用本地事務數據直接回滾,用戶數據不受影響。
3)Service A生成一條步驟2)(Step2)的消息給MQ。
解決方案:如果這一步失敗,就利用本地事務數據將步驟2)直接回滾,用戶數據不受影響。
4)Service A返回成功信息給調用端。
解決方案:不用處理。
5)Service B監聽Step2的消息,獲得一條消息。
解決方案:如果這一步失敗,MQ有對應機制,無須擔心。
6)Service B將數據庫中的b1改為b2。
解決方案:如果這一步失敗,則利用本地事務直接將數據回滾,再利用消息重試的特性重新回到步驟5)。
7)Service B生成一條步驟3)(Step3)的消息給MQ。
解決方案:如果這一步失敗,MQ有生產消息失敗重試機制。若出現極端情況,服務器會直接崩潰,因為Step2的消息還沒有消費,MQ會有重試機制,然后找另一個消費者重新從步驟5)執行。
8)Service B將Step2的消息設置為已消費。
解決方案:如果這一步失敗,MQ會有重試機制,找另一個消費者重新從步驟5)執行。
9)Service C監聽Step3的消息,獲得一條消息。
解決方案:參考步驟5)的解決方案。
10)Service C將數據庫中的c1改為c2。
解決方案:參考步驟6)的解決方案。
11)Service C將Step3的消息設置為已消費。
解決方案:參考步驟8)的解決方案。
以上就是最終一致性的解決方案,這個方案還有兩個問題。
1)因為利用了MQ的重試機制,所以有可能出現步驟6)和步驟10)重復執行的情況,此時該怎么辦?比如,上面流程中的步驟8)如果失敗了,就會從步驟5)重新執行,這時就會出現步驟6)執行兩遍的情況。為此,在下游(步驟6)和步驟10))更新數據時,需要保證業務代碼的冪等性(關于冪等性,在第1章提過)。
2)如果每個業務流程都需要這樣處理,豈不是需要額外寫很多代碼?那是否可以將類似流程的重復代碼抽取出來?答案是可以,這里使用的MQ相關邏輯在其他業務流程中也通用,這個項目最終就是將這些代碼抽取出來并進行了封裝。因為重復代碼抽取的方法比較簡單,這里就不展開了。
實時一致性方案
實時一致性其實就是常說的分布式事務。
MySQL其實有一個兩階段提交的分布式事務方案MySQL XA,但是該方案存在嚴重的性能問題。比如,一個數據庫的事務與多個數據庫間的XA事務性能可能相差10倍。另外,XA的事務處理過程會長期占用鎖資源,所以項目組一開始就沒有考慮這個方案。
而當時比較流行的方案是使用TCC模式,下面簡單介紹一下。
TCC模式
在TCC模式中,會把原來的一個接口分為Try接口、Confirm接口、Cancel接口。
1)Try接口:用來檢查數據、預留業務資源。
2)Confirm接口:用來確認實際業務操作、更新業務資源。
3)Cancel接口:是指釋放Try接口中預留的資源。
比如在積分兌換折扣券的例子中,需要調用賬戶服務減積分(步驟1)、營銷服務加折扣券(步驟2)這兩個服務,那么針對賬戶服務減積分這個接口,需要寫3個方法,代碼如下所示。
同樣,針對營銷服務加折扣券這個接口,也需要寫3個方法,而后調用的大體步驟如圖13-3所示。
• 圖13-3 賬戶和營銷服務TCC處理流程
圖13-3中,除Cancel步驟以外的步驟,代表成功的調用路徑,如果中間出錯,則去調用相關服務的回退(Rollback)方法進行手工回退。該方案原來只需要在每個服務中寫一段業務代碼,而現在需要分成3段來寫,而且還涉及一些注意事項。
1)需要保證每個服務的Try方法執行成功后,Confirm方法在業務邏輯上能夠執行成功。
2)可能會出現Try方法執行失敗而Cancel被觸發的情況,此時需要保證正確回滾。
3)可能因為網絡擁堵而出現Try方法調用被堵塞的情況,此時事務控制器判斷Try失敗并觸發了Cancel方法,之后Try方法的調用請求到了服務這里,應該拒絕Try請求邏輯。
4)所有的Try、Confirm、Cancel都需要確保冪等性。
5)整個事務期間的數據庫數據處于一個臨時的狀態,其他請求需要訪問這些數據時,需要考慮如何正確被其他請求使用,而這種使用包括讀取和并發的修改。
所以,TCC模式是一個實施起來很麻煩的方案,除了每個業務代碼的工作量乘3之外,還需要通過相應邏輯應對上面的注意事項,這樣出錯的概率就太高了。
后來,筆者在一篇介紹Seata的文章中了解到AT模式也能解決這個問題。
Seata中AT模式的自動回滾
自動回滾對于使用Seata的人來說操作比較簡單,只需要在觸發整個事務的業務發起方的方法中加入@GlobalTransactional標注,并且使用普通的@Transactional包裝好分布式事務中相關服務的相關方法即可。
對于Seata的內在機制,AT模式的自動回滾往往需要執行以下步驟(分為3個階段)。
階段1
1)解析每個服務方法執行的SQL,記錄SQL的類型(Update、Insert或Delete),修改表并更新SQL條件等信息。
2)根據前面的條件信息生成查詢語句,并記錄修改前的數據鏡像。
3)執行業務的SQL。4)記錄修改后的數據鏡像。
5)插入回滾日志:把前后鏡像數據及業務SQL相關的信息組成一條回滾日志記錄,插入UNDOLOG表中。
6)提交前,向TC注冊分支,并申請相關修改數據行的全局鎖。
7)本地事務提交:業務數據的更新與前面步驟生成的UNDOLOG一并提交。
8)將本地事務提交的結果上報給事務控制器。
階段2
收到事務控制器的分支回滾請求后,開啟一個本地事務,執行如下操作。
1)查找相應的UNDOLOG記錄。
2)數據校驗:將UNDOLOG中的后鏡像數據與當前數據進行對比,如果存在不同,說明數據被當前全局事務之外的動作做了修改,此時需要根據配置策略進行處理。
3)根據UNDOLOG中的前鏡像數據和業務SQL的相關信息生成回滾語句并執行。
4)提交本地事務,并把本地事務的執行結果(即分支事務回滾的結果)上報事務控制器。
階段3
1)收到事務控制器的分支提交請求后,將請求放入一個異步任務隊列
中,并馬上返回提交成功的結果給事務控制器。
2)異步任務階段的分支提交請求將異步、批量地刪除相應的UNDOLOG記錄。
以上就是Seata AT模式的簡單介紹。
嘗試Seata
當時,雖然Seata還沒有更新到1.0,且官方也不推薦線上使用,但是項目組最終還是使用了它,原因如下。
1)因為實時一致性的場景很少,而且發生頻率低,所以并不會大規模使用,影響面在可控范圍內。如果實時一致性的場景發生頻率高,并發量就高,業務人員對性能的要求也高,此時就會與業務溝通,采用最終一致性的方案。
2)Seata AT模式與TCC模式相比,只有增加一個@GlobalTransactional的工作量,因此兩者的工作量相差很多,也就是說,對項目組來說,投入產出比更高,值得冒險。這可能也是Seata發展很快的原因之一。
雖然Seata AT模式有些小缺陷,但是瑕不掩瑜。
小結
最終一致性與實時一致性的解決方案設計完成后,不僅沒有給業務開發人員帶來額外工作量,也沒有影響業務項目進度的日常推進,還大大減少了數據不一致的出現概率,因此數據不一致的痛點得到了較大緩解。