前言
為了解決傳統(tǒng)的單體應(yīng)用(Monolithic Application)在可擴(kuò)展性麦向、可靠性螃宙、適應(yīng)性惶傻、高部署成本等方面的問(wèn)題嚼摩,許多公司(比如Amazon灶体、eBay和NetFlix等)開(kāi)始使用微服務(wù)架構(gòu)(Microservice Architecture)構(gòu)建自己的應(yīng)用。
微服務(wù)架構(gòu)(維基百科):
微服務(wù) (Microservices) 是一種軟件架構(gòu)風(fēng)格 (Software Architecture Style)掐暮,它是以專注于單一責(zé)任與功能的小型功能區(qū)塊 (Small Building Blocks) 為基礎(chǔ)蝎抽,利用模組化的方式組合出復(fù)雜的大型應(yīng)用程序,各功能區(qū)塊使用與語(yǔ)言無(wú)關(guān) (Language-Independent/Language agnostic) 的 API 集相互通訊路克。
但是樟结,微服務(wù)架構(gòu)在帶來(lái)一系列好處的同時(shí),也帶來(lái)了若干挑戰(zhàn)精算。除了分布式系統(tǒng)固有的復(fù)雜性以外瓢宦,微服務(wù)架構(gòu)也深刻影響了應(yīng)用和數(shù)據(jù)庫(kù)之間的關(guān)系,與傳統(tǒng)多個(gè)服務(wù)共享一個(gè)數(shù)據(jù)庫(kù)的方式不同灰羽,微服務(wù)架構(gòu)每個(gè)服務(wù)都有自己的數(shù)據(jù)庫(kù)驮履。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),這就為微服務(wù)中的數(shù)據(jù)管理提出了更高的要求廉嚼。
微服務(wù)架構(gòu)中的數(shù)據(jù)管理
在傳統(tǒng)的單體應(yīng)用中玫镐,通常使用單個(gè)的關(guān)系型數(shù)據(jù)庫(kù)。這類數(shù)據(jù)庫(kù)所提供的事務(wù)語(yǔ)義怠噪,具備ACID特性恐似。
ACID:
- Atomicity(原子性):一個(gè)事務(wù)中的操作是原子的,其中任何一步失敗傍念,系統(tǒng)都能夠完全回到事務(wù)前的狀態(tài)
- Consistency(一致性):數(shù)據(jù)庫(kù)的狀態(tài)始終保持一致
- Isolation(隔離性):多個(gè)并發(fā)執(zhí)行的事務(wù)不會(huì)互相影響
- Durability(持久性):事務(wù)處理結(jié)束后矫夷,對(duì)數(shù)據(jù)的修改是永久的
應(yīng)用得益于數(shù)據(jù)庫(kù)的這些特性,能夠用簡(jiǎn)單的方式對(duì)數(shù)據(jù)進(jìn)行修改與讀取憋槐,而無(wú)需花費(fèi)太多精力考慮數(shù)據(jù)一致性問(wèn)題双藕。
但是,在微服務(wù)架構(gòu)下阳仔,為了在微服務(wù)之間建立松耦合的關(guān)系蔓彩,通常每一個(gè)微服務(wù)都會(huì)擁有自己獨(dú)立的數(shù)據(jù)庫(kù),僅僅通過(guò)對(duì)外暴露的API來(lái)進(jìn)行數(shù)據(jù)交換驳概。這種情況下赤嚼,我們就要面臨分布式數(shù)據(jù)管理帶來(lái)的挑戰(zhàn)。也就是說(shuō)顺又,在實(shí)現(xiàn)業(yè)務(wù)邏輯時(shí)更卒,如何保證服務(wù)之間的數(shù)據(jù)一致性。
實(shí)時(shí)一致性
我們首先考慮在系統(tǒng)中實(shí)現(xiàn)實(shí)時(shí)一致性的情況稚照。比如以一個(gè)銀行系統(tǒng)為例蹂空,客戶通常會(huì)有一個(gè)儲(chǔ)蓄賬戶和一個(gè)理財(cái)賬戶「┟龋現(xiàn)在,考慮客戶從自己的儲(chǔ)蓄賬戶向理財(cái)賬戶轉(zhuǎn)賬10000元的場(chǎng)景上枕。
假設(shè)現(xiàn)在有兩張表 deposit_account 和 finance_account咐熙,分別用于存儲(chǔ)儲(chǔ)蓄賬戶和理財(cái)賬戶的信息,用戶的ID是201辨萍。那么棋恼,在單一數(shù)據(jù)庫(kù)場(chǎng)景下,通過(guò)數(shù)據(jù)庫(kù)事務(wù)可以很容易完成這個(gè)操作:
Begin transaction
update deposit_account_table set amount=amount-10000 where userId=201;
update finance_account amount=amount+10000 where userId=1;
End transaction
commit;
這樣在單體應(yīng)用中锈玉,由于所有數(shù)據(jù)都是保存在同一個(gè)數(shù)據(jù)庫(kù)中爪飘,通過(guò)數(shù)據(jù)庫(kù)提供的ACID特性,就可以輕松實(shí)現(xiàn)數(shù)據(jù)的實(shí)時(shí)一致性拉背。
但是师崎,在微服務(wù)架構(gòu)中,可能的設(shè)計(jì)是存在兩個(gè)服務(wù):儲(chǔ)蓄服務(wù)(Deposit Service)和理財(cái)服務(wù)(Finance Service)椅棺,假設(shè)由儲(chǔ)蓄服務(wù)負(fù)責(zé)處理客戶的轉(zhuǎn)賬請(qǐng)求犁罩。而如下圖所示,這兩個(gè)服務(wù)都分別維護(hù)自己的數(shù)據(jù)两疚,因此儲(chǔ)蓄服務(wù)無(wú)法直接訪問(wèn)理財(cái)服務(wù)的數(shù)據(jù)昼汗,而只能通過(guò)API去修改客戶的余額。
此時(shí)鬼雀,為了滿足訂單服務(wù)與客戶服務(wù)之間的實(shí)時(shí)一致性要求顷窒,可以采用分布式事務(wù),比如基于兩階段提交協(xié)議(Two-phase commit, 2PC)的實(shí)現(xiàn)來(lái)做到這一點(diǎn)源哩。(關(guān)于2PC鞋吉,已經(jīng)有大量的研究成果和成功實(shí)踐經(jīng)驗(yàn),本文將不再做太多闡述励烦,具體可自行參見(jiàn)相關(guān)文獻(xiàn)和資料)
根據(jù)CAP定理谓着,我們追求實(shí)時(shí)一致性時(shí),通常需要犧牲掉部分可用性坛掠。比如以上場(chǎng)景中赊锚,當(dāng) Finance Service 由于軟硬件故障或網(wǎng)絡(luò)問(wèn)題而不可用的時(shí)候,系統(tǒng)將無(wú)法為用戶提供內(nèi)部轉(zhuǎn)賬服務(wù)屉栓。
此外舷蒲,作為典型的同步操作,2PC也存在著比較比較嚴(yán)重的性能問(wèn)題友多,并不適合高并發(fā)場(chǎng)景牲平。因此,在數(shù)據(jù)一致性上我們需要尋求其他的解決方案域滥。
最終一致性
如果我們考慮只保證系統(tǒng)的最終一致性纵柿,那么就可以避免使用2PC蜈抓,從而提高系統(tǒng)可用性和性能。
仍然以以上的用戶內(nèi)部賬戶之間的轉(zhuǎn)賬服務(wù)為例昂儒。當(dāng)用戶從儲(chǔ)蓄賬戶向理財(cái)賬戶轉(zhuǎn)賬時(shí)沟使,減少儲(chǔ)蓄賬戶的金額與增加理財(cái)賬戶的金額這兩個(gè)動(dòng)作,可以無(wú)需在一個(gè)事務(wù)里面完成渊跋,而是分成兩步:
- 儲(chǔ)蓄服務(wù)減去儲(chǔ)蓄賬戶中的金額腊嗡,并生成一個(gè)憑證(消息)發(fā)送給理財(cái)服務(wù);
- 理財(cái)服務(wù)收到憑證后刹枉,在理財(cái)賬戶中增加相應(yīng)的金額。
我們會(huì)發(fā)現(xiàn)以上過(guò)程在第1步完成之后屈呕,第2步完成之前微宝,儲(chǔ)蓄賬戶與理財(cái)賬戶之間實(shí)際上是存在短時(shí)間的數(shù)據(jù)不一致的。但是虎眨,只要最終第2步能夠完成蟋软,系統(tǒng)的數(shù)據(jù)就仍然能夠保持一致性,這就是我們所說(shuō)的最終一致性嗽桩。
在最終一致性這個(gè)前提下岳守,即使理財(cái)服務(wù)在某段時(shí)間內(nèi)不可用,系統(tǒng)仍然能夠能為用戶提供內(nèi)部轉(zhuǎn)賬服務(wù)碌冶,從而提高了系統(tǒng)的可用性湿痢。
而這樣一種基于最終一致性的解決方案,就是本文將要介紹的事件驅(qū)動(dòng)的架構(gòu)(Event-driven Architecture)扑庞。
事件驅(qū)動(dòng)的架構(gòu)
所謂事件驅(qū)動(dòng)的架構(gòu)譬重,也就是使用事件來(lái)實(shí)現(xiàn)跨多個(gè)服務(wù)的業(yè)務(wù)邏輯。
在這一架構(gòu)里罐氨,當(dāng)有重要事件發(fā)生時(shí)臀规,比如更新業(yè)務(wù)數(shù)據(jù),某個(gè)微服務(wù)會(huì)發(fā)布事件栅隐,其它微服務(wù)則訂閱這些事件塔嬉;當(dāng)某一微服務(wù)接收到事件就可以更新自己的業(yè)務(wù)數(shù)據(jù),同時(shí)發(fā)布新的事件觸發(fā)下一步更新租悄。而事件的發(fā)布與訂閱谨究,則依賴于一個(gè)可靠的消息代理(Message Broker)。
以上文的場(chǎng)景為例泣棋,在事件驅(qū)動(dòng)的架構(gòu)中记盒,從儲(chǔ)蓄賬戶轉(zhuǎn)賬到理財(cái)賬戶的過(guò)程如下:
- 儲(chǔ)蓄服務(wù)將用戶的儲(chǔ)蓄賬戶中的金額減少10000,并發(fā)布“向理財(cái)賬戶轉(zhuǎn)賬”事件外傅;
- 理財(cái)服務(wù)獲取“轉(zhuǎn)賬到理財(cái)賬戶”事件纪吮, 更新理財(cái)賬戶俩檬,將理財(cái)賬戶的金額增加10000,并發(fā)布“理財(cái)賬戶轉(zhuǎn)入”事件碾盟;
- 儲(chǔ)蓄服務(wù)獲取“理財(cái)賬戶轉(zhuǎn)入”事件棚辽,結(jié)束本次轉(zhuǎn)賬交易。
在這里需要考慮的一個(gè)問(wèn)題冰肴,就是轉(zhuǎn)賬失敗處理屈藐。比如以上第2步如果因?yàn)椤袄碡?cái)賬戶被凍結(jié)無(wú)法轉(zhuǎn)入資金”之類的原因失敗了,理財(cái)服務(wù)就應(yīng)該發(fā)布“理財(cái)賬戶轉(zhuǎn)入失敗”事件熙尉,儲(chǔ)蓄服務(wù)獲取到該事件后联逻,需要對(duì)儲(chǔ)蓄賬戶進(jìn)行回滾,將減少的金額重新增加回去检痰。
以上的過(guò)程與傳統(tǒng)的數(shù)據(jù)管理基于ACID模型不一樣的是包归,它是基于BASE模型的。
BASE:
- Basically Available(基本可用):系統(tǒng)在出現(xiàn)不可預(yù)知的故障的時(shí)候铅歼,允許損失部分可用性公壤,但不等于系統(tǒng)不可用
- Soft State(軟狀態(tài)):允許系統(tǒng)中的數(shù)據(jù)存在中間狀態(tài),并認(rèn)為該中間狀態(tài)的存在不會(huì)影響系統(tǒng)的整體可用性
- Eventually Consistent(最終一致性):系統(tǒng)保證最終數(shù)據(jù)能夠達(dá)到一致
事件發(fā)布
在事件驅(qū)動(dòng)的架構(gòu)中椎椰,跨服務(wù)完成業(yè)務(wù)邏輯的一個(gè)關(guān)鍵點(diǎn)是每個(gè)服務(wù)自動(dòng)更新數(shù)據(jù)庫(kù)和發(fā)布事件厦幅,也就是要以原子粒度更新數(shù)據(jù)庫(kù)和發(fā)布事件。例如慨飘,儲(chǔ)蓄服務(wù)必須在對(duì)儲(chǔ)蓄賬戶表進(jìn)行更新确憨,然后發(fā)布“向理財(cái)賬戶轉(zhuǎn)賬”事件,這兩個(gè)操作需要原子化實(shí)現(xiàn)瓤的。如果服務(wù)在更新數(shù)據(jù)庫(kù)之后缚态、發(fā)布事件之前崩潰,系統(tǒng)會(huì)變得不一致堤瘤。
保證數(shù)據(jù)更新與事件發(fā)布原子化的方法玫芦,有以下幾種:
- 使用本地事務(wù)發(fā)布事件
- 挖掘數(shù)據(jù)庫(kù)事務(wù)日志
- 使用事件源
使用本地事務(wù)發(fā)布事件
一個(gè)實(shí)現(xiàn)原子化的方法是使用本地事務(wù)來(lái)更新業(yè)務(wù)實(shí)體和事件列表,由一個(gè)獨(dú)立進(jìn)程來(lái)發(fā)布事件本辐。具體來(lái)說(shuō)桥帆,就是在存儲(chǔ)業(yè)務(wù)實(shí)體狀態(tài)的數(shù)據(jù)庫(kù)中,使用一個(gè)事件表來(lái)充當(dāng)消息隊(duì)列慎皱。應(yīng)用啟動(dòng)一個(gè)(本地)數(shù)據(jù)庫(kù)事務(wù)老虫,更新業(yè)務(wù)實(shí)體的狀態(tài),在事件表中插入一個(gè)事件茫多,并提交該事務(wù)祈匙。一個(gè)獨(dú)立的消息發(fā)布線程或進(jìn)程查詢?cè)撌录恚瑢⑹录l(fā)布到消息代理,并標(biāo)注該事件為已發(fā)布夺欲。下圖展示了這一設(shè)計(jì)跪帝。
儲(chǔ)蓄服務(wù)更新儲(chǔ)蓄賬戶的余額,然后在事件表中插入“轉(zhuǎn)賬到理財(cái)賬戶”的事件些阅。事件發(fā)布線程或進(jìn)程在事件表中查詢未發(fā)布的事件并發(fā)布伞剑,然后更新事件表,將該事件標(biāo)記為已發(fā)布市埋。
這種方法的優(yōu)點(diǎn)是:
- 使用本地事務(wù)黎泣,保證了數(shù)據(jù)被更新時(shí)事件一定能夠被發(fā)布
- 實(shí)現(xiàn)簡(jiǎn)單,只需要系統(tǒng)具備本地事務(wù)的能力即可實(shí)現(xiàn)
這種方法的一個(gè)缺點(diǎn)是缤谎,數(shù)據(jù)更新操作與所要發(fā)布的事件之間的對(duì)應(yīng)關(guān)系抒倚,是由應(yīng)用的開(kāi)發(fā)者實(shí)現(xiàn)的,因此有很大可能出錯(cuò)坷澡。
挖掘數(shù)據(jù)庫(kù)事務(wù)日志
實(shí)現(xiàn)原子化的另一種方式是由線程或者進(jìn)程通過(guò)挖掘數(shù)據(jù)庫(kù)事務(wù)或提交日志來(lái)發(fā)布事件托呕。應(yīng)用更新數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)的事務(wù)日志會(huì)記錄這些變更洋访。事務(wù)日志挖掘線程或進(jìn)程讀取這些日志镣陕,并把事件發(fā)布到消息代理谴餐。
比如一個(gè)B2C的電商網(wǎng)站姻政,就可以通過(guò)挖掘訂單數(shù)據(jù)的更新日志,來(lái)進(jìn)行事件發(fā)布岂嗓。如下圖所示:
這一方法的范例是開(kāi)源的 LinkedIn Databus 項(xiàng)目汁展。Databus 挖掘 Oracle 事務(wù)日志并發(fā)布與之對(duì)應(yīng)的事件,LinkedIn 則使用 Databus 維持各種來(lái)源的數(shù)據(jù)存儲(chǔ)與記錄系統(tǒng)一致厌殉。
另一個(gè)范例則是 AWS DynamoDB 采用的流機(jī)制食绿。AWS DynamoDB 是一個(gè)可管理的 NoSQL 數(shù)據(jù)庫(kù),其中每個(gè) DynamoDB 流包括 DynamoDB 表在過(guò)去 24 小時(shí)之內(nèi)的時(shí)序變化公罕,包括創(chuàng)建器紧、更新和刪除操作。應(yīng)用能夠讀取這些變更楼眷,將其作為事件發(fā)布铲汪。
這種方法的優(yōu)點(diǎn)是:
- 要發(fā)布的事件直接來(lái)源于數(shù)據(jù)庫(kù)的事務(wù)日志,因此不會(huì)出錯(cuò)
- 應(yīng)用無(wú)需關(guān)注事件的發(fā)布罐柳,簡(jiǎn)化了應(yīng)用開(kāi)發(fā)者的工作
但是這種方法也有一些缺點(diǎn):
- 事務(wù)日志的格式與所使用的數(shù)據(jù)庫(kù)相關(guān)掌腰,因此事件挖掘 的實(shí)現(xiàn)會(huì)由于數(shù)據(jù)庫(kù)的種類或版本的變化而隨之需要修改
- 由于是直接從數(shù)據(jù)庫(kù)的更新記錄生成事件,因此可能會(huì)無(wú)法逆向推斷出業(yè)務(wù)邏輯张吉,因此并不適合于所有場(chǎng)景(比如前文所述的轉(zhuǎn)賬場(chǎng)景)
使用事件源
事件源采用一種截然不同的齿梁、以事件為中心的方法來(lái)保存業(yè)務(wù)實(shí)體——不同于存儲(chǔ)實(shí)體的當(dāng)前狀態(tài),應(yīng)用存儲(chǔ)的是狀態(tài)改變的事件序列。每當(dāng)業(yè)務(wù)實(shí)體的狀態(tài)改變勺择,新事件就被附加到事件列表创南,并且應(yīng)用可以通過(guò)事件回放來(lái)重構(gòu)實(shí)體的當(dāng)前狀態(tài)。鑒于保存事件是一個(gè)單一的操作酵幕,因此本質(zhì)上也是原子化的扰藕。
要了解事件源如何運(yùn)行,可以以儲(chǔ)蓄服務(wù)為例芳撒。在傳統(tǒng)的方法中邓深,每次轉(zhuǎn)賬交易都會(huì)更新儲(chǔ)蓄賬戶表的記錄。而使用事件源的時(shí)候笔刹,儲(chǔ)蓄服務(wù)以狀態(tài)更改事件的方式存儲(chǔ)用戶的儲(chǔ)蓄賬戶芥备,每個(gè)事件都包含足夠的數(shù)據(jù)去重建儲(chǔ)蓄賬戶狀態(tài)。
事件長(zhǎng)期保存在事件倉(cāng)庫(kù)(Event Store)舌菜,使用 API 添加和檢索實(shí)體的事件萌壳。同時(shí),事件倉(cāng)庫(kù)起到類似上文提及的消息代理的作用日月,通過(guò) API 讓服務(wù)訂閱事件袱瓮,將所有事件傳達(dá)到所有感興趣的訂閱者。所以爱咬,事件倉(cāng)庫(kù)可以認(rèn)為是數(shù)據(jù)庫(kù)與消息代理的綜合體尺借,是事件源方法的支柱。
事件源方法有如下的優(yōu)點(diǎn):
- 事件即狀態(tài)精拟,發(fā)布事件就是在更新?tīng)顟B(tài)燎斩,因此天然具有原子性,并且不會(huì)出錯(cuò)
- 由于存儲(chǔ)的是事件蜂绎,而不是域?qū)ο笳け恚虼吮苊饬?a target="_blank" rel="nofollow">對(duì)象關(guān)系抗阻不匹配的問(wèn)題(object?relational impedance mismatch problem)
- 由于存儲(chǔ)了所有的業(yè)務(wù)狀態(tài)更新事件,因此可以通過(guò)事件回放推斷出任一時(shí)間點(diǎn)的業(yè)務(wù)實(shí)體狀態(tài)
事件源方法也有以下這些缺點(diǎn):
- 要實(shí)現(xiàn)一個(gè)可靠和高性能的事件倉(cāng)庫(kù)并不是一件容易的事情
- 應(yīng)用代碼需要根據(jù)事件倉(cāng)庫(kù)的 API 進(jìn)行重寫(xiě)
- 事件倉(cāng)庫(kù)只直接支持通過(guò)主鍵查詢業(yè)務(wù)實(shí)體师枣,因此對(duì)于復(fù)雜視圖的查詢比較困難(可以通過(guò)CQRS方法解決怪瓶,具體參見(jiàn)下文)
命令查詢分離(CQRS)
在事件源方法中,不再直接存儲(chǔ)任何業(yè)務(wù)實(shí)體的狀態(tài)践美,而是代之以狀態(tài)變更事件洗贰。在進(jìn)行復(fù)雜視圖的查詢時(shí),如果還按照與命令操作同樣的方式拨脉,將會(huì)遇到一些困難哆姻。比如要發(fā)起如下的一個(gè)同時(shí)涉及儲(chǔ)蓄賬戶和理財(cái)賬戶的查詢操作:
SELECT *
FROM DEPOSIT_ACCOUNT deposit, FINANCE_ACCOUNT finance
WHERE
deposit.user_id = finance.user_id
AND finance.state = 'active'
AND deposit.amount > 100000
AND finance.amount > 5000
在非事件源的方式下,可以很容易的從儲(chǔ)蓄賬戶表和理財(cái)賬戶表查詢到相應(yīng)數(shù)據(jù)玫膀。但是在事件源方式下矛缨,事件倉(cāng)庫(kù)中存儲(chǔ)的是一系列事件,并且只能通過(guò)主鍵(比如 deposit_account.id 或 finance_account.id)去查詢相應(yīng)的業(yè)務(wù)實(shí)體,此時(shí)要處理類似 deposit.amount > 100000 這樣的查詢條件以及條件組合時(shí)箕昭,是非常復(fù)雜和低效的灵妨。
為了解決這一問(wèn)題,可以采用CQRS方法落竹,將命令與查詢分離泌霍。命令操作仍然通過(guò)各服務(wù)的 API 以更新事件列表的方式進(jìn)行,而查詢操作則通過(guò)一個(gè)統(tǒng)一的視圖查詢服務(wù)(View Query Service)完成述召。
根據(jù)存儲(chǔ)在事件倉(cāng)庫(kù)中的事件集合朱转,可以計(jì)算得到每個(gè)業(yè)務(wù)實(shí)體的狀態(tài),這些狀態(tài)以物化視圖(Materialized View)的方式存儲(chǔ)在一個(gè)數(shù)據(jù)庫(kù)中积暖。當(dāng)有新的事件產(chǎn)生時(shí)藤为,也同樣會(huì)自動(dòng)更新視圖。這樣夺刑,視圖查詢服務(wù)就可以像查詢普通的數(shù)據(jù)庫(kù)數(shù)據(jù)一樣實(shí)現(xiàn)各種查詢場(chǎng)景缅疟。具體的設(shè)計(jì)可參考下圖所示:
結(jié)論
在微服務(wù)架構(gòu)中,每個(gè)微服務(wù)都有其私有數(shù)據(jù)存儲(chǔ)遍愿,不同的微服務(wù)可能使用不同的數(shù)據(jù)庫(kù)存淫。這種架構(gòu)帶來(lái)便利的同時(shí),也給分布式數(shù)據(jù)管理帶來(lái)挑戰(zhàn)沼填,其中最大的挑戰(zhàn)就是在實(shí)現(xiàn)跨服務(wù)的業(yè)務(wù)邏輯時(shí)桅咆,如何保持服務(wù)之間的數(shù)據(jù)一致性。
對(duì)于許多應(yīng)用倾哺,解決方案就是使用事件驅(qū)動(dòng)的架構(gòu)轧邪。事件驅(qū)動(dòng)的架構(gòu)帶來(lái)的挑戰(zhàn)是如何原子化地更新?tīng)顟B(tài)和發(fā)布事件刽脖。有幾個(gè)方法可以做到這一點(diǎn)羞海,包括把數(shù)據(jù)庫(kù)用作消息隊(duì)列、事務(wù)日志挖掘和事件源曲管。