版權聲明:本文爲博主原創文章,轉載須註明做者與來源。html
爲了解決傳統的單體應用(Monolithic Application)在可擴展性、可靠性、適應性、高部署成本等方面的問題,許多公司(好比Amazon、eBay和NetFlix等)開始使用微服務架構(Microservice Architecture)構建本身的應用。nginx
微服務架構(維基百科):
微服務 (Microservices) 是一種軟件架構風格 (Software Architecture Style),它是以專一於單一責任與功能的小型功能區塊 (Small Building Blocks) 爲基礎,利用模組化的方式組合出複雜的大型應用程序,各功能區塊使用與語言無關 (Language-Independent/Language agnostic) 的 API 集相互通信。程序員
可是,微服務架構在帶來一系列好處的同時,也帶來了若干挑戰。除了分佈式系統固有的複雜性之外,微服務架構也深入影響了應用和數據庫之間的關係,與傳統多個服務共享一個數據庫的方式不一樣,微服務架構每一個服務都有本身的數據庫。對於開發者來講,這就爲微服務中的數據管理提出了更高的要求。sql
在傳統的單體應用中,一般使用單個的關係型數據庫。這類數據庫所提供的事務語義,具有ACID特性。數據庫
ACID:
- Atomicity(原子性):一個事務中的操做是原子的,其中任何一步失敗,系統都可以徹底回到事務前的狀態
- Consistency(一致性):數據庫的狀態始終保持一致
- Isolation(隔離性):多個併發執行的事務不會互相影響
- Durability(持久性):事務處理結束後,對數據的修改是永久的markdown
應用得益於數據庫的這些特性,可以用簡單的方式對數據進行修改與讀取,而無需花費太多精力考慮數據一致性問題。網絡
可是,在微服務架構下,爲了在微服務之間創建鬆耦合的關係,一般每個微服務都會擁有本身獨立的數據庫,僅僅經過對外暴露的API來進行數據交換。這種狀況下,咱們就要面臨分佈式數據管理帶來的挑戰。也就是說,在實現業務邏輯時,如何保證服務之間的數據一致性。架構
咱們首先考慮在系統中實現實時一致性的狀況。好比以一個銀行系統爲例,客戶一般會有一個儲蓄帳戶和一個理財帳戶。如今,考慮客戶從本身的儲蓄帳戶向理財帳戶轉帳10000元的場景。併發
假設如今有兩張表 deposit_account 和 finance_account,分別用於存儲儲蓄帳戶和理財帳戶的信息,用戶的ID是201。那麼,在單一數據庫場景下,經過數據庫事務能夠很容易完成這個操做:機器學習
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;
這樣在單體應用中,因爲全部數據都是保存在同一個數據庫中,經過數據庫提供的ACID特性,就能夠輕鬆實現數據的實時一致性。
可是,在微服務架構中,可能的設計是存在兩個服務:儲蓄服務(Deposit Service)和理財服務(Finance Service),假設由儲蓄服務負責處理客戶的轉帳請求。而以下圖所示,這兩個服務都分別維護本身的數據,所以儲蓄服務沒法直接訪問理財服務的數據,而只能經過API去修改客戶的餘額。
此時,爲了知足訂單服務與客戶服務之間的實時一致性要求,能夠採用分佈式事務,好比基於兩階段提交協議(Two-phase commit, 2PC)的實現來作到這一點。(關於2PC,已經有大量的研究成果和成功實踐經驗,本文將再也不作太多闡述,具體可自行參見相關文獻和資料)
根據CAP定理,咱們追求實時一致性時,一般須要犧牲掉部分可用性。好比以上場景中,當 Finance Service 因爲軟硬件故障或網絡問題而不可用的時候,系統將沒法爲用戶提供內部轉帳服務。
此外,做爲典型的同步操做,2PC也存在着比較比較嚴重的性能問題,並不適合高併發場景。所以,在數據一致性上咱們須要尋求其餘的解決方案。
若是咱們考慮只保證系統的最終一致性,那麼就能夠避免使用2PC,從而提升系統可用性和性能。
仍然以以上的用戶內部帳戶之間的轉帳服務爲例。當用戶從儲蓄帳戶向理財帳戶轉帳時,減小儲蓄帳戶的金額與增長理財帳戶的金額這兩個動做,能夠無需在一個事務裏面完成,而是分紅兩步:
0. 儲蓄服務減去儲蓄帳戶中的金額,並生成一個憑證(消息)發送給理財服務;
0. 理財服務收到憑證後,在理財帳戶中增長相應的金額。
咱們會發現以上過程在第1步完成以後,第2步完成以前,儲蓄帳戶與理財帳戶之間其實是存在短期的數據不一致的。可是,只要最終第2步可以完成,系統的數據就仍然可以保持一致性,這就是咱們所說的最終一致性。
在最終一致性這個前提下,即便理財服務在某段時間內不可用,系統仍然可以能爲用戶提供內部轉帳服務,從而提升了系統的可用性。
而這樣一種基於最終一致性的解決方案,就是本文將要介紹的事件驅動的架構(Event-driven Architecture)。
所謂事件驅動的架構,也就是使用事件來實現跨多個服務的業務邏輯。
在這一架構裏,當有重要事件發生時,好比更新業務數據,某個微服務會發布事件,其它微服務則訂閱這些事件;當某一微服務接收到事件就能夠更新本身的業務數據,同時發佈新的事件觸發下一步更新。而事件的發佈與訂閱,則依賴於一個可靠的消息代理(Message Broker)。
以上文的場景爲例,在事件驅動的架構中,從儲蓄帳戶轉帳到理財帳戶的過程以下:
0. 儲蓄服務將用戶的儲蓄帳戶中的金額減小10000,併發布「向理財帳戶轉帳」事件;
0. 理財服務獲取「轉帳到理財帳戶」事件, 更新理財帳戶,將理財帳戶的金額增長10000,併發布「理財帳戶轉入」事件;
0. 儲蓄服務獲取「理財帳戶轉入」事件,結束本次轉帳交易。
在這裏須要考慮的一個問題,就是轉帳失敗處理。好比以上第2步若是由於「理財帳戶被凍結沒法轉入資金」之類的緣由失敗了,理財服務就應該發佈「理財帳戶轉入失敗」事件,儲蓄服務獲取到該事件後,須要對儲蓄帳戶進行回滾,將減小的金額從新增長回去。
以上的過程與傳統的數據管理基於ACID模型不同的是,它是基於BASE模型的。
BASE:
- Basically Available(基本可用):系統在出現不可預知的故障的時候,容許損失部分可用性,但不等於系統不可用
- Soft State(軟狀態):容許系統中的數據存在中間狀態,並認爲該中間狀態的存在不會影響系統的總體可用性
- Eventually Consistent(最終一致性):系統保證最終數據可以達到一致
在事件驅動的架構中,跨服務完成業務邏輯的一個關鍵點是每一個服務自動更新數據庫和發佈事件,也就是要以原子粒度更新數據庫和發佈事件。例如,儲蓄服務必須在對儲蓄帳戶表進行更新,而後發佈「向理財帳戶轉帳」事件,這兩個操做須要原子化實現。若是服務在更新數據庫以後、發佈事件以前崩潰,系統會變得不一致。
保證數據更新與事件發佈原子化的方法,有如下幾種:
- 使用本地事務發佈事件
- 挖掘數據庫事務日誌
- 使用事件源
一個實現原子化的方法是使用本地事務來更新業務實體和事件列表,由一個獨立進程來發布事件。具體來講,就是在存儲業務實體狀態的數據庫中,使用一個事件表來充當消息隊列。應用啓動一個(本地)數據庫事務,更新業務實體的狀態,在事件表中插入一個事件,並提交該事務。一個獨立的消息發佈線程或進程查詢該事件表,將事件發佈到消息代理,並標註該事件爲已發佈。下圖展現了這一設計。
儲蓄服務更新儲蓄帳戶的餘額,而後在事件表中插入「轉帳到理財帳戶」的事件。事件發佈線程或進程在事件表中查詢未發佈的事件併發布,而後更新事件表,將該事件標記爲已發佈。
這種方法的優勢是:
- 使用本地事務,保證了數據被更新時事件必定可以被髮布
- 實現簡單,只須要系統具有本地事務的能力便可實現
這種方法的一個缺點是,數據更新操做與所要發佈的事件之間的對應關係,是由應用的開發者實現的,所以有很大可能出錯。
實現原子化的另外一種方式是由線程或者進程經過挖掘數據庫事務或提交日誌來發布事件。應用更新數據庫,數據庫的事務日誌會記錄這些變動。事務日誌挖掘線程或進程讀取這些日誌,並把事件發佈到消息代理。
好比一個B2C的電商網站,就能夠經過挖掘訂單數據的更新日誌,來進行事件發佈。以下圖所示:
這一方法的範例是開源的 LinkedIn Databus 項目。Databus 挖掘 Oracle 事務日誌併發布與之對應的事件,LinkedIn 則使用 Databus 維持各類來源的數據存儲與記錄系統一致。
另外一個範例則是 AWS DynamoDB 採用的流機制。AWS DynamoDB 是一個可管理的 NoSQL 數據庫,其中每一個 DynamoDB 流包括 DynamoDB 表在過去 24 小時以內的時序變化,包括建立、更新和刪除操做。應用可以讀取這些變動,將其做爲事件發佈。
這種方法的優勢是:
- 要發佈的事件直接來源於數據庫的事務日誌,所以不會出錯
- 應用無需關注事件的發佈,簡化了應用開發者的工做
可是這種方法也有一些缺點:
- 事務日誌的格式與所使用的數據庫相關,所以事件挖掘 的實現會因爲數據庫的種類或版本的變化而隨之須要修改
- 因爲是直接從數據庫的更新記錄生成事件,所以可能會沒法逆向推斷出業務邏輯,所以並不適合於全部場景(好比前文所述的轉帳場景)
事件源採用一種大相徑庭的、以事件爲中心的方法來保存業務實體——不一樣於存儲實體的當前狀態,應用存儲的是狀態改變的事件序列。每當業務實體的狀態改變,新事件就被附加到事件列表,而且應用能夠經過事件回放來重構實體的當前狀態。鑑於保存事件是一個單一的操做,所以本質上也是原子化的。
要了解事件源如何運行,能夠以儲蓄服務爲例。在傳統的方法中,每次轉帳交易都會更新儲蓄帳戶表的記錄。而使用事件源的時候,儲蓄服務以狀態更改事件的方式存儲用戶的儲蓄帳戶,每一個事件都包含足夠的數據去重建儲蓄帳戶狀態。
事件長期保存在事件倉庫(Event Store),使用 API 添加和檢索實體的事件。同時,事件倉庫起到相似上文說起的消息代理的做用,經過 API 讓服務訂閱事件,將全部事件傳達到全部感興趣的訂閱者。因此,事件倉庫能夠認爲是數據庫與消息代理的綜合體,是事件源方法的支柱。
事件源方法有以下的優勢:
- 事件即狀態,發佈事件就是在更新狀態,所以自然具備原子性,而且不會出錯
- 因爲存儲的是事件,而不是域對象,所以避免了對象關係抗阻不匹配的問題(object‑relational impedance mismatch problem)
- 因爲存儲了全部的業務狀態更新事件,所以能夠經過事件回放推斷出任一時間點的業務實體狀態
事件源方法也有如下這些缺點:
- 要實現一個可靠和高性能的事件倉庫並非一件容易的事情
- 應用代碼須要根據事件倉庫的 API 進行重寫
- 事件倉庫只直接支持經過主鍵查詢業務實體,所以對於複雜視圖的查詢比較困難(能夠經過CQRS方法解決,具體參見下文)
在事件源方法中,再也不直接存儲任何業務實體的狀態,而是代之以狀態變動事件。在進行復雜視圖的查詢時,若是還按照與命令操做一樣的方式,將會遇到一些困難。好比要發起以下的一個同時涉及儲蓄帳戶和理財帳戶的查詢操做:
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
在非事件源的方式下,能夠很容易的從儲蓄帳戶表和理財帳戶表查詢到相應數據。可是在事件源方式下,事件倉庫中存儲的是一系列事件,而且只能經過主鍵(好比 deposit_account.id 或 finance_account.id)去查詢相應的業務實體,此時要處理相似 deposit.amount > 100000 這樣的查詢條件以及條件組合時,是很是複雜和低效的。
爲了解決這一問題,能夠採用CQRS方法,將命令與查詢分離。命令操做仍然經過各服務的 API 以更新事件列表的方式進行,而查詢操做則經過一個統一的視圖查詢服務(View Query Service)完成。
根據存儲在事件倉庫中的事件集合,能夠計算獲得每一個業務實體的狀態,這些狀態以物化視圖(Materialized View)的方式存儲在一個數據庫中。當有新的事件產生時,也一樣會自動更新視圖。這樣,視圖查詢服務就能夠像查詢普通的數據庫數據同樣實現各類查詢場景。具體的設計可參考下圖所示:
在微服務架構中,每一個微服務都有其私有數據存儲,不一樣的微服務可能使用不一樣的數據庫。這種架構帶來便利的同時,也給分佈式數據管理帶來挑戰,其中最大的挑戰就是在實現跨服務的業務邏輯時,如何保持服務之間的數據一致性。
對於許多應用,解決方案就是使用事件驅動的架構。事件驅動的架構帶來的挑戰是如何原子化地更新狀態和發佈事件。有幾個方法能夠作到這一點,包括把數據庫用做消息隊列、事務日誌挖掘和事件源。