相關源碼:https://github.com/SkyChenSky/Sikirohtml
很多小夥伴看了個人博客的後跟我探討問題時都離不開數據一致性、數據關聯、數據重複建立的問題,只要你們作的分佈式系統不管是否微服務化,或多或少都會遇到上述問題,而上述的問題的本質其實就是分佈式事務、分佈式數據關聯與冪等性。這三個問題也是不少面試官在面試的時候檢驗應聘者是否有實踐過度布式系統的經驗的標準之一,而微服務做爲分佈式系統的架構風格,在實施過程當中也沒法倖免以上問題。git
用微服務架構風格設計出來的系統是典型的分佈式系統。github
分佈式計算是指系統的工做方式,主要分爲數據分佈式和任務分佈式:面試
數據分佈式也稱爲數據並行,把數據拆分後,利用多臺計算機並行執行多個相同任務。優勢是縮短全部任務整體執行時間,缺點是沒法減小單個任務的執行時間。數據庫
任務分佈式也稱爲任務並行,單個串行的任務拆分紅多個可並行子任務。優勢是提升性能、可擴展性、可維護性,缺點是增長設計複雜性。服務器
方式網絡 |
描述架構 |
數據分佈式併發 |
利用多臺計算機並行執行多個相同任務負載均衡 |
任務分佈式 |
單個串行的任務拆分紅多個可並行子任務 |
咱們平常工做的時候 ,接觸到任務分佈式的狀況相對比較多例如:第三方支付請求,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是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,從實現上比前面兩個相對複雜,可是通用性相對高一些。
該篇到這裏就結束了,主要總結了日常在分佈式系統不得不去面對的問題,雖然你們會經過一些設計,儘量去避免,可是惟一不變的是需求的變化,所以咱們儘量優先了解各類處理方案,若有遇到就可針對場景選擇合適的方案。