系列文章
- .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,從實現上比前面兩個相對複雜,但是通用性相對高一些。
結束
該篇到這裡就結束了,主要總結了平常在分散式系統不得不去面對的問題,雖然大家會通過一些設計,儘可能去避免,但是唯一不變的是需求的變化,因此我們儘可能優先了解各種處理方案,如有遇到就可針對場景選擇合適的方案。