然後我們向你展示了一種解決方案就是将每個服務的業務邏輯實作為一組DDD聚合。然後每個事務隻能更新或建立一個單獨的聚合。然後通過事件來維護聚合(和服務)之間的資料一緻性。
在本集中,我們将會向你介紹使用事件的時候遇到了一個新的問題,就是怎麼樣通過原子方式更新聚合和釋出事件。然後會展示如何使用事件源來解決這個問題,事件源是一種以事件為中心的業務邏輯設計和持久化的方法。之後,我們會闡述微服務架構下的查詢困難的問題。然後向你介紹一種稱為指令查詢責任分離(CQRS)的方法來實作可擴充和高性能的查詢。
可靠地更新狀态和釋出事件
從表面上看,使用事件來保持聚合之間的一緻性似乎很簡單。
當一個服務建立或更新資料庫的一個聚合時,它隻是簡單地釋出一個事件。
但是,這隻是表象,其實還有一個核心問題就是:更新資料庫和釋出事件必須是原子的。否則,就會出現類似這樣的情況:如果服務在更新資料庫之後但在釋出事件之前崩潰,則系統就出現了不一緻的問題。
傳統的解決方案是一般都是使用分布式事務來搞,一個涉及資料庫和消息broker的分布式事務。但是,由于上一集所述的原因,2PC不是一個可行的選擇。
其實除了2PC ,還有幾種解決這個問題的方法。
一種解決方案就是,應用程式可以通過向類似Kafka這樣的消息中間件的broker釋出一個事件來執行更新。然後一個消息consumer訂閱這個事件,通過消費該事件然後最終更新資料庫。這種方法可以確定資料庫被更新并且事件被釋出。
但是缺點就是這種一緻性模型過于複雜,至少有點複雜。而且應用程式不能夠立即讀取到自己剛剛的寫入。
圖1 - 通過釋出事件到消息broker來更新資料庫
另一種做法就是,如圖2所示,就是應用程式追加事務日志到資料庫(a.k.a.commit log),将每個記錄的更改轉換為事件,然後把事件釋出到消息broker。這種做法的一個重要好處就是應用程式本身不需要任何的改變。
然而,一個缺點是,這種做法是一種底層(low-level)的事件,而不是上層業務事件。可能難以将上層業務事件(由于資料庫更新的原因)從底層更改逆轉到表中的行。
原文:it can be difficult toreverse engineer the high-level business event - the reason for the databaseupdate - from the low-level changes to the rows in the tables.
<a href="https://s2.51cto.com/wyfs02/M01/8F/8B/wKioL1jkp5bwYYG0AAANZ93X4q8271.jpg" target="_blank"></a>
圖2 - 追加資料庫事務日志
第三種解決方案就是,圖3所示的這種,使用資料庫表來作為一種臨時性的message queue。當一個服務更新一個聚合,它會insert一個事件到EVENTS表,作為本地ACID事務的一部分。然後一個單獨的程序輪詢EVENTS表并将事件釋出到消息broker。
這種做法的好處就是service能夠釋出high-level的業務事件。
缺點是這種做法容易出錯,有這種潛在的可能,因為事件釋出代碼必須與業務邏輯同步。
<a href="https://s4.51cto.com/wyfs02/M01/8F/8B/wKioL1jkp93gxI1FAAAPjmZaj1U903.jpg" target="_blank"></a>
圖3 - 使用資料庫表作為message queue
上面三種做法都有比較典型的缺點。
釋出一個事件到message broker并稍後更新的做法總是不能提供一種read-your-writes的一緻性,也就是隻能保證最終一緻。
追加事務日志提供了一緻的讀取,但卻不能釋出進階業務事件。
使用資料庫表作為message queue提供了一緻的讀取并且可以釋出high-level業務事件,但
卻對開發人員有依賴,就是開發人員得記得在狀态發生改變的時候加上釋出事件的邏輯。
幸運的是,我們還有另外一種解決方案,那就是event sourcing,事件源。它是一種針對持久化和業務邏輯的一種以事件為中心方法,稱為事件源。這裡解釋的不夠清楚,稍後慢慢展開。
使用事件源來開發微服務
我第一次了解到這個概念是在大概五年多以前,之後對這個新生事物一直充滿了好奇,直到我開始開發微服務。接下來,你将會看到通過事件源來實作事件驅動的微服務架構是多麼不錯的一種方法。
一個service通過event sourcing使用一系列的事件來持久化每個聚合。
當建立或更新一個聚合的時候,這個service會在資料庫裡儲存一個或多個事件,這種資料庫裡存儲event的方式可以叫做是event store,以下我們就叫“事件資料庫”。
它通過加載這些事件并replay這些事件,進而實作更新聚合的目前狀态。
在函數式程式設計裡,一個service通過執行一個函數式的fold或reduce來重構聚合,而不是事件。
由于事件就是狀态,是以你就不會再有原子地更新狀态和釋出事件的問題了。
例如,比如訂單服務(Order Service)。不是将每個訂單作為一行存儲在ORDERS表中,而是将每個訂單聚合作為一系列的事件,比如訂單已建立,訂單已準許,訂單已發貨等持久化到EVENTS表中。圖4顯示了這些事件如何存儲在基于SQL的事件資料庫(event store)中。
<a href="https://s1.51cto.com/wyfs02/M02/8F/8D/wKiom1jkqA_BlsadAAA22ieHK7M335.jpg" target="_blank"></a>
圖4 - 使用事件源來持久化一個訂單
每列的意思:
entity_type 和entity_id –唯一辨別一個聚合
event_id – 事件ID,唯一辨別
event_type – 事件類型
event_data -事件屬性的序列化JSON表示
一些事件包含大量資料。例如,訂單建立(Order Created)事件包含完整訂單,包括其訂單項,付款資訊和交貨資訊。其他事件,如訂單出貨(Order Shipped)事件,包含很少或沒有資料,隻是表示狀态轉換。
事件源(Event Sourcing)和釋出事件
嚴格的講,事件源隻是簡單的将聚合們作為事件進行了持久化。更直接的說,就是使用事件源來作為一種可靠的事件釋出機制。儲存一個事件是一個固有的原子操作,它可以確定事件資料庫(event store)把事件傳遞給感興趣的服務。
例如,如果事件被存儲在上面所示的EVENTS表中,訂閱者可以簡單地輪詢表以查找新事件。更複雜的事件資料庫(event store)将使用另一種做法,這種做法具有更高性能和可擴充性。例如,Eventuate Local使用追加事務日志的方式。它從MySQL replication流中讀取插入到EVENTS表中的事件,并将它們釋出到Apache Kafka。
至于Eventuate Local是個什麼鬼?你可以去github 搜搜。下面放一張圖: <a href="https://s3.51cto.com/wyfs02/M00/8F/8D/wKiom1jkqDugBIgkAAC6YRU-MF8990.jpg" target="_blank"></a>
使用Snapshot改善性能
訂單(Order)聚合具有相對較少的狀态轉換,是以它隻有少量的事件。
是以,針對這些事件查詢事件資料庫(event store)并重構Order聚合,效率是不錯的。然而,一些聚合有很多的事件。例如,客戶(Customer)聚合可能有大量的預留信用(Credit Reserved)事件。随着時間的推移,加載和消費(fold)這些事件的效率會越來越低。
一個常見的解決方案是定期儲存聚合狀态的快照(snapshot)。應用程式通過加載最近的快照然後從快照建立之後發生的那些事件開始來恢複聚合的狀态。
線上商店示例中的客戶(Customer)聚合具有非常簡單的結構:客戶的資訊,他們的信用額度(credit limit)和他們的信用預留(credit reservations)。
客戶(Customer)的快照隻是其狀态的JSON序列化。圖5展現了如何從與事件#103的客戶(Customer)的狀态相對應的快照中重新建立一個客戶(Customer)。客戶服務(Customer Service)隻需要加載快照和加載事件#103後發生的事件。
<a href="https://s4.51cto.com/wyfs02/M00/8F/8B/wKioL1jkqGSBf0KoAAAjN1_j4kE184.jpg" target="_blank"></a>
圖5 – 使用快照來優化性能
客戶服務(Customer Service)通過反序列化快照的JSON後加載并消費#104到#106的事件來重新建立那個客戶(Customer)。
事件源實作
事件資料庫(event store)是資料庫和消息borker的混合體。它是一個資料庫,因為它有一個API,用于通過主鍵插入和檢索聚合的事件。事件資料庫(event store)也是消息broker,因為它具有用于訂閱事件的API。
有一些不同的方法來實作事件資料庫(event store)。
一個做法是編寫自己的事件源架構。例如,您可以在RDBMS中持久化事件。一種簡單的,但性能略低的方式來釋出事件,然後訂閱者輪詢事件的EVENTS表。
另一個做法是使用專用的事件資料庫(event store),它通常能夠提供更豐富的功能以及更好的性能和可擴充性。“事件源”的開發者之一Greg Young有一個基于.NET的開源事件資料庫,稱為Event Store。 Lightbend,這個公司以前叫Typesafe,有一個叫Lagom的微服務架構,是基于事件源的。這裡推薦一個我自己的創業項目,Eventuate,一個用于微服務的事件源架構,你可以把它作為一個雲服務,你也可以把它認為是一個基于Kafka 或RDBMS的開源項目。
事件源的好處與缺點
事件源有好處也有缺點。
事件源的一個主要優點是它可以在聚合的狀态發生變化時可靠地釋出事件。它為事件驅動的微服務架構打下了良好的基礎。而且,由于每個事件都可以記錄進行更改的使用者的身份,是以事件源還提供了一個準确的稽核日志。事件流可用于各種其他目的,包括向使用者發送通知以及應用內建等等。
事件源的另一個好處是它存儲每個聚合的整個曆史。你可以輕松實作檢索聚合的過去狀态的時态查詢。要确定在給定時間點的聚合的狀态,您隻需消費(fold)直到該點為止發生的事件。例如,可以直接計算過去某個時間點客戶的可用信用額。
事件源也避免了O / R阻抗失衡的問題。這是因為它持久化了事件而不是聚合。事件通常具有簡單,容易序列化的結構。服務(service)可以通過序列化其狀态的記錄來對複雜聚合進行快照。 Memento模式在聚合和它的序列化表示之間增加了一個中間層。
有關O/R impedance mismatch: 對象關系阻抗失衡(object-relational impedance mismatch )是當關系資料庫管理系統(RDBMS)由以面向對象的程式設計語言或風格編寫的應用程式(或多個應用程式)服務時經常遇到的一組概念和技術困難,特别是因為對象或類定義必須映射到關系模式定義的資料庫表。
事件源當然不是完美的,它也有一些缺點。它是一個完全不一樣的和而且你可能并不熟悉的程式設計模型,是以要花一些時間去學習。為了使現有應用程式使用事件源,你必須要重寫業務邏輯。幸運的是,這是一個相當機械的轉換,你可以在将應用程式遷移到微服務的時候做這件事情。
事件源的另一個缺點是消息broker通常保證至少一次(at-least once)傳遞。非幂等的事件處理handler必須檢測并丢棄那些重複的事件。事件源架構可以通過為每個事件配置設定單調遞增的id來解決這個問題。事件處理handler然後可以通過對最大事件ID跟蹤來檢測重複事件。
事件源的另一個局限就是事件(和快照!)的schema将随時間發展。 由于事件永久存儲,當服務重建聚合時,服務可能需要折疊與多個schema版本對應的事件。 簡化服務的一種方法是,當事件源架構從事件資料庫(event store)加載它們時,将所有事件轉換為最新版本的模式。是以,服務隻需消費(fold)最新版本的事件。
事件源的另一個缺點是查詢事件資料庫(event store)可能比較困難。讓我們想象一下,例如,您需要找到信用額度較低的客戶。你不能簡單地寫SELECT * FROM CUSTOMERWHERE CREDIT_LIMIT <? AND c.CREATION_DATE>?。因為根本就沒有信用額度(CREDIT_LIMIT)這樣的列。相反,你不得不使用嵌套SELECT的更複雜而且還可能無效的查詢,通過處理和消費(fold)事件來計算信用額度。更糟糕的是,基于NoSQL的事件資料庫(event store)通常隻支援基于主鍵的查找。是以,必須使用“指令查詢責任分離“(CQRS)的方法實施查詢。CQRS 的全稱:Command Query Responsibility Segregation。
我們接下來的内容就是介紹CQRS。
使用CQRS實作查詢
事件源是在微服務體系結構中實作高效查詢的主要障礙。這還不是唯一的問題,還有比如你使用SQL去查找一些高價值訂單的新客戶。
在微服務架構中,你不能join CUSTOMER和ORDER這兩張表。每個表由不同的服務所擁有,并且隻能通過該服務的API通路。你不能編寫連接配接多個服務所擁有的表的傳統查詢。事件源使事情變得更糟,阻礙你編寫簡單,直接的查詢。讓我們來看看在微服務架構中是如何實作類似查詢的。
如何使用CQRS
實作查詢的好方法是使用稱為指令查詢責任分離(CQRS)的體系結構模式: Command Query Responsibility Segregation。如名稱所示,CQRS将應用程式分為兩部分。第一部分是指令側(command-side),其處理指令(例如,HTTP POST,PUT和DELETE)以建立,更新和删除聚合。前提是這些聚合是使用事件源實作的。應用程式的第二部分是查詢側(query-side),其通過查詢聚合的一個或多個物化視圖(materialized views)來處理查詢(例如HTTP GET)。查詢側通過訂閱由指令側釋出的事件來保持視圖(view)與聚合(aggregate)同步。
查詢側(query-side)視圖可以使用任何類型的能滿足需求的資料庫來實作。根據需求,應用程式的查詢端可能使用一個或多個以下資料庫:
表1. 查詢側視圖資料庫選擇
<a href="https://s2.51cto.com/wyfs02/M01/8F/8D/wKiom1jkqJnwtEzzAAHx19GSM5M992.jpg" target="_blank"></a>
在很多場合,CQRS是一個以事件為基礎(event-based)的綜合體,比如使用RDBMS作為記錄系統再使用比如Elasticsearch來處理文本查詢。CQRS的查詢側可以使用其它類型的資料庫,支援多種類型的資料庫,不僅僅是文本搜尋引擎。而且,它通過訂閱事件準實時地去更新查詢側的視圖。
圖6顯示了應用于線上商店示例的CQRS模式。客戶服務(Customer Service)和訂單服務(Order Service)是指令端服務。它們提供用于建立和更新客戶和訂單的API。客戶視圖服務(Customer View Service)是查詢側服務。它提供了一個用于查詢客戶的API。
<a href="https://s3.51cto.com/wyfs02/M02/8F/8B/wKioL1jkqL6g9c1lAAAiQyTdb8U121.jpg" target="_blank"></a>
圖6 – 線上商店中使用 CQRS
客戶視圖服務(Customer View Service)訂閱指令端服務釋出的客戶(Customer)和訂單(Order)事件。它更新那個用MongoDB實作的視圖存儲(view store)。該服務維護一個MongoDB文檔集合,每個客戶一個。每個文檔都具有客戶詳細資訊的屬性。它還具有存儲客戶最近訂單的屬性。此集合支援各種查詢,包括上面說到的那些查詢。
CQRS的好處和缺點
CQRS既有優點也有缺點。 CQRS的一個主要優點是它可以在微服務架構中實作查詢,特别是使用事件源的架構。它使應用程式有效地支援一組不同的查詢。另一個好處就是把指令側和查詢側分離,達到了解耦的作用。
CQRS也有一些缺點。一個缺點就是需要額外的工作來開發和維護這套系統。你需要開發和部署更新和查詢視圖的查詢端服務。還有就是你需要部署視圖資料庫(view store)。
CQRS的另一個缺點是處理指令側和查詢側視圖之間的“滞後”。查詢層相比指令側存在一定的時延。更新聚合,然後立即查詢視圖的用戶端應用程式可能會看到聚合的以前版本。是以必須通過一些手法來避免暴露這些潛在的不一緻性給使用者。
總結
使用事件來維護服務之間的資料一緻性時的主要挑戰是原子級地更新資料庫和釋出事件。傳統的解決方案是使用跨資料庫和消息broker的分布式事務。然而,2PC不是現代應用的可行技術。更好的方法是使用事件源,這是一種以事件為中心的方法來處理業務邏輯設計和持久化。
微服務架構中的另一個挑戰是查詢。查詢通常需要join由多個服務擁有的資料。但是,join不能再使用了,因為資料對每個服務都是私有的。使用事件源還使得更加難以有效地實作查詢,因為目前狀态沒有被顯式地存儲。解決方案是使用指令查詢責任分離(CQRS)并維護可以容易查詢的聚合的一個或多個物化視圖。
<b> 本文轉自yushiwh 51CTO部落格,原文連結:http://blog.51cto.com/yushiwh/1907071</b><b>,如需轉載請自行聯系原作者</b>
<b></b>