引用:http://www.itdecent.cn/p/6f5662908dae
鏈接:http://www.itdecent.cn/p/ebfc51ea1fe3
垂直分表
垂直分表在日常開(kāi)發(fā)和設(shè)計(jì)中比較常見(jiàn),通俗的說(shuō)法叫做“大表拆小表”,拆分是基于關(guān)系型數(shù)據(jù)庫(kù)中的“列”(字段)進(jìn)行的。通常情況,某個(gè)表中的字段比較多,可以新建立一張“擴(kuò)展表”,將不經(jīng)常使用或者長(zhǎng)度較大的字段拆分出去放到“擴(kuò)展表”中,如下圖所示:

在字段很多的情況下,拆分開(kāi)確實(shí)更便于開(kāi)發(fā)和維護(hù)(筆者曾見(jiàn)過(guò)某個(gè)遺留系統(tǒng)中,一個(gè)大表中包含100多列的)。某種意義上也能避免“跨頁(yè)”的問(wèn)題(MySQL、MSSQL底層都是通過(guò)“數(shù)據(jù)頁(yè)”來(lái)存儲(chǔ)的,“跨頁(yè)”問(wèn)題可能會(huì)造成額外的性能開(kāi)銷(xiāo),這里不展開(kāi),感興趣的朋友可以自行查閱相關(guān)資料進(jìn)行研究)。
拆分字段的操作建議在數(shù)據(jù)庫(kù)設(shè)計(jì)階段就做好。如果是在發(fā)展過(guò)程中拆分,則需要改寫(xiě)以前的查詢(xún)語(yǔ)句,會(huì)額外帶來(lái)一定的成本和風(fēng)險(xiǎn),建議謹(jǐn)慎。
垂直分庫(kù)
垂直分庫(kù)在“微服務(wù)”盛行的今天已經(jīng)非常普及了。基本的思路就是按照業(yè)務(wù)模塊來(lái)劃分出不同的數(shù)據(jù)庫(kù),而不是像早期一樣將所有的數(shù)據(jù)表都放到同一個(gè)數(shù)據(jù)庫(kù)中。如下圖:

系統(tǒng)層面的“服務(wù)化”拆分操作,能夠解決業(yè)務(wù)系統(tǒng)層面的耦合和性能瓶頸,有利于系統(tǒng)的擴(kuò)展維護(hù)。而數(shù)據(jù)庫(kù)層面的拆分,道理也是相通的。與服務(wù)的“治理”和“降級(jí)”機(jī)制類(lèi)似,我們也能對(duì)不同業(yè)務(wù)類(lèi)型的數(shù)據(jù)進(jìn)行“分級(jí)”管理、維護(hù)、監(jiān)控、擴(kuò)展等。
眾所周知,數(shù)據(jù)庫(kù)往往最容易成為應(yīng)用系統(tǒng)的瓶頸,而數(shù)據(jù)庫(kù)本身屬于“有狀態(tài)”的,相對(duì)于Web和應(yīng)用服務(wù)器來(lái)講,是比較難實(shí)現(xiàn)“橫向擴(kuò)展”的。數(shù)據(jù)庫(kù)的連接資源比較寶貴且單機(jī)處理能力也有限,在高并發(fā)場(chǎng)景下,垂直分庫(kù)一定程度上能夠突破IO、連接數(shù)及單機(jī)硬件資源的瓶頸,是大型分布式系統(tǒng)中優(yōu)化數(shù)據(jù)庫(kù)架構(gòu)的重要手段。
然后,很多人并沒(méi)有從根本上搞清楚為什么要拆分,也沒(méi)有掌握拆分的原則和技巧,只是一味的模仿大廠的做法。導(dǎo)致拆分后遇到很多問(wèn)題(例如:跨庫(kù)join,分布式事務(wù)等)。
水平分表
水平分表也稱(chēng)為橫向分表,比較容易理解,就是將表中不同的數(shù)據(jù)行按照一定規(guī)律分布到不同的數(shù)據(jù)庫(kù)表中(這些表保存在同一個(gè)數(shù)據(jù)庫(kù)中),這樣來(lái)降低單表數(shù)據(jù)量,優(yōu)化查詢(xún)性能。最常見(jiàn)的方式就是通過(guò)主鍵或者時(shí)間等字段進(jìn)行Hash和取模后拆分。如下圖所示:

水平分表,能夠降低單表的數(shù)據(jù)量,一定程度上可以緩解查詢(xún)性能瓶頸。但本質(zhì)上這些表還保存在同一個(gè)庫(kù)中,所以庫(kù)級(jí)別還是會(huì)有IO瓶頸。所以,一般不建議采用這種做法。
水平分庫(kù)分表
水平分庫(kù)分表與上面講到的水平分表的思想相同,唯一不同的就是將這些拆分出來(lái)的表保存在不同的數(shù)據(jù)中。這也是很多大型互聯(lián)網(wǎng)公司所選擇的做法。如下圖:

某種意義上來(lái)講,有些系統(tǒng)中使用的“冷熱數(shù)據(jù)分離”(將一些使用較少的歷史數(shù)據(jù)遷移到其他的數(shù)據(jù)庫(kù)中。而在業(yè)務(wù)功能上,通常默認(rèn)只提供熱點(diǎn)數(shù)據(jù)的查詢(xún)),也是類(lèi)似的實(shí)踐。在高并發(fā)和海量數(shù)據(jù)的場(chǎng)景下,分庫(kù)分表能夠有效緩解單機(jī)和單庫(kù)的性能瓶頸和壓力,突破IO、連接數(shù)、硬件資源的瓶頸。當(dāng)然,投入的硬件成本也會(huì)更高。同時(shí),這也會(huì)帶來(lái)一些復(fù)雜的技術(shù)問(wèn)題和挑戰(zhàn)(例如:跨分片的復(fù)雜查詢(xún),跨分片事務(wù)等)
垂直分庫(kù)帶來(lái)的的難點(diǎn)
跨庫(kù)join的問(wèn)題
在拆分之前,系統(tǒng)中很多列表和詳情頁(yè)所需的數(shù)據(jù)是可以通過(guò)sql join來(lái)完成的。而拆分后,數(shù)據(jù)庫(kù)可能是分布式在不同實(shí)例和不同的主機(jī)上,join將變得非常麻煩。而且基于架構(gòu)規(guī)范,性能,安全性等方面考慮,一般是禁止跨庫(kù)join的。那該怎么辦呢?首先要考慮下垂直分庫(kù)的設(shè)計(jì)問(wèn)題,如果可以調(diào)整,那就優(yōu)先調(diào)整。如果無(wú)法調(diào)整的情況,下面筆者將結(jié)合以往的實(shí)際經(jīng)驗(yàn),總結(jié)幾種常見(jiàn)的解決思路,并分析其適用場(chǎng)景。
跨庫(kù)Join的幾種解決思路
全局表
所謂全局表,就是有可能系統(tǒng)中所有模塊都可能會(huì)依賴(lài)到的一些表。比較類(lèi)似我們理解的“數(shù)據(jù)字典”。為了避免跨庫(kù)join查詢(xún),我們可以將這類(lèi)表在其他每個(gè)數(shù)據(jù)庫(kù)中均保存一份。同時(shí),這類(lèi)數(shù)據(jù)通常也很少發(fā)生修改(甚至幾乎不會(huì)),所以也不用太擔(dān)心“一致性”問(wèn)題。
字段冗余
這是一種典型的反范式設(shè)計(jì),在互聯(lián)網(wǎng)行業(yè)中比較常見(jiàn),通常是為了性能來(lái)避免join查詢(xún)。
舉個(gè)電商業(yè)務(wù)中很簡(jiǎn)單的場(chǎng)景:
“訂單表”中保存“賣(mài)家Id”的同時(shí),將賣(mài)家的“Name”字段也冗余,這樣查詢(xún)訂單詳情的時(shí)候就不需要再去查詢(xún)“賣(mài)家用戶(hù)表”。
字段冗余能帶來(lái)便利,是一種“空間換時(shí)間”的體現(xiàn)。但其適用場(chǎng)景也比較有限,比較適合依賴(lài)字段較少的情況。最復(fù)雜的還是數(shù)據(jù)一致性問(wèn)題,這點(diǎn)很難保證,可以借助數(shù)據(jù)庫(kù)中的觸發(fā)器或者在業(yè)務(wù)代碼層面去保證。當(dāng)然,也需要結(jié)合實(shí)際業(yè)務(wù)場(chǎng)景來(lái)看一致性的要求。就像上面例子,如果賣(mài)家修改了Name之后,是否需要在訂單信息中同步更新呢?
數(shù)據(jù)同步
定時(shí)A庫(kù)中的tab_a表和B庫(kù)中tbl_b有關(guān)聯(lián),可以定時(shí)將指定的表做同步。當(dāng)然,同步本來(lái)會(huì)對(duì)數(shù)據(jù)庫(kù)帶來(lái)一定的影響,需要性能影響和數(shù)據(jù)時(shí)效性中取得一個(gè)平衡。這樣來(lái)避免復(fù)雜的跨庫(kù)查詢(xún)。筆者曾經(jīng)在項(xiàng)目中是通過(guò)ETL工具來(lái)實(shí)施的。
系統(tǒng)層組裝
在系統(tǒng)層面,通過(guò)調(diào)用不同模塊的組件或者服務(wù),獲取到數(shù)據(jù)并進(jìn)行字段拼裝。說(shuō)起來(lái)很容易,但實(shí)踐起來(lái)可真沒(méi)有這么簡(jiǎn)單,尤其是數(shù)據(jù)庫(kù)設(shè)計(jì)上存在問(wèn)題但又無(wú)法輕易調(diào)整的時(shí)候。
具體情況通常會(huì)比較復(fù)雜。下面筆者結(jié)合以往實(shí)際經(jīng)驗(yàn),并通過(guò)偽代碼方式來(lái)描述。
簡(jiǎn)單的列表查詢(xún)的情況

偽代碼很容易理解,先獲取“我的提問(wèn)列表”數(shù)據(jù),然后再根據(jù)列表中的UserId去循環(huán)調(diào)用依賴(lài)的用戶(hù)服務(wù)獲取到用戶(hù)的RealName,拼裝結(jié)果并返回。
有經(jīng)驗(yàn)的讀者一眼就能看出上訴偽代碼存在效率問(wèn)題。循環(huán)調(diào)用服務(wù),可能會(huì)有循環(huán)RPC,循環(huán)查詢(xún)數(shù)據(jù)庫(kù)…不推薦使用。再看看改進(jìn)后的:

這種實(shí)現(xiàn)方式,看起來(lái)要優(yōu)雅一點(diǎn),其實(shí)就是把循環(huán)調(diào)用改成一次調(diào)用。當(dāng)然,用戶(hù)服務(wù)的數(shù)據(jù)庫(kù)查詢(xún)中很可能是In查詢(xún),效率方面比上一種方式更高。(坊間流傳In查詢(xún)會(huì)全表掃描,存在性能問(wèn)題,傳聞不可全信。其實(shí)查詢(xún)優(yōu)化器都是基本成本估算的,經(jīng)過(guò)測(cè)試,在In語(yǔ)句中條件字段有索引的時(shí)候,條件較少的情況是會(huì)走索引的。這里不細(xì)展開(kāi)說(shuō)明,感興趣的朋友請(qǐng)自行測(cè)試)。
小結(jié)
簡(jiǎn)單字段組裝的情況下,我們只需要先獲取“主表”數(shù)據(jù),然后再根據(jù)關(guān)聯(lián)關(guān)系,調(diào)用其他模塊的組件或服務(wù)來(lái)獲取依賴(lài)的其他字段(如例中依賴(lài)的用戶(hù)信息),最后將數(shù)據(jù)進(jìn)行組裝。
通常,我們都會(huì)通過(guò)緩存來(lái)避免頻繁RPC通信和數(shù)據(jù)庫(kù)查詢(xún)的開(kāi)銷(xiāo)。
列表查詢(xún)帶條件過(guò)濾的情況
在上述例子中,都是簡(jiǎn)單的字段組裝,而不存在條件過(guò)濾??床鸱智暗腟QL:

這種連接查詢(xún)并且還帶條件過(guò)濾的情況,想在代碼層面組裝數(shù)據(jù)其實(shí)是非常復(fù)雜的(尤其是左表和右表都帶條件過(guò)濾的情況會(huì)更復(fù)雜),不能像之前例子中那樣簡(jiǎn)單的進(jìn)行組裝了。試想一下,如果像上面那樣簡(jiǎn)單的進(jìn)行組裝,造成的結(jié)果就是返回的數(shù)據(jù)不完整,不準(zhǔn)確。
有如下幾種解決思路:
查出所有的問(wèn)答數(shù)據(jù),然后調(diào)用用戶(hù)服務(wù)進(jìn)行拼裝數(shù)據(jù),再根據(jù)過(guò)濾字段state字段進(jìn)行過(guò)濾,最后進(jìn)行排序和分頁(yè)并返回。這種方式能夠保證數(shù)據(jù)的準(zhǔn)確性和完整性,但是性能影響非常大,不建議使用。
查詢(xún)出state字段符合/不符合的UserId,在查詢(xún)問(wèn)答數(shù)據(jù)的時(shí)候使用in/not in進(jìn)行過(guò)濾,排序,分頁(yè)等。過(guò)濾出有效的問(wèn)答數(shù)據(jù)后,再調(diào)用用戶(hù)服務(wù)獲取數(shù)據(jù)進(jìn)行組裝。這種方式明顯更優(yōu)雅點(diǎn)。筆者之前在某個(gè)項(xiàng)目的特殊場(chǎng)景中就是采用過(guò)這種方式實(shí)現(xiàn)。
跨庫(kù)事務(wù)(分布式事務(wù))的問(wèn)題
按業(yè)務(wù)拆分?jǐn)?shù)據(jù)庫(kù)之后,不可避免的就是“分布式事務(wù)”的問(wèn)題。以往在代碼中通過(guò)spring注解簡(jiǎn)單配置就能實(shí)現(xiàn)事務(wù)的,現(xiàn)在則需要花很大的成本去保證一致性。
CAP理論

分布式事務(wù)相關(guān)的兩階段提交和三階段提交
兩階段提交協(xié)議在主流開(kāi)發(fā)語(yǔ)言平臺(tái),數(shù)據(jù)庫(kù)產(chǎn)品中都有廣泛應(yīng)用和實(shí)現(xiàn)的,下面來(lái)介紹一下XOpen組織提供的DTP模型圖:

XA協(xié)議指的是TM(事務(wù)管理器)和RM(資源管理器)之間的接口。目前主流的關(guān)系型數(shù)據(jù)庫(kù)產(chǎn)品都是實(shí)現(xiàn)了XA接口的。JTA(Java Transaction API)是符合X/Open DTP模型的,事務(wù)管理器和資源管理器之間也使用了XA協(xié)議。 本質(zhì)上也是借助兩階段提交協(xié)議來(lái)實(shí)現(xiàn)分布式事務(wù)的,下面分別來(lái)看看XA事務(wù)成功和失敗的模型圖:

在JavaEE平臺(tái)下,WebLogic、Webshare等主流商用的應(yīng)用服務(wù)器提供了JTA的實(shí)現(xiàn)和支持。而在Tomcat下是沒(méi)有實(shí)現(xiàn)的(其實(shí)筆者并不認(rèn)為T(mén)omcat能算是JavaEE應(yīng)用服務(wù)器),這就需要借助第三方的框架Jotm、Automikos等來(lái)實(shí)現(xiàn),兩者均支持spring事務(wù)整合。
而在Windows .NET平臺(tái)中,則可以借助ado.net中的TransactionScop API來(lái)編程實(shí)現(xiàn),還必須配置和借助Windows操作系統(tǒng)中的MSDTC服務(wù)。如果你的數(shù)據(jù)庫(kù)使用的mysql,并且mysql是部署在Linux平臺(tái)上的,那么是無(wú)法支持分布式事務(wù)的。
提供回滾接口
在服務(wù)化架構(gòu)中,功能X,需要去協(xié)調(diào)后端的A、B甚至更多的原子服務(wù)。那么問(wèn)題來(lái)了,假如A和B其中一個(gè)調(diào)用失敗了,那可怎么辦呢?
在筆者的工作中經(jīng)常遇到這類(lèi)問(wèn)題,往往提供了一個(gè)BFF層來(lái)協(xié)調(diào)調(diào)用A、B服務(wù)。如果有些是需要同步返回結(jié)果的,我會(huì)盡量按照“串行”的方式去調(diào)用。如果調(diào)用A失敗,則不會(huì)盲目去調(diào)用B。如果調(diào)用A成功,而調(diào)用B失敗,會(huì)嘗試去回滾剛剛對(duì)A的調(diào)用操作。
當(dāng)然,有些時(shí)候我們不必嚴(yán)格提供單獨(dú)對(duì)應(yīng)的回滾接口,可以通過(guò)傳遞參數(shù)巧妙的實(shí)現(xiàn)。
這樣的情況,我們會(huì)盡量把可提供回滾接口的服務(wù)放在前面。舉個(gè)例子說(shuō)明:
我們的某個(gè)論壇網(wǎng)站,每天登錄成功后會(huì)獎(jiǎng)勵(lì)用戶(hù)5個(gè)積分,但是積分和用戶(hù)又是兩套獨(dú)立的子系統(tǒng)服務(wù),對(duì)應(yīng)不同的DB,這控制起來(lái)就比較麻煩了。解決思路:
把登錄和加積分的服務(wù)調(diào)用放在BFF層一個(gè)本地方法中。
當(dāng)用戶(hù)請(qǐng)求登錄接口時(shí),先執(zhí)行加積分操作,加分成功后再執(zhí)行登錄操作
如果登錄成功,那當(dāng)然最好了,積分也加成功了。如果登錄失敗,則調(diào)用加積分對(duì)應(yīng)的回滾接口(執(zhí)行減積分的操作)。
總結(jié):這種方式缺點(diǎn)比較多,通常在復(fù)雜場(chǎng)景下是不推薦使用的,除非是非常簡(jiǎn)單的場(chǎng)景,非常容易提供回滾,而且依賴(lài)的服務(wù)也非常少的情況。
這種實(shí)現(xiàn)方式會(huì)造成代碼量龐大,耦合性高。而且非常有局限性,因?yàn)橛泻芏嗟臉I(yè)務(wù)是無(wú)法很簡(jiǎn)單的實(shí)現(xiàn)回滾的,如果串行的服務(wù)很多,回滾的成本實(shí)在太高
本地消息表
這種實(shí)現(xiàn)方式的思路,其實(shí)是源于ebay,后來(lái)通過(guò)支付寶等公司的布道,在業(yè)內(nèi)廣泛使用。其基本的設(shè)計(jì)思想是將遠(yuǎn)程分布式事務(wù)拆分成一系列的本地事務(wù)。如果不考慮性能及設(shè)計(jì)優(yōu)雅,借助關(guān)系型數(shù)據(jù)庫(kù)中的表即可實(shí)現(xiàn)。
舉個(gè)經(jīng)典的跨行轉(zhuǎn)賬的例子來(lái)描述
扣款1W,通過(guò)本地事務(wù)保證了憑證消息插入到消息表中。

通知對(duì)方銀行賬戶(hù)上加1W了。那問(wèn)題來(lái)了,如何通知到對(duì)方呢?
通常采用兩種方式:
采用時(shí)效性高的MQ,由對(duì)方訂閱消息并監(jiān)聽(tīng),有消息時(shí)自動(dòng)觸發(fā)事件
采用定時(shí)輪詢(xún)掃描的方式,去檢查消息表的數(shù)據(jù)。
兩種方式其實(shí)各有利弊,僅僅依靠MQ,可能會(huì)出現(xiàn)通知失敗的問(wèn)題。而過(guò)于頻繁的定時(shí)輪詢(xún),效率也不是最佳的(90%是無(wú)用功)。所以,我們一般會(huì)把兩種方式結(jié)合起來(lái)使用。
解決了通知的問(wèn)題,又有新的問(wèn)題了。萬(wàn)一這消息有重復(fù)被消費(fèi),往用戶(hù)帳號(hào)上多加了錢(qián),那豈不是后果很?chē)?yán)重?
仔細(xì)思考,其實(shí)我們可以消息消費(fèi)方,也通過(guò)一個(gè)“消費(fèi)狀態(tài)表”來(lái)記錄消費(fèi)狀態(tài)。在執(zhí)行“加款”操作之前,檢測(cè)下該消息(提供標(biāo)識(shí))是否已經(jīng)消費(fèi)過(guò),消費(fèi)完成后,通過(guò)本地事務(wù)控制來(lái)更新這個(gè)“消費(fèi)狀態(tài)表”。這樣子就避免重復(fù)消費(fèi)的問(wèn)題。
總結(jié):上訴的方式是一種非常經(jīng)典的實(shí)現(xiàn),基本避免了分布式事務(wù),實(shí)現(xiàn)了“最終一致性”。但是,關(guān)系型數(shù)據(jù)庫(kù)的吞吐量和性能方面存在瓶頸,頻繁的讀寫(xiě)消息會(huì)給數(shù)據(jù)庫(kù)造成壓力。所以,在真正的高并發(fā)場(chǎng)景下,該方案也會(huì)有瓶頸和限制的。
MQ(非事務(wù)消息)
通常情況下,在使用非事務(wù)消息支持的MQ產(chǎn)品時(shí),我們很難將業(yè)務(wù)操作與對(duì)MQ的操作放在一個(gè)本地事務(wù)域中管理。通俗點(diǎn)描述,還是以上述提到的“跨行轉(zhuǎn)賬”為例,我們很難保證在扣款完成之后對(duì)MQ投遞消息的操作就一定能成功。這樣一致性似乎很難保證。
先從消息生產(chǎn)者這端來(lái)分析,請(qǐng)看偽代碼:

根據(jù)上述代碼及注釋?zhuān)覀儊?lái)分析下可能的情況:
操作數(shù)據(jù)庫(kù)成功,向MQ中投遞消息也成功,皆大歡喜
操作數(shù)據(jù)庫(kù)失敗,不會(huì)向MQ中投遞消息了
操作數(shù)據(jù)庫(kù)成功,但是向MQ中投遞消息時(shí)失敗,向外拋出了異常,剛剛執(zhí)行的更新數(shù)據(jù)庫(kù)的操作將被回滾
上面分析的幾種情況來(lái)看,貌似問(wèn)題都不大的。那么我們來(lái)分析下消費(fèi)者端面臨的問(wèn)題:
消息出列后,消費(fèi)者對(duì)應(yīng)的業(yè)務(wù)操作要執(zhí)行成功。如果業(yè)務(wù)執(zhí)行失敗,消息不能失效或者丟失。需要保證消息與業(yè)務(wù)操作一致
盡量避免消息重復(fù)消費(fèi)。如果重復(fù)消費(fèi),也不能因此影響業(yè)務(wù)結(jié)果
如何保證消息與業(yè)務(wù)操作一致,不丟失?
主流的MQ產(chǎn)品都具有持久化消息的功能。如果消費(fèi)者宕機(jī)或者消費(fèi)失敗,都可以執(zhí)行重試機(jī)制的(有些MQ可以自定義重試次數(shù))
如何避免消息被重復(fù)消費(fèi)造成的問(wèn)題?
保證消費(fèi)者調(diào)用業(yè)務(wù)的服務(wù)接口的冪等性
通過(guò)消費(fèi)日志或者類(lèi)似狀態(tài)表來(lái)記錄消費(fèi)狀態(tài),便于判斷(建議在業(yè)務(wù)上自行實(shí)現(xiàn),而不依賴(lài)MQ產(chǎn)品提供該特性)
總結(jié):這種方式比較常見(jiàn),性能和吞吐量是優(yōu)于使用關(guān)系型數(shù)據(jù)庫(kù)消息表的方案。如果MQ自身和業(yè)務(wù)都具有高可用性,理論上是可以滿足大部分的業(yè)務(wù)場(chǎng)景的。不過(guò)在沒(méi)有充分測(cè)試的情況下,不建議在交易業(yè)務(wù)中直接使用。
TMQ(事務(wù)消息)
舉個(gè)例子,Bob向Smith轉(zhuǎn)賬,那我們到底是先發(fā)送消息,還是先執(zhí)行扣款操作?
好像都可能會(huì)出問(wèn)題。如果先發(fā)消息,扣款操作失敗,那么Smith的賬戶(hù)里面會(huì)多出一筆錢(qián)。反過(guò)來(lái),如果先執(zhí)行扣款操作,后發(fā)送消息,那有可能扣款成功了但是消息沒(méi)發(fā)出去,Smith收不到錢(qián)。除了上面介紹的通過(guò)異常捕獲和回滾的方式外,還有沒(méi)有其他的思路呢?
下面以阿里巴巴的RocketMQ中間件為例,分析下其設(shè)計(jì)和實(shí)現(xiàn)思路。
RocketMQ第一階段發(fā)送Prepared消息時(shí),會(huì)拿到消息的地址,第二階段執(zhí)行本地事物,第三階段通過(guò)第一階段拿到的地址去訪問(wèn)消息,并修改狀態(tài)。細(xì)心的讀者可能又發(fā)現(xiàn)問(wèn)題了,如果確認(rèn)消息發(fā)送失敗了怎么辦?RocketMQ會(huì)定期掃描消息集群中的事物消息,這時(shí)候發(fā)現(xiàn)了Prepared消息,它會(huì)向消息發(fā)送者確認(rèn),Bob的錢(qián)到底是減了還是沒(méi)減呢?如果減了是回滾還是繼續(xù)發(fā)送確認(rèn)消息呢?RocketMQ會(huì)根據(jù)發(fā)送端設(shè)置的策略來(lái)決定是回滾還是繼續(xù)發(fā)送確認(rèn)消息。這樣就保證了消息發(fā)送與本地事務(wù)同時(shí)成功或同時(shí)失敗。如下圖:

總結(jié):據(jù)筆者的了解,各大知名的電商平臺(tái)和互聯(lián)網(wǎng)公司,幾乎都是采用類(lèi)似的設(shè)計(jì)思路來(lái)實(shí)現(xiàn)“最終一致性”的。這種方式適合的業(yè)務(wù)場(chǎng)景廣泛,而且比較可靠。不過(guò)這種方式技術(shù)實(shí)現(xiàn)的難度比較大。目前主流的開(kāi)源MQ(ActiveMQ、RabbitMQ、Kafka)均未實(shí)現(xiàn)對(duì)事務(wù)消息的支持,所以需二次開(kāi)發(fā)或者新造輪子。比較遺憾的是,RocketMQ事務(wù)消息部分的代碼也并未開(kāi)源,需要自己去實(shí)現(xiàn)。
其他補(bǔ)償方式
做過(guò)支付寶交易接口的同學(xué)都知道,我們一般會(huì)在支付寶的回調(diào)頁(yè)面和接口里,解密參數(shù),然后調(diào)用系統(tǒng)中更新交易狀態(tài)相關(guān)的服務(wù),將訂單更新為付款成功。同時(shí),只有當(dāng)我們回調(diào)頁(yè)面中輸出了success字樣或者標(biāo)識(shí)業(yè)務(wù)處理成功相應(yīng)狀態(tài)碼時(shí),支付寶才會(huì)停止回調(diào)請(qǐng)求。否則,支付寶會(huì)每間隔一段時(shí)間后,再向客戶(hù)方發(fā)起回調(diào)請(qǐng)求,直到輸出成功標(biāo)識(shí)為止。
其實(shí)這就是一個(gè)很典型的補(bǔ)償例子,跟一些MQ重試補(bǔ)償機(jī)制很類(lèi)似。
一般成熟的系統(tǒng)中,對(duì)于級(jí)別較高的服務(wù)和接口,整體的可用性通常都會(huì)很高。如果有些業(yè)務(wù)由于瞬時(shí)的網(wǎng)絡(luò)故障或調(diào)用超時(shí)等問(wèn)題,那么這種重試機(jī)制其實(shí)是非常有效的。
當(dāng)然,考慮個(gè)比較極端的場(chǎng)景,假如系統(tǒng)自身有bug或者程序邏輯有問(wèn)題,那么重試1W次那也是無(wú)濟(jì)于事的。那豈不是就發(fā)生了“明明已經(jīng)付款,卻顯示未付款不發(fā)貨”類(lèi)似的悲???
其實(shí)為了交易系統(tǒng)更可靠,我們一般會(huì)在類(lèi)似交易這種高級(jí)別的服務(wù)代碼中
加入詳細(xì)日志記錄的,一旦系統(tǒng)內(nèi)部引發(fā)類(lèi)似致命異常,會(huì)有郵件通知。
后臺(tái)會(huì)有定時(shí)任務(wù)掃描和分析此類(lèi)日志,檢查出這種特殊的情況,會(huì)嘗試通過(guò)程序來(lái)補(bǔ)償并郵件通知相關(guān)人員。
在某些特殊的情況下,還會(huì)有“人工補(bǔ)償”的,這也是最后一道屏障。
相關(guān)問(wèn)題
數(shù)據(jù)庫(kù)是否需要進(jìn)行垂直分庫(kù)?
根據(jù)系統(tǒng)架構(gòu)和公司實(shí)際情況來(lái),如果你們的系統(tǒng)還是個(gè)簡(jiǎn)單的單體應(yīng)用,并且沒(méi)有什么訪問(wèn)量和數(shù)據(jù)量,那就別著急折騰“垂直分庫(kù)”了,否則沒(méi)有任何收益,也很難有好結(jié)果。
切記,“過(guò)度設(shè)計(jì)”和“過(guò)早優(yōu)化”是很多架構(gòu)師和技術(shù)人員常犯的毛病。
上面舉例的都太簡(jiǎn)單了,我們的后臺(tái)報(bào)表系統(tǒng)中join的表都有n個(gè)了, 分庫(kù)后該怎么查?
有很多朋友跟我提過(guò)類(lèi)似的問(wèn)題。其實(shí)互聯(lián)網(wǎng)的業(yè)務(wù)系統(tǒng)中,本來(lái)就應(yīng)該盡量避免join的,如果有多個(gè)join的,要么是設(shè)計(jì)不合理,要么是技術(shù)選型有誤。請(qǐng)自行科普下OLAP和OLTP,報(bào)表類(lèi)的系統(tǒng)在傳統(tǒng)BI時(shí)代都是通過(guò)OLAP數(shù)據(jù)倉(cāng)庫(kù)去實(shí)現(xiàn)的(現(xiàn)在則更多是借助離線分析、流式計(jì)算等手段實(shí)現(xiàn)),而不該向上面描述的那樣直接在業(yè)務(wù)庫(kù)中執(zhí)行大量join和統(tǒng)計(jì)。
Ref:
http://blog.csdn.net/dinglang_2009/article/details/53195835
http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
背景
你已經(jīng)采用了每服務(wù)每數(shù)據(jù)庫(kù)模式。每個(gè)服務(wù)都有獨(dú)自的數(shù)據(jù)庫(kù)。然而,一些業(yè)務(wù)事務(wù)跨越了多個(gè)服務(wù),因此你需要一個(gè)機(jī)制確保跨服務(wù)的數(shù)據(jù)一致性。比如,設(shè)想下你構(gòu)建一個(gè)電商應(yīng)用,客戶(hù)擁有信用額度。應(yīng)用程序必須確保一個(gè)新訂單沒(méi)有超出客戶(hù)的信用額度。由于訂單和顧客數(shù)據(jù)在不同的數(shù)據(jù)庫(kù),應(yīng)用程序無(wú)法簡(jiǎn)單的采用一個(gè)本地的ACID事務(wù)。
問(wèn)題
怎么跨服務(wù)管理數(shù)據(jù)一致性?
限制
2PC不是個(gè)可選項(xiàng)
解決方案
用Saga來(lái)實(shí)現(xiàn)跨越多個(gè)服務(wù)的業(yè)務(wù)事務(wù)。Saga代表著一系列本地事務(wù)。每個(gè)本地事務(wù)更新數(shù)據(jù)庫(kù),發(fā)布消息或事件來(lái)觸發(fā)Saga里的下一個(gè)本地事務(wù)。如果一個(gè)本地事務(wù)由于違反了業(yè)務(wù)規(guī)則而失敗,Saga執(zhí)行一系列補(bǔ)償事務(wù)以撤回被前面事務(wù)產(chǎn)生的更改。

Saga模式代替兩階段提交
有兩種方式可以協(xié)調(diào)Saga:
Choreography - 每個(gè)本地事務(wù)發(fā)起領(lǐng)域事件,觸發(fā)其他服務(wù)里的本地事務(wù)
Orchestration - Orchestrator 對(duì)象告訴參與者執(zhí)行哪個(gè)本地事務(wù)
示例:基于Choreography的Saga

Choreography
電商應(yīng)用采用choreography為基礎(chǔ)的方法創(chuàng)建訂單,將按照以下步驟:
Order Service創(chuàng)建一個(gè)pending狀態(tài)的訂單,并發(fā)布OrderCreated事件
Customer Service收到事件,嘗試驗(yàn)證訂單的信用。它發(fā)起Credit Reserved事件或者Credit Limit Exceeded事件
Order Service收到事件,將訂單狀態(tài)修改為approved或者cancelled
示例:基于Orchestration的Saga

Orchestration
電商應(yīng)用采用orchestration為基礎(chǔ)的方法創(chuàng)建訂單,將按照以下步驟:
Order Service創(chuàng)建一個(gè)pending狀態(tài)的訂單,并創(chuàng)建一個(gè)CreateOrderSaga
CreateOrderSaga往Customer Service發(fā)送ReserveCredit命令
Customer Service嘗試驗(yàn)證訂單信用,并發(fā)送一個(gè)回復(fù)
CreateOrderSaga接收回復(fù),往Order Service發(fā)送ApproveOrder或者RejectOrder命令
Order Service修改訂單的狀態(tài)為approved或者cancelled
結(jié)果
這個(gè)模式有如下優(yōu)勢(shì):
允許應(yīng)用跨越多個(gè)服務(wù)管理市局一致性,而不使用分布式事務(wù)
這個(gè)解決方案有如下弊端:
編程模型更加復(fù)雜。比如,開(kāi)發(fā)人員必須設(shè)計(jì)補(bǔ)償事務(wù),明確撤回Saga早些時(shí)候做出的更改。
還有如下問(wèn)題需要解決:
為了可靠,服務(wù)必須原子的更新數(shù)據(jù)庫(kù)和發(fā)布事件。不能使用跨越數(shù)據(jù)庫(kù)和消息隊(duì)列的傳統(tǒng)分布式事務(wù)的機(jī)制。相反,必須采用如下列出的一種模式。
相關(guān)模式
每服務(wù)每數(shù)據(jù)庫(kù)產(chǎn)生了對(duì)這個(gè)模式的需求
如下模式是原子性的更新?tīng)顟B(tài)和發(fā)布事件的方法:
參見(jiàn)
作者的書(shū)Microservices patterns從更多細(xì)節(jié)介紹了這個(gè)模式. 這本書(shū)的示例應(yīng)用采用Eventuate Tram Sagas framework來(lái)實(shí)現(xiàn)了Saga
作者的MicroXchg 2018 presentation(幻燈片和視頻)
以下例子用不同方式實(shí)現(xiàn)了客戶(hù)和訂單的例子:
Choreography-based saga,服務(wù)用Eventuate Tram framework發(fā)布領(lǐng)域事件
Orchestration-based saga,Order Service使用Eventuate Tram Sagas framework實(shí)現(xiàn)的orchestrator Saga
Choreography and event sourcing-based saga,服務(wù)使用Eventuate event sourcing framework發(fā)布領(lǐng)域事件
作者的兩篇InfoQ文章Developing Transactional Microservices Using Aggregates, Event Sourcing and CQRS 介紹了如何使用 event sourcing 來(lái)實(shí)現(xiàn) choreography Saga