系列文章
- .Net微服務實戰之技術選型篇
- .Net微服務實戰之技術架構分層篇
- .Net微服務實戰之DevOps篇
- .Net微服務實戰之負載均衡(上)
- .Net微服務實戰之CI/CD
- .Net微服務實戰之Kubernetes的搭建與使用
- .Net微服務實戰之負載均衡(下)
相關源碼:https://github.com/SkyChenSky/Sikiro
前言
不少小伙伴看了我的博客的后跟我探討問題時都離不開數據一致性、數據關聯、數據重復創建的問題,只要大家做的分布式系統無論是否微服務化,或多或少都會遇到上述問題,而上述的問題的本質其實就是分布式事務、分布式數據關聯與冪等性。這三個問題也是很多面試官在面試的時候檢驗應聘者是否有實踐過分布式系統的經驗的標准之一,而微服務作為分布式系統的架構風格,在實施過程中也無法幸免以上問題。
PS:評論可能比正文更精彩
分布式基礎概念
用微服務架構風格設計出來的系統是典型的分布式系統。
分布式計算是指系統的工作方式,主要分為數據分布式和任務分布式:
數據分布式也稱為數據並行,把數據拆分后,利用多台計算機並行執行多個相同任務。優點是縮短所有任務總體執行時間,缺點是無法減少單個任務的執行時間。
任務分布式也稱為任務並行,單個串行的任務拆分成多個可並行子任務。優點是提高性能、可擴展性、可維護性,缺點是增加設計復雜性。
方式 |
描述 |
數據分布式 |
利用多台計算機並行執行多個相同任務 |
任務分布式 |
單個串行的任務拆分成多個可並行子任務 |
分布式系統必須面臨的哪些問題?
我們日常工作的時候 ,接觸到任務分布式的情況相對比較多例如:第三方支付請求,API編排數據關聯。從場景划分主要分為單服務多數據庫,多服務多數據庫,多服務單數據庫,以上三種場景都會存在多台服務器之間跨網絡調用的情況,由原單進程單數據庫內的簡單實現的原子性、一致性變得不得不去面對因為跨網絡請求得冪等性和數據一致性。
數據庫一致性又分讀和寫,讀對應着數據庫跨庫跨服務器的數據關聯,寫對應着分布式事務的數據最終一致性的處理。
數據關聯的復雜度場景主要體現在分庫分服務器與多接口數據關聯的場景應該怎么解決?
分布式事務如果在單服務多數據庫的場景下想必大家都會想出像Sql Sever的MSDTC的XA協議事務。如果是在多服務多數據庫該選用怎樣的分布式事務方案?
在分布式場景下冪等性的保證是無法避免的,網絡是存在不確定性的,一個請求可能會成功,但也會因為客觀因素導致失敗,那么重新發起請求就無發避免的了,那么如何保證我不會重復創建數據與數據被覆蓋呢?
下文我將從數據關聯,分布式事務和冪等性三個角度進行敘述方案。
數據關聯
數據關聯的主要方案有三種,應用層數據聚合、冗余設計(反范式)、數據庫從庫集成。
方案名稱 |
方案描述 |
應用層數據聚合 |
分別調用查詢API,在業務邏輯層組裝,適用於簡單的關聯。 |
冗余設計(反范式) |
在目標表添加冗余字段,適用於記錄遞增的,不適用於冗余字段更新頻繁,實現起來簡單,有擴展性問題 |
數據庫從庫集成 |
通過主從同步把相關表同步到一台服務器做跨庫查詢,適用於復雜查詢、報表類的,有技術復雜度,從長遠收益來看能應對多種場景 |
舉個常見的例子:分布式情況下,比如現在有兩個服務,分別是用戶,訂單。每個服務都是自己獨立的數據庫。用戶數據庫有用戶信息表,訂單數據都有關聯用戶的唯一id。
應用層數據聚合:
先調用訂單服務得到訂單列表后,再根據訂單列表的用戶ID集合調一次用戶服務查詢出用戶列表。再通過內存遍歷把訂單列表與用戶列表在業務層整合。
優點,實現簡單;缺點,也是簡單,該方案只能適合簡單的查詢過濾,以主表為驅動的關聯。
public async Task<List<Order>> GetOrder() { //訂單集合 var orderList = await _order.GetList(); //userId集合 var userIds = orderList.Select(a => a.UserId).ToList(); //關聯用戶集合 var users = await _user.GetByIds(userIds); //應用層數據聚合關聯 orderList.ForEach(order => { order.Name = users.FirstOrDefault(a => a.UserId == order.UserId)?.Name; }); return orderList; }
冗余設計(反范式):
在訂單表增加和用戶有關信息的字段。
優點,實現簡單,以應用層數據聚合方案有更多的過濾條件;缺點,冗余的字段如果更新存在同步問題,該方案適用於更新頻繁少的遞增日志類數據。
數據庫從庫集成:
通過主從同步技術,把相關的業務表同步到同一台服務器我們稱為ReportDB,再通過在代碼層面把數據源連接指向從庫做跨庫聯表查詢處理。
優點,通過強大的SQL解決復雜的報表類查詢;缺點,擁有技術復雜度,需要數據庫主從處理。
分布式事務
分布式事務分剛性事務與柔性事務,剛性事務對應ACID理論,而柔性事務也就是最終一致性,對應BASE理論。最終一致性指如果數據再一段時間內沒有被另外的數據操作所更改,那它最終會達到與強一致性過程相同的結果。
分布式系統場景下很少使用xa事務,主要原因是xa事務是基於基礎設施層面的強一致性事務,場景主要在一個服務多個數據源,追求強一致性,復雜度高,吞吐量低。
而最終一致性方案更多是基於服務應用層的弱一致性事務,場景主要是多服務多數據源與多服務單數據源,滿足了BASE理論的三個特點:基本可用、軟狀態、最終一致性
以訂單支付為例講述下BASE理論,客戶在A平台發起了訂單支付,訂單支付時狀態為支付中,完成后支付后,等待支付系統的回調,但是這個時候,A平台的回調API接口異常了,訂單狀態無法同步為已支付狀態,這個時候客戶看到訂單的金額支付出去了,但是去搜索訂單模塊的時候發現還是未支付,於是反饋給了客服,開發部經過一段時間的問題定位與排查,發現是回調API掛了於是重啟后,數分鍾訂單狀態就同步成已完成了。
BASE理論 | |
基本可用(Basically Available) | 分布式系統在出現不可預知故障時,允許損失部分可用性 |
軟狀態(Soft state) | 允許系統中的數據存在中間狀態,並認為該中間狀態的存在不會影響系統的整體可用性 |
最終一致性(Eventually consistent) | 系統中所有的數據副本,在經過一段時間的同步后,最終能夠達到一個一致的狀態 |
從上面的例子來看,支付中就是軟狀態,回調API服務雖然掛了,但是前台系統還是可以提供給客戶端查詢使用就是基本可用,只不過訂單狀態不對,當然最后服務也恢復后達成數據最終一致性。
分布式數據一致性方案 |
|||
名稱 |
場景 |
優點 |
缺點 |
異步請求/回調 |
跨網絡環境、同網絡環境 |
實現簡單 |
強業務 |
TCC |
跨網絡環境、同網絡環境 |
有現成的框架、實現簡單 |
強業務 |
基於消息可靠的最終一致性 |
同網絡環境 |
有現成的框架、通用性強 |
中間件依賴 |
分布式事務方案常見的主要有這幾種:異步請求/回調、TCC、基於消息可靠的最終一致性,TCC與基於消息可靠的最終一致性在Java和.Net都是有現成的框架,而異步請求/回調更多是與支付機構對接的場景會比較多,實現簡單、通用性強,如果團隊技術能力不足也可以使用該方案代替。
異步請求/回調更多是應對並發處理的異步解決方案,查過相關資料並沒有納入相關分布式事務方案中,但是在我的實際工作經驗中該方案也是可以達成最終一致性。
異步請求/回調
該方案在與支付機構對接的場景比較常見,其核心以業務發起請求,被調用端以數據優先入庫,稍后異步處理,處理完成后則回調請求業務端提供的API。
這種異步處理方式一般獲取結果的方式推拉結合,外部系統主動回調給本地稱之為推,本地系統每隔一段時間主動查詢外部系統結果稱為拉,兩者可以按照業務的時效性結合策略使用。
公司內部系統之間也可以這么做,業務系統請求對接系統,被請求后數據庫直接入庫,然后通過定時調度任務異步做業務處理,業務處理成功還是失敗都修改狀態,最后由回調調度任務把業務處理的狀態、處理信息回調給業務系統的回調API,為了避免回調調度任務因故障無法回調,可以設置策略由業務系統主動查詢對接系統提供的查詢API,推拉結合保證了系統可用性和數據時效性。
TCC
TCC是Try、Comfirm、Cancel三個單詞的縮寫,Try是資源預留、鎖定,Comfirm是確認提交,Cancel是指撤銷。 一個資源的處理需要提供三個接口,從業務侵入性來看是比較強的。
TCC的執行步驟與2PC有點相似,先進入預提交階段,對A、B、C三個資源的分別進行try處理,如果try請求成功,相應的資源就會被修改成中間狀態,可以理解成被凍結。接下來就會根據每個資源try后的情況判斷如何執行。如果全部try成功,則會進入Comfirm處理,只要能try成功就能Comfirm成功。如果其中一個資源try失敗了,則會對所有進行Cancel處理。
TCC與2PC看起來相似,但還是有區別的,TCC是應用服務層面的,而2PC則是基礎設施層,而2PC因為是強一致性基於遵守ACID,在事務未提交時處於阻塞狀態,如果失敗則會事務回滾,而TCC是沒有事務回滾的,每個階段處理都穿透到數據庫都是Commit操作。
基於消息的最終一致性
該方案其實是ebay多年前提出的本地消息表的解決方案,該方案的核心點在於,執行本地事務后再提交隊列消息,這兩步驟操作因為非原子性的跨進程操作,因為需要保證發送到消息隊列的消息能正常發布與正常的消費,這就是我們常說的保證消息可靠,那么在執行本地事務的時候,本地業務表與消息憑據表會作為一個原子性事務提交到數據庫,消息憑據表會記錄着消息隊列的消息序列化數據,如果本地事務提交成功了,但是發送消息隊列的時候失敗了,就會通過后台線程(進程)查詢消息憑據表,把未發送成功的消息反序列化出來重新發起。
無論再消息發布端還是消息消費端都會因為與消息隊列交互后,修改消息憑據表狀態的情況,如果與消息隊列交互是正常的,但是修改消息憑據狀態失敗了,補償服務仍然會進行不必要的重發,那么這個場景容易導致數據重復創建與覆蓋,因此需要關注冪等性的處理了。
該方案在.Net有CAP這個分布式事務框架,無需開發人員自己自己實現。
冪等性
冪等性的定義,相同的參數在同一個方法里,無論執行一次還是多次都會響應相同的結果
舉個例子銀行轉行,A銀行賬戶扣了100元,B銀行賬戶加100元,這樣數據一致的。但是在給B賬戶加100元的時候,B銀行系統處理超時,但是其實這個時候B銀行是已經處理成功了,只不過沒響應回去,那么A銀行系統就會重發,如果沒有冪等性處理的話,A重試了3次,B賬戶就會加3次100。一邊扣100,一邊加300,那么數據就不一致了。
對於查詢和刪除數據的場景都有天然的冪等性,那么我們考慮冪等性處理更多是關注於新建數據與更新數據。
新建數據的場景,如果沒有處理好冪等性,那么就會導致數據重復創建,原因有可能是用戶連續點擊后發起請求,也有可能是API網關的retry請求。解決方案也相對比較簡單,API提供主鍵參數(流水號)傳入,就是由調用端預生成主鍵(流水號)傳入API進行請求,API端生成流水與余額扣減作為同一個事務處理。此時如果因為某個原因進行了兩次調用,因為第一次創建成功了,第二次則會因為主鍵的唯一性拋出了異常,這里需要注意的是得捕獲到的唯一鍵異常應處理成執行成功的響應。
更新數據的場景,如果沒處理號冪等性,可能會因為RPC框架或者API網關的Retry機制導致重復請求,這樣就會造成了ABA的數據覆蓋問題,所謂的ABA就是,第一次請求A數據已經進行寫處理了,接着到了第二次請求B數據進行對A數據進行了修改成功了,但是因為第一次請求因為某個原因導致客戶端無法接收到響應,因此API網關或者RPC框架進行了重發,所以第三次把A數據又對已有的B數據進行修改覆蓋。針對該問題解決方案主要是使用數據版本判斷。
冪等性處理方案 |
||
場景 |
問題 |
方案 |
新建數據 |
重復創建 |
由調用端預生成訂單號,唯一鍵約束 |
更新數據 |
ABA覆蓋問題 |
添加版本號判斷 |
以上兩種方法處理方式從數據庫層面解決,相對比較簡單直接,侵入性比較強,還有一種方案可以從Web框架層面解決,結合Web框架的AOP與Redis判斷,每次請求都會附帶一個requestID傳入到接口,由Filter攔截后Add到Redis。此方案需要引入Redis,從實現上比前面兩個相對復雜,但是通用性相對高一些。
結束
該篇到這里就結束了,主要總結了平常在分布式系統不得不去面對的問題,雖然大家會通過一些設計,盡可能去避免,但是唯一不變的是需求的變化,因此我們盡可能優先了解各種處理方案,如有遇到就可針對場景選擇合適的方案。