1. 程式人生 > >ENode框架Conference案例分析系列之

ENode框架Conference案例分析系列之

前言

本文可能對大多數不太瞭解ENode的朋友來說,理解起來比較費勁,這篇文章主要講思路,而不是一上來就講結果。我寫文章,總是希望能把自己的思考過程儘量能表達出來,能讓大家知道每一個設計背後的思考的東西。我覺得,任何設計的結果可能看起來很高大上,一張圖即可,但背後的思考,才是更有價值的東西。

本篇文章想寫一下ENode如何處理由於業務需求的變化而導致的模型重構的問題。DDD之所以能解決複雜的業務問題是因為DDD是一種模型驅動的軟體設計方法。用領域模型來捕獲業務需求,根據業務需求,抽象出滿足需求的領域模型,並能讓模型隨著業務需求的不斷變化而不斷跟著精煉、演進。我想,歸納起來,對領域模型的重構修改主要為以下四種情況:

  1. 只需要修改業務邏輯而不需要修改模型結構,比如只是修改某個業務規則的判斷邏輯;
  2. 需要對模型結構做一些小的改動,比如新增、改名、刪除一個屬性;
  3. 需要對模型做大手術,比如需要將原來的一個聚合拆分為兩個獨立的聚合;比如電子商務系統中,原本商品的庫存資訊可能在商品聚合根上,但後來由於庫存管理的複雜,需要把庫存獨立到新的上下文(庫存上下文),並獨立維護;
  4. 還有一些情況可能會出現把一個聚合根降級為另一個聚合根下的子實體,或者把一個實體升級為一個獨立聚合根的情況。這種情況我個人認為一般不會出現,如果出現,一般是原來的領域模型設計出現了大問題導致,而這種情況一般也可以通過新增獨立聚合根來解決;

傳統應用面對以上重構的處理方式

傳統的應用是指沒有使用Event Sourcing(ES),比如經典三層架構或經典DDD的四層架構,這些架構通過資料庫存放所有業務資料的最新狀態。這種架構的處理方式是:

  1. 只修改業務邏輯的情況:直接修改邏輯即可,資料庫不需要做修改;
  2. 新增、改名、刪除模型屬性的情況:對於新增屬性的情況,我們只需要在db對應的表中新增欄位,然後新增的欄位使用預設值即可;對於屬性改名的情況,我覺得只需要在程式碼層面修改屬性的名稱即可,而DB中表欄位的名稱無需修改(如果要改欄位名,就是大工程了);對於刪除模型屬性的情況,一般也只需要修改程式碼即可,DB表中的欄位無需刪除,或者你一定要刪除也無影響;
  3. 對於一個聚合根拆分為兩個聚合根的情況:處理比較複雜,一般需要通過新建表、資料遷移、程式碼邏輯切換幾個步驟;
  4. 對於聚合根降級或升級的情況:有時比較簡單,因為雖然程式碼層面,聚合之間的關係改變了,但DB層面,表的結構和關係並沒有改變(一般只要我們的資料庫表設計時是面向第三正規化的,那資料庫往往是穩定的)。這種情況,比較簡單,直接重構程式碼即可;但是有時可能出現也需要對錶結構做修改甚至大改的情況,這種情況,也需要要像3一樣的處理方式;

對於3、4兩種場景中需要做資料遷移和切換的情況,一般都是系統需要大改了,一般不會經常發生。如果出現了,那我們要坐下來,根據實際的情況,好好想想要怎麼解決。架構師要設計好程式碼重構方案、資料遷移方案,以及資料訪問的切換方案。如果切換失敗,還要支援快速的回滾;總之,這是一個大工程。好在我們現在已經有一些成熟的甚至自動化的資料遷移方案(可複用的)或工具,可以幫助我們極大的簡化重構的改造成本。

使用ES(Event Sourcing,事件溯源)的架構的處理方式

使用ES的系統,應該承認,重構時要面對的問題要比傳統的應用要複雜很多。比如,資料庫中除了儲存了聚合根的最新狀態,還多了一個EventStore,EventStore中儲存了每個聚合根產生的所有歷史事件。另外,由於是EDA(事件驅動架構),所以,資料不僅僅在DB中,還有一部分訊息(command或event)還在佇列裡,還沒有被處理。所以,我們還要處理這些還留在佇列裡的訊息。所以,ES架構的應用,在資料遷移或切換時,除了要遷移CQRS讀庫中的聚合根的最新狀態的資料,還要考慮額外的兩個東西:1)EventStore中事件的遷移;2)Queue中未被處理的訊息的處理;所以,ES架構的應用,在處理這種大改動時,要做的方案可能比傳統的要複雜很多。這也是我想寫本文的目的,希望通過一個虛構的例子(實際生活中也可能會發生),來分析ES架構下,我們怎麼處理這種情況,希望能給未來使用ES架構開發應用的朋友提供一些借鑑意義。

然後針對上面的4種重構的情況,1比較簡單,也只需要改動產生事件的地方的邏輯即可;2也比較簡單,只需要修改對應的事件,在事件類中增加或刪除屬性即可,反正JSON序列化和反序列化都是相容的;3,4兩種情況比較複雜,我們資料遷移時要考慮的東西比傳統的架構要多。我上面已經提到了。

拆分聚合根的例子

經過前面幾篇文章的介紹,我們知道conference案例中,conferenceManagement上下文(會議管理上下文)負責管理會議的基本資訊和會議的所有座位的庫存資訊。但是假設我們未來隨著庫存管理的複雜,希望把庫存管理的邏輯剝離到獨立的上下文,會議座位庫存管理上下文(Bounded Context, BC)。這種情況,相當於把原來的conference聚合根拆分為兩個聚合根,原conference聚合根只負責維護會議的基本資訊,不再維護座位的庫存資訊;然後庫存資訊交給一個獨立的聚合根(conferenceSeatInventory)維護。一個conference聚合根會有一個唯一的conferenceSeatInventory聚合根對應,但conferenceSeatInventory聚合根的ID可以獨立,然後conference聚合根的ID可以作為conferenceSeatInventory聚合根的一個屬性。就像Payment聚合根上有一個OrderId一樣,呵呵。這樣拆分後,我們的會議管理的上下文只需要關注會議本身的基本資訊,包括UI的設計也是一樣。然後專門有庫存管理的系統(包括UI),負責管理會議的座位的庫存資訊。

這個拆分聚合根的場景是我假想出來的,實際也許並不會發生,也許會發生,取決於我們的業務如何發展。就像阿里的庫存中心也許最早的時候並沒有獨立出來,而是和商品中心在一起的。但後來隨著對庫存管理的重視或者庫存管理(尤其是減庫存)的本身業務的不斷複雜,我們把庫存管理獨立出來了。

模型重構

在做資料遷移之前,我們先要把新的庫存管理的上下文做好併發布上線。但是此時,這個上下文還不會有什麼命令過來。

資料遷移和切換的思路

使用ENode開發的應用程式是一個CQRS+EDA+ES的架構。然後,我們也知道了,這種架構下,我們要遷移的資料有以下三種:

  1. EventStore中的事件;
  2. CQRS讀庫中的資料;
  3. 分散式佇列(EQueue)中還沒有被消費的訊息:命令、事件;

我的思路是:

  1. 首先把資料遷移的影響範圍降低到最小。所以,我們可以先把所有目前和conference聚合根的庫存變化相關的命令和事件訊息隔離到獨立的佇列中去。這個事情很簡單,因為ENode目前使用的分散式訊息佇列是EQueue,而EQueue傳送訊息或訂閱訊息都是基於Topic的。當我們要傳送一個訊息時,需要告訴EQueue當前要傳送到哪個Topic下的哪個Queue;當我們要訂閱訊息時,也是告訴EQueue要訂閱哪些Topic。所以,我們可以為conference聚合根和庫存變化相關的命令、事件配置獨立的topic即可(比如叫ChangeConferenceInventoryCommandTopic,ConferenceInventoryChangedEventTopic)。這樣一來,我們只需要從這兩個獨立的topic著手進行資料遷移即可。
  2. 新增一個獨立的Event Handler,它負責訂閱conference聚合根的庫存變化相關的事件,即訂閱topic為ConferenceInventoryChangedEventTopic的事件。它的處理邏輯是根據事件,建立並持久化新的和新的聚合根(conferenceSeatInventory)對應的庫存修改事件到EventStore。但是這裡的情況比較複雜,我需要詳細展開下。比如當前我們收到一個事件(事件版本號可能為10),然後發現這個事件對應的conference聚合根還沒有對應的conferenceSeatInventory對應的任何事件,因為是第一次同步。那此時我們需要把conference聚合根的所有以前的歷史事件(版本號為1到10的所有事件)都從EventStore中拿出來,然後把和庫存變化相關的事件轉換為對應的新的事件,然後把這些新的事件全部儲存到EventStore中。這裡有一個問題,就是我們怎麼知道當前聚合根的之前的事件是否都已經同步好了呢?一個簡單的辦法是,每次一個事件過來,我們都從EventStore中查詢當前是否已經把這個事件所有之前的事件都同步過了,如果是,那就可以直接同步當前這個事件了。如果不是,那就需要先把之前的事件同步完成。但是這樣無疑是低效的。實際上我們可以在當前機器的記憶體中儲存每個聚合根當前已經同步了的事件的版本號。也就是用一個ConcurrentDictionary來儲存每個聚合根的已同步事件的版本號。然後,判斷時,只需要根據這個字典進行判斷即可,這樣無疑效率提高很多。但是為何只需要把同步進度快取在當前機器的記憶體中呢?為何不是採用分散式快取呢?因為ENode中的所有的命令或事件訊息在路由時,都是根據聚合根ID進行路由的。同一個聚合根ID的訊息都是總是被路由到同一個Queue的。而根據EQueue的架構,同一個Queue的消費者總是同一個。所以,我們可以這樣做。
  3. 上面的Event Handler只處理了conference聚合根產生的新事件。但是那些以前產生過庫存修改事件,但是近期沒產生過的聚合根怎麼辦呢?我們需要通過開發一個事件同步任務(Event Sync Task),該任務掃描EventStore中所有conference聚合根的所有和庫存修改相關的事件,並把這些事件轉換為新的聚合根的庫存修改的事件。可以很容易發現,這個任務和上面的Event Handler可能會出現併發衝突。這種併發衝突是預期之內的,我們可以通過技術手段來解決。比如我們可以通過以被同步的conference聚合根的事件的版本號來做樂觀併發控制。當目標的EventStore中持久化同一個目標聚合根(conferenceSeatInventory)的相同版本的事件時,就丟擲ConcurrentException即可。然後我們程式碼中,當遇到這種併發異常時,只需要查詢最新以同步的事件版本,然後嘗試同步這個事件版本之後的事件即可。這裡有一個問題需要提一下,就是EventStore資料庫的Events表可能存放的是所有的聚合根的所有的事件,這樣這個Events表中的資料是非常多的。所以,我們通常在設計EventStore的Events表時,儘量先做一層業務層面的垂直拆分。即把不同型別的聚合根所產生的事件隔離儲存,比如conference聚合根的事件都放在conferenceEvents表裡;payment聚合根的所有事件都放在paymentEvents表裡。這樣一來,我們的事件表裡的記錄就不會那麼多了。但是即便是一個聚合根的所有事件,可能也是非常多的。那我們就需要採用水平分割了,即把同一個聚合根的所有的事件再進行分庫分表。這個是題外話了,這本只是提一下。我們這裡的目的是關注在同步已有的conferenceEvents表中的所有的事件,將這些事件轉換為新的聚合根的事件,並持久化到新的事件表中。驚喜:寫到這裡突然發現,有著這個事件同步的任務,那上面這個Event Handler就可以不需要了哦,因為我本來計劃這個同步任務只負責一次性同步所有的歷史事件,而不同步後續新增的事件的。但是通過我剛才的描述,大家知道這個同步任務是會定時同步後續新增的事件的。這就意味著上面的Event Handler沒有存在的必要了哦,大家覺得是不是呢?
  4. 經過2,3兩步,我們確保了最後conferenceEvents表中的所有歷史事件和不斷新增的增量事件都會自動同步到新的事件表(conferenceSeatInventoryEvents)了。也就是我們上面的目標中的目標1;接下來我們開始思考如何生成庫存聚合根的CQRS的讀庫資料的同步。庫存聚合根的讀庫資料的更新比較簡單,只需要設計另一個獨立的事件掃描任務(Event Scan Task),該任務的職責是掃描conferenceSeatInventoryEvents表,掃描當前所有未掃描過的後續事件,並逐個更新到讀庫。我們需要在記憶體中記錄當前掃描到哪條記錄了(並需要定時把掃描進度儲存起來,比如寫到某個檔案裡,已應付這個掃描任務意外停止或者機器意外關機的情況,這樣的話我們就可以從檔案載入上次掃描的最後一個位置,從那個位置之後再繼續掃描),然後每次從哪條記錄之後繼續掃描即可。一次可以掃描最多1000條記錄,間隔10s掃描一次。這些我們可以自己配置。看過ENode事件表設計的朋友應該知道,ENode設計的事件表,有一個自增的Sequence欄位,該欄位設計的目的就是用於事件的增量掃描或同步的,呵呵。如果沒有Sequence欄位,我們就無法知道記錄新增進表的全域性順序了。好了,通過這個掃描任務,我們也就可以通過非同步的方式,最終確保conferenceSeatInventory聚合根的讀庫會更新了。
  5. 接下來我們思考如何處理佇列中還未被消費的命令和事件。為什麼要考慮這個問題?因為我們的要求是希望可以做到儘量不停機發布的。也就是說,這個資料的遷移以及切換的過程儘量對使用者透明。而要在做切換時,我們必須確保新老聚合根的庫存相關的事件以及讀庫的狀態必須完全一致的。否則,當切換到新的聚合根時,由於老的聚合根中還有一些命令或事件還沒被處理,那就會導致我們切換到新的聚合根後,用的是舊(過時)資料,這樣就不對了。那怎麼解決這個問題呢?由於前面第一步,我們已經把相關的topic獨立了出來,有:ChangeConferenceInventoryCommandTopic、ConferenceInventoryChangedEventTopic這兩個topic。那我們只要確保這兩topic下的所有的訊息都消費完成就行了。但是隻要還有新的訊息進入到這兩個topic的queue,那就不可能有這個時候。幸好EQueue在設計之初,就考慮了這種情況,EQueue支援禁用某個Queue,禁用後,就不允許訊息傳送者往這個Queue中發訊息了,但是這個Queue中的訊息還可以允許被消費。所以,當我們想開始做切換前,可以先把這兩個topic的所有Queue都禁用掉。然後就確保了不會有新的訊息能傳送成功。然後只需要等待短暫的幾秒,等待這兩個topic下的所有的訊息都被消費完(可以通過檢視EQueue管理控制檯上佇列的消費進度知道是否訊息都消費完了),且上面所說的2,3,4三個步驟也都處理了所有的事件,那就意味著新老聚合根的所有庫存相關的事件和讀庫資料都已經完全一致了。然後我們就可以進行切換了。
  6. 怎麼切換呢?只需要把傳送命令的源頭進行切換即可。就是把原來發給conference聚合根的修改庫存的命令,現在改為傳送給conferenceSeatInventory聚合根的修改庫存的命令。我們可以把所有可能會發送相關這些命令的伺服器部署新程式碼然後重新發布一下即可。當然這個釋出過程可能也需要幾分鐘時間。所以,大家可以看到,整個切換的過程,可能需要幾分鐘左右的時間。

總結

上面的思路我想了好幾天,真是費了我不少的腦細胞。傳統的面向DB的資料遷移方案,一般也有全量資料遷移和增量資料遷移,以及最後的切換操作。

而ENode在處理這種情況時,由於整個架構是EDA的架構,在資料遷移時我們正好可以利用EDA架構的優點(當什麼事件發生時,外部可以被通知到,並且我們可以隨時新增額外的Event Handler來做額外的事情)。上面的幾步思路,就是利用了這種思路,從而可以做到對這個程式無侵入的前提下完成資料的遷移和系統的切換。

另外一點,不知道大家發現沒,上面的資料切換其實也沒想象中那麼複雜,仔細想一下,我們只需要同步事件,並把同步後的事件再進一步更新讀庫,最後禁用訊息佇列,再等待訊息佇列中的訊息都消費完成,最後切換即可。為什麼呢?因為事件的不變性。我們的事件表裡的記錄是絕對不會變的,不像傳統的DB的資料遷移方案,表中的每一行記錄都有可能變化或者刪除,這就會資料遷移帶來很大的障礙。而ES的架構,因為事件記錄的不變性,所以也讓我們在進行資料遷移時,能夠變得簡單。而且,我們也合理的利用了CQRS架構的特性,即讀庫的更新是完全根據事件表的。所以,只要我們確保事件表都絕對完全同步完成了,那總能確保最後的讀庫也能同步完成。這點也是非常爽的。

最後我在思考的問題是,是否有可能把切換過程縮短到幾秒呢?呵呵。應該是可以的,就是我們可以預先把所有要修改的程式碼先改好,然後通過某個配置項來決定當前要執行哪個程式碼,傳送哪個命令。然後當需要切換時,我們只需要修改配置項即可。這個可以通過zookeeper等工具實現即可。這樣整個切換過程就只需要短短几秒即可,對使用者不會造成很大影響。而且如果發現切換過去有問題,還可以隨時再切換回來。做到方案的可回滾性。

最後,大家可以再想想這個資料遷移和切換的方案,哪些地方可以被複用呢?

相關推薦

ENode框架Conference案例分析系列

前言 本文可能對大多數不太瞭解ENode的朋友來說,理解起來比較費勁,這篇文章主要講思路,而不是一上來就講結果。我寫文章,總是希望能把自己的思考過程儘量能表達出來,能讓大家知道每一個設計背後的思考的東西。我覺得,任何設計的結果可能看起來很高大上,一張圖即可,但背後的思考,才是更有價值的東西。 本篇文章想寫

Java分析系列五:常見的Thread Dump日誌案例分析

目錄 [隱藏] 症狀及解決方案 下面列出幾種常見的症狀即對應的解決方案: CPU佔用率很高,響應很慢 按照《Java記憶體洩漏分析系列之一:使用jstack定位執行緒堆疊資訊》中所說的方法,先找到佔用CPU的程序,然後再定位到對應的執行緒,最後分析出對應的堆疊資訊

[gitbook] Android框架分析系列Android stagefright框架

請支援作者原創: https://mr-cao.gitbooks.io/Android/content 點選開啟連結 本文以Android6.0系統原始碼為基礎,分析Android

layui框架詳細分析系列框架主體組織結構

layui框架主體 今天正式的進入框架主體部分的學習與分析,該框架開源從GitHub上clone下來的原始碼主要的部分就是src部分,該部分主要的目錄結構構成如下: 從上圖可以看出css儲存樣式,font儲存圖示(iconfont), image

R語言數據分析系列

r語 來看 tab barplot code 繪制 ber map lib R語言數據分析系列之五 —— by comaple.zhang 本節來討論一下R語言的基本圖形展示,先來看一張效果圖吧。 這是一張用R語言生成的,虛擬的wordcloud雲圖,詳細

nova創建虛擬機源碼分析系列七 傳入參數轉換成內部id

接口 函數 device 博文 nat build 消息 通過 rop 上一篇博文將nova創建虛機的流程推進到了/compute/api.py中的create()函數,接下來就繼續分析。 在分析之前簡單介紹nova組件源碼的架構。以conductor組件為例: 每個組件

nova創建虛擬機源碼分析系列八 compute創建虛機

alt 創建 put manager 信息 模塊 manage tor float /conductor/api.py _build_instance() /conductor/rpcapi.py _build_instance() 1 構造一些數據類型2 修改一些a

Java分析系列四:jstack生成的Thread Dump日誌執行緒狀態

前面文章中只分析了Thread Dump日誌檔案的結構,今天針對日誌檔案中 Java EE middleware, third party & custom application Threads 部分執行緒的狀態進行詳細的分析。 目錄 [隱藏] 1 Thread Dump日誌

Java分析系列三:jstat命令的使用及VM Thread分析

前面提到了一個使用jstack的shell指令碼,通過命令可以很快地定位到指定執行緒對應的堆疊資訊。 目錄 [隱藏] 1 使用jstat命令 2 JVM記憶體模型 3 JVM記憶體引數設定 3.1 堆記憶體設定 3.2 非堆記憶體設定

分析系列二:jstack生成的Thread Dump日誌結構解析

上一篇文章講述瞭如何使用jstack生成日誌檔案,這篇文章首先對Thread Dump日誌檔案的結構進行分析。 目錄 [隱藏] 1 第一部分:Full thread dump identifier 2 第二部分:Java EE middleware, third party &a

Dubbo 原始碼分析系列三 —— 架構原理

1 核心功能 首先要了解Dubbo提供的三大核心功能: Remoting:遠端通訊 提供對多種NIO框架抽象封裝,包括“同步轉非同步”和“請求-響應”模式的資訊交換方式。 Cluster: 服務框架 提供基於介面方法的透明遠端過程呼叫,包括多協議支援,以及

金融量化分析-python量化分析系列---使用python獲取股票歷史資料和實時分筆資料

財經資料介面包tushare的使用(一) Tushare是一款開源免費的金融資料介面包,可以用於獲取股票的歷史資料、年度季度報表資料、實時分筆資料、歷史分筆資料,本文對tushare的用法,已經存在的一些問題做一些介紹。 一:安裝tushare 為避免由於依賴包缺失導致安裝失敗,請先安裝anaconda,

Tomcat 原始碼分析系列環境搭建

Tomcat 原始碼環境搭建 tomcat 9 和 idea 環境搭建 環境準備 JDK 1.10 git idea tomcat 原始碼 maven ant 國內的maven 倉庫映象 安裝Intellij Idea 新

jQuery2.0.3原始碼分析系列(29) 視窗尺寸

.width() 基礎回顧 一般的,在獲取瀏覽器視窗的大小和位置時,有以下幾個屬性可以使用: 在不同的瀏覽器中,以下12個屬性所代表的意義也是不一樣的 特別需要注意的是,當使用或者不使用<!DOCTYPE>宣告顯示一個文件的時候,以上12個屬性的意義也會發生變化。 特在IE 9中

Bootstrap原始碼分析系列核心CSS

本節主要介紹核心CSS,從整體架構中的7個Less檔案對應的原始碼分別進行分析 scaffolding.less 這個檔案編譯後的css檔案(886~989行)其作用就像定義全域性樣式。 //調整css盒模型為border-box,這樣修改使得新增padding不至於元

Bootstrap原始碼分析系列初始化和依賴項

在上一節中我們介紹了Bootstrap整體架構,本節我們將介紹Bootstrap框架第二部分初始化及依賴項,這部分內容位於原始碼的第8~885行,開啟原始碼這部分內容似乎也不是很難理解。但是請站在一個開發者的角度來面對這段原始碼。為什麼要這樣寫?如果沒有Bootstrap

spark高階資料分析系列第二章用 Scala 和 Spark 進行資料分析

2.1資料科學家的Scala spark是用scala語言編寫的,使用scala語言進行大資料開發的好處有 1、效能開銷小 減少不同環境下傳遞程式碼和資料的錯誤和效能開銷 2、能用上最新的版

spark高階資料分析系列第三章音樂推薦和 Audioscrobbler 資料集

3.1資料集和整體思路資料集本章實現的是歌曲推薦,使用的是ALS演算法,ALS是spark.mllib中唯一的推薦演算法,因為只有ALS演算法可以進行並行運算。使用資料集在這裡,裡面包含該三個檔案:表一:user_artist_data.txt   包含該的是(使用者ID、歌

SAP-MM-PA精解分析系列基本介紹(01)-採購基本流程

MM基本知識(01)-採購基本流程        業務舉例:        物料在企業中的採購管理,涉及多種渠道和方式,一般來說,企業的採購一部分來自於外部供應商,一部分來自於自己公司下的其他分支部門。在這些採購過程中,涉及到的部門功能有采購、倉庫管理、發票校驗。     

時間序列(time serie)分析系列時間序列特徵(feature)7

文章目錄 1.問題描述 2.特徵構建 2.1時間特徵 2.2平移特徵 2.3視窗特徵 3.總結 1.問題描述 時間序列資料作為一種典型的資料,常存在於各行各業。比如客流、車流、銷量、KPI指標等等