.NET 微服務(wù)實(shí)戰(zhàn)之必須得面對的分布式問題

轉(zhuǎn)自:陳珙 cnblogs.com/skychen1218/p/14346459.html
前言
不少小伙伴看了我的博客的后跟我探討問題時都離不開數(shù)據(jù)一致性、數(shù)據(jù)關(guān)聯(lián)、數(shù)據(jù)重復(fù)創(chuàng)建的問題,只要大家做的分布式系統(tǒng)無論是否微服務(wù)化,或多或少都會遇到上述問題,而上述的問題的本質(zhì)其實(shí)就是分布式事務(wù)、分布式數(shù)據(jù)關(guān)聯(lián)與冪等性。
這三個問題也是很多面試官在面試的時候檢驗(yàn)應(yīng)聘者是否有實(shí)踐過分布式系統(tǒng)的經(jīng)驗(yàn)的標(biāo)準(zhǔn)之一,而微服務(wù)作為分布式系統(tǒng)的架構(gòu)風(fēng)格,在實(shí)施過程中也無法幸免以上問題。
源碼:https://github.com/SkyChenSky/Sikiro
分布式基礎(chǔ)概念
用微服務(wù)架構(gòu)風(fēng)格設(shè)計(jì)出來的系統(tǒng)是典型的分布式系統(tǒng)。
分布式計(jì)算是指系統(tǒng)的工作方式,主要分為數(shù)據(jù)分布式和任務(wù)分布式:
數(shù)據(jù)分布式也稱為數(shù)據(jù)并行,把數(shù)據(jù)拆分后,利用多臺計(jì)算機(jī)并行執(zhí)行多個相同任務(wù)。優(yōu)點(diǎn)是縮短所有任務(wù)總體執(zhí)行時間,缺點(diǎn)是無法減少單個任務(wù)的執(zhí)行時間。
任務(wù)分布式也稱為任務(wù)并行,單個串行的任務(wù)拆分成多個可并行子任務(wù)。優(yōu)點(diǎn)是提高性能、可擴(kuò)展性、可維護(hù)性,缺點(diǎn)是增加設(shè)計(jì)復(fù)雜性。

分布式系統(tǒng)必須面臨的哪些問題?
我們?nèi)粘9ぷ鞯臅r候 ,接觸到任務(wù)分布式的情況相對比較多例如:第三方支付請求,API編排數(shù)據(jù)關(guān)聯(lián)。從場景劃分主要分為單服務(wù)多數(shù)據(jù)庫,多服務(wù)多數(shù)據(jù)庫,多服務(wù)單數(shù)據(jù)庫,以上三種場景都會存在多臺服務(wù)器之間跨網(wǎng)絡(luò)調(diào)用的情況,由原單進(jìn)程單數(shù)據(jù)庫內(nèi)的簡單實(shí)現(xiàn)的原子性、一致性變得不得不去面對因?yàn)榭缇W(wǎng)絡(luò)請求得冪等性和數(shù)據(jù)一致性。
數(shù)據(jù)庫一致性又分讀和寫,讀對應(yīng)著數(shù)據(jù)庫跨庫跨服務(wù)器的數(shù)據(jù)關(guān)聯(lián),寫對應(yīng)著分布式事務(wù)的數(shù)據(jù)最終一致性的處理。
數(shù)據(jù)關(guān)聯(lián)的復(fù)雜度場景主要體現(xiàn)在分庫分服務(wù)器與多接口數(shù)據(jù)關(guān)聯(lián)的場景應(yīng)該怎么解決?
分布式事務(wù)如果在單服務(wù)多數(shù)據(jù)庫的場景下想必大家都會想出像Sql Sever的MSDTC的XA協(xié)議事務(wù)。如果是在多服務(wù)多數(shù)據(jù)庫該選用怎樣的分布式事務(wù)方案?
在分布式場景下冪等性的保證是無法避免的,網(wǎng)絡(luò)是存在不確定性的,一個請求可能會成功,但也會因?yàn)榭陀^因素導(dǎo)致失敗,那么重新發(fā)起請求就無發(fā)避免的了,那么如何保證我不會重復(fù)創(chuàng)建數(shù)據(jù)與數(shù)據(jù)被覆蓋呢?
下文我將從數(shù)據(jù)關(guān)聯(lián),分布式事務(wù)和冪等性三個角度進(jìn)行敘述方案。
數(shù)據(jù)關(guān)聯(lián)
數(shù)據(jù)關(guān)聯(lián)的主要方案有三種,應(yīng)用層數(shù)據(jù)聚合、冗余設(shè)計(jì)(反范式)、數(shù)據(jù)庫從庫集成。

舉個常見的例子:分布式情況下,比如現(xiàn)在有兩個服務(wù),分別是用戶,訂單。每個服務(wù)都是自己獨(dú)立的數(shù)據(jù)庫。用戶數(shù)據(jù)庫有用戶信息表,訂單數(shù)據(jù)都有關(guān)聯(lián)用戶的唯一id。

應(yīng)用層數(shù)據(jù)聚合
先調(diào)用訂單服務(wù)得到訂單列表后,再根據(jù)訂單列表的用戶ID集合調(diào)一次用戶服務(wù)查詢出用戶列表。再通過內(nèi)存遍歷把訂單列表與用戶列表在業(yè)務(wù)層整合。
優(yōu)點(diǎn),實(shí)現(xiàn)簡單;缺點(diǎn),也是簡單,該方案只能適合簡單的查詢過濾,以主表為驅(qū)動的關(guān)聯(lián)。
public async Task> GetOrder()
{
//訂單集合
var orderList = await _order.GetList();
//userId集合
var userIds = orderList.Select(a => a.UserId).ToList();
//關(guān)聯(lián)用戶集合
var users = await _user.GetByIds(userIds);
//應(yīng)用層數(shù)據(jù)聚合關(guān)聯(lián)
orderList.ForEach(order =>
{
order.Name = users.FirstOrDefault(a => a.UserId == order.UserId)?.Name;
});
return orderList;
}
冗余設(shè)計(jì)(反范式)
在訂單表增加和用戶有關(guān)信息的字段。
優(yōu)點(diǎn),實(shí)現(xiàn)簡單,以應(yīng)用層數(shù)據(jù)聚合方案有更多的過濾條件;缺點(diǎn),冗余的字段如果更新存在同步問題,該方案適用于更新頻繁少的遞增日志類數(shù)據(jù)。

數(shù)據(jù)庫從庫集成
通過主從同步技術(shù),把相關(guān)的業(yè)務(wù)表同步到同一臺服務(wù)器我們稱為ReportDB,再通過在代碼層面把數(shù)據(jù)源連接指向從庫做跨庫聯(lián)表查詢處理。
優(yōu)點(diǎn),通過強(qiáng)大的SQL解決復(fù)雜的報(bào)表類查詢;缺點(diǎn),擁有技術(shù)復(fù)雜度,需要數(shù)據(jù)庫主從處理。

分布式事務(wù)
分布式事務(wù)分剛性事務(wù)與柔性事務(wù),剛性事務(wù)對應(yīng)ACID理論,而柔性事務(wù)也就是最終一致性,對應(yīng)BASE理論。最終一致性指如果數(shù)據(jù)再一段時間內(nèi)沒有被另外的數(shù)據(jù)操作所更改,那它最終會達(dá)到與強(qiáng)一致性過程相同的結(jié)果。
分布式系統(tǒng)場景下很少使用xa事務(wù),主要原因是xa事務(wù)是基于基礎(chǔ)設(shè)施層面的強(qiáng)一致性事務(wù),場景主要在一個服務(wù)多個數(shù)據(jù)源,追求強(qiáng)一致性,復(fù)雜度高,吞吐量低。
而最終一致性方案更多是基于服務(wù)應(yīng)用層的弱一致性事務(wù),場景主要是多服務(wù)多數(shù)據(jù)源與多服務(wù)單數(shù)據(jù)源,滿足了BASE理論的三個特點(diǎn):基本可用、軟狀態(tài)、最終一致性
以訂單支付為例講述下BASE理論,客戶在A平臺發(fā)起了訂單支付,訂單支付時狀態(tài)為支付中,完成后支付后,等待支付系統(tǒng)的回調(diào),但是這個時候,A平臺的回調(diào)API接口異常了,訂單狀態(tài)無法同步為已支付狀態(tài),這個時候客戶看到訂單的金額支付出去了,但是去搜索訂單模塊的時候發(fā)現(xiàn)還是未支付,于是反饋給了客服,開發(fā)部經(jīng)過一段時間的問題定位與排查,發(fā)現(xiàn)是回調(diào)API掛了于是重啟后,數(shù)分鐘訂單狀態(tài)就同步成已完成了。

從上面的例子來看,支付中就是軟狀態(tài),回調(diào)API服務(wù)雖然掛了,但是前臺系統(tǒng)還是可以提供給客戶端查詢使用就是基本可用,只不過訂單狀態(tài)不對,當(dāng)然最后服務(wù)也恢復(fù)后達(dá)成數(shù)據(jù)最終一致性。

分布式事務(wù)方案常見的主要有這幾種:異步請求/回調(diào)、TCC、基于消息可靠的最終一致性,TCC與基于消息可靠的最終一致性在Java和.Net都是有現(xiàn)成的框架,而異步請求/回調(diào)更多是與支付機(jī)構(gòu)對接的場景會比較多,實(shí)現(xiàn)簡單、通用性強(qiáng),如果團(tuán)隊(duì)技術(shù)能力不足也可以使用該方案代替。
異步請求/回調(diào)更多是應(yīng)對并發(fā)處理的異步解決方案,查過相關(guān)資料并沒有納入相關(guān)分布式事務(wù)方案中,但是在我的實(shí)際工作經(jīng)驗(yàn)中該方案也是可以達(dá)成最終一致性。
異步請求/回調(diào)

該方案在與支付機(jī)構(gòu)對接的場景比較常見,其核心以業(yè)務(wù)發(fā)起請求,被調(diào)用端以數(shù)據(jù)優(yōu)先入庫,稍后異步處理,處理完成后則回調(diào)請求業(yè)務(wù)端提供的API。
這種異步處理方式一般獲取結(jié)果的方式推拉結(jié)合,外部系統(tǒng)主動回調(diào)給本地稱之為推,本地系統(tǒng)每隔一段時間主動查詢外部系統(tǒng)結(jié)果稱為拉,兩者可以按照業(yè)務(wù)的時效性結(jié)合策略使用。
公司內(nèi)部系統(tǒng)之間也可以這么做,業(yè)務(wù)系統(tǒng)請求對接系統(tǒng),被請求后數(shù)據(jù)庫直接入庫,然后通過定時調(diào)度任務(wù)異步做業(yè)務(wù)處理,業(yè)務(wù)處理成功還是失敗都修改狀態(tài),最后由回調(diào)調(diào)度任務(wù)把業(yè)務(wù)處理的狀態(tài)、處理信息回調(diào)給業(yè)務(wù)系統(tǒng)的回調(diào)API,為了避免回調(diào)調(diào)度任務(wù)因故障無法回調(diào),可以設(shè)置策略由業(yè)務(wù)系統(tǒng)主動查詢對接系統(tǒng)提供的查詢API,推拉結(jié)合保證了系統(tǒng)可用性和數(shù)據(jù)時效性。
TCC

TCC是Try、Comfirm、Cancel三個單詞的縮寫,Try是資源預(yù)留、鎖定,Comfirm是確認(rèn)提交,Cancel是指撤銷。一個資源的處理需要提供三個接口,從業(yè)務(wù)侵入性來看是比較強(qiáng)的。
TCC的執(zhí)行步驟與2PC有點(diǎn)相似,先進(jìn)入預(yù)提交階段,對A、B、C三個資源的分別進(jìn)行try處理,如果try請求成功,相應(yīng)的資源就會被修改成中間狀態(tài),可以理解成被凍結(jié)。接下來就會根據(jù)每個資源try后的情況判斷如何執(zhí)行。如果全部try成功,則會進(jìn)入Comfirm處理,只要能try成功就能Comfirm成功。如果其中一個資源try失敗了,則會對所有進(jìn)行Cancel處理。
TCC與2PC看起來相似,但還是有區(qū)別的,TCC是應(yīng)用服務(wù)層面的,而2PC則是基礎(chǔ)設(shè)施層,而2PC因?yàn)槭菑?qiáng)一致性基于遵守ACID,在事務(wù)未提交時處于阻塞狀態(tài),如果失敗則會事務(wù)回滾,而TCC是沒有事務(wù)回滾的,每個階段處理都穿透到數(shù)據(jù)庫都是Commit操作。
基于消息的最終一致性

該方案其實(shí)是ebay多年前提出的本地消息表的解決方案,該方案的核心點(diǎn)在于,執(zhí)行本地事務(wù)后再提交隊(duì)列消息,這兩步驟操作因?yàn)榉窃有缘目邕M(jìn)程操作,因?yàn)樾枰WC發(fā)送到消息隊(duì)列的消息能正常發(fā)布與正常的消費(fèi),這就是我們常說的保證消息可靠,那么在執(zhí)行本地事務(wù)的時候,本地業(yè)務(wù)表與消息憑據(jù)表會作為一個原子性事務(wù)提交到數(shù)據(jù)庫,消息憑據(jù)表會記錄著消息隊(duì)列的消息序列化數(shù)據(jù),如果本地事務(wù)提交成功了,但是發(fā)送消息隊(duì)列的時候失敗了,就會通過后臺線程(進(jìn)程)查詢消息憑據(jù)表,把未發(fā)送成功的消息反序列化出來重新發(fā)起。
無論再消息發(fā)布端還是消息消費(fèi)端都會因?yàn)榕c消息隊(duì)列交互后,修改消息憑據(jù)表狀態(tài)的情況,如果與消息隊(duì)列交互是正常的,但是修改消息憑據(jù)狀態(tài)失敗了,補(bǔ)償服務(wù)仍然會進(jìn)行不必要的重發(fā),那么這個場景容易導(dǎo)致數(shù)據(jù)重復(fù)創(chuàng)建與覆蓋,因此需要關(guān)注冪等性的處理了。
該方案在.Net有CAP這個分布式事務(wù)框架,無需開發(fā)人員自己自己實(shí)現(xiàn)。
冪等性
冪等性的定義,相同的參數(shù)在同一個方法里,無論執(zhí)行一次還是多次都會響應(yīng)相同的結(jié)果
舉個例子銀行轉(zhuǎn)行,A銀行賬戶扣了100元,B銀行賬戶加100元,這樣數(shù)據(jù)一致的。但是在給B賬戶加100元的時候,B銀行系統(tǒng)處理超時,但是其實(shí)這個時候B銀行是已經(jīng)處理成功了,只不過沒響應(yīng)回去,那么A銀行系統(tǒng)就會重發(fā),如果沒有冪等性處理的話,A重試了3次,B賬戶就會加3次100。一邊扣100,一邊加300,那么數(shù)據(jù)就不一致了。
對于查詢和刪除數(shù)據(jù)的場景都有天然的冪等性,那么我們考慮冪等性處理更多是關(guān)注于新建數(shù)據(jù)與更新數(shù)據(jù)。
新建數(shù)據(jù)的場景,如果沒有處理好冪等性,那么就會導(dǎo)致數(shù)據(jù)重復(fù)創(chuàng)建,原因有可能是用戶連續(xù)點(diǎn)擊后發(fā)起請求,也有可能是API網(wǎng)關(guān)的retry請求。解決方案也相對比較簡單,API提供主鍵參數(shù)(流水號)傳入,就是由調(diào)用端預(yù)生成主鍵(流水號)傳入API進(jìn)行請求,API端生成流水與余額扣減作為同一個事務(wù)處理。此時如果因?yàn)槟硞€原因進(jìn)行了兩次調(diào)用,因?yàn)榈谝淮蝿?chuàng)建成功了,第二次則會因?yàn)橹麈I的唯一性拋出了異常,這里需要注意的是得捕獲到的唯一鍵異常應(yīng)處理成執(zhí)行成功的響應(yīng)。
更新數(shù)據(jù)的場景,如果沒處理號冪等性,可能會因?yàn)镽PC框架或者API網(wǎng)關(guān)的Retry機(jī)制導(dǎo)致重復(fù)請求,這樣就會造成了ABA的數(shù)據(jù)覆蓋問題,所謂的ABA就是,第一次請求A數(shù)據(jù)已經(jīng)進(jìn)行寫處理了,接著到了第二次請求B數(shù)據(jù)進(jìn)行對A數(shù)據(jù)進(jìn)行了修改成功了,但是因?yàn)榈谝淮握埱笠驗(yàn)槟硞€原因?qū)е驴蛻舳藷o法接收到響應(yīng),因此API網(wǎng)關(guān)或者RPC框架進(jìn)行了重發(fā),所以第三次把A數(shù)據(jù)又對已有的B數(shù)據(jù)進(jìn)行修改覆蓋。針對該問題解決方案主要是使用數(shù)據(jù)版本判斷。

以上兩種方法處理方式從數(shù)據(jù)庫層面解決,相對比較簡單直接,侵入性比較強(qiáng),還有一種方案可以從Web框架層面解決,結(jié)合Web框架的AOP與Redis判斷,每次請求都會附帶一個requestID傳入到接口,由Filter攔截后Add到Redis。此方案需要引入Redis,從實(shí)現(xiàn)上比前面兩個相對復(fù)雜,但是通用性相對高一些。
結(jié)束
該篇到這里就結(jié)束了,主要總結(jié)了平常在分布式系統(tǒng)不得不去面對的問題,雖然大家會通過一些設(shè)計(jì),盡可能去避免,但是唯一不變的是需求的變化,因此我們盡可能優(yōu)先了解各種處理方案,如有遇到就可針對場景選擇合適的方案。
【推薦】.NET Core開發(fā)實(shí)戰(zhàn)視頻課程?★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門篇-開篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個接口多種實(shí)現(xiàn)的依賴注入與動態(tài)選擇看這篇就夠了
用abp vNext快速開發(fā)Quartz.NET定時任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
