微服務應用-基於Spring Cloud和Reactor構建網上商店微服務(下)
前言
上篇主要是講解理論知識和專案架構要點,這篇將集中在微服務中使用Spring Boot、Spring Cloud和Project Reactor實現事件溯源的原始主題。文章中也會介紹專案實現一些技術細節,專案Git下載地址:https://github.com/kbastani/spring-cloud-event-sourcing-example,專案我自己已經運行了一遍,非常適合學習使用。下面是原文翻譯內容:
Project Reactor
Project Reactor 是一個開源基於JVM實現Reactive流規範的開發框架,是Spring生態系統一個成員,在微服務中,經常在一個上下文下需要和其他微服務互動操作,由於微服務架構天然屬性是最終一致性,而最終一致性並不保證資料的安全性。它提供我們一個使用非同步非堵塞方式進行通訊的方式,這裡正是使用Reactor目的所在。
很少情況下,領域模型狀態會被跨微服務共享,但是如果在微服務之間需要共享狀態怎麼辦?或者說多個微服務需要訪問同一個資料庫資料表怎麼辦?在微服務中ES只儲存有序事件的日誌,使用事件流取代領域模型在資料庫中儲存,我們可以儲存有序事件流來代表物件的狀態,這樣,意味著我們就不再使用基於HTTP的RESTful進行微服務之間同步通訊,這些同步會造成堵塞和效能延遲。
Reactor為核心的事件溯源
在網上商店的微服務之一是購物車服務(Shopping Cart Service),已驗證的使用者從商店Web應用程式的使用者介面瀏覽產品目錄。使用者可以新增和刪除他們的購物車的商品條目,以及清除他們的購物車或結帳。
一個使用者的購物車為如何實現事件追溯工作描繪了一個簡單的樣例。我們以網上商店中購物車服務為案例,展示Reactor + ES是如何實現的:
購物車服務Shopping Cart Service是一個MYSQL資料庫擁有者,有一個數據表稱為cart_event。這個表包含使用者操作動作產生的有序事件日誌,使用者操作就是反覆將商品加入購物車或去除等各種購物車管理操作。
Example 1. CartEventType.java // These will be the events that are stored in the event log for a cart public enum CartEventType { ADD_ITEM, REMOVE_ITEM, CLEAR_CART, CHECKOUT }
CartEventType是列舉型別,已經列出了4種不同的事件型別。這些事件型別中的每一個都代表使用者在購物車上執行的動作。根據ES,這些購物車事件可以影響使用者的購物車的最終狀態結果。當用戶新增或刪除一個商品條碼到他們的購物車時,一個動作產生一個事件,會對購物車中進行遞增或遞減一行條目。當這些事件使用同樣順序進行回放時,同樣一系列的條目會被重新建立或刪除:
id created_at last_modified cart_event_type product_id quantity user_id
1 1460990971645 1460990971645 0 SKU-12464 2 0
2 1460992816398 1460992816398 1 SKU-12464 1 0
3 1460992826474 1460992826474 0 SKU-12464 2 0
4 1460992832872 1460992832872 0 SKU-12464 2 0
5 1460992836027 1460992836027 1 SKU-12464 5 0
我們看到每行都有一個唯一時間戳來確保嚴格順序,使用整數來代表4個購物車事件型別,product_id 和資料quantity都是每次加入購物車的商品條碼資訊。
這一結果顯示在上面的截圖,在這裡,我們看到一個使用者的購物車,生成作為一個總結果集的物件。
選擇事件儲存庫
當選擇事件追溯的適當儲存選項時,有很多可用的選項。今天幾乎所有的資料庫都能提供資料流查詢功能的工作,然而,有一些流行的開源專案,在這方面有著突出的優點。現在Event Sourcing標準的儲存庫是 Apache Kafka,微服務之間共享狀態是通過共享Kafka的事件日誌實現的,這是一個未來部落格的主題。在這個例子中我們將使用MySQL,這是實現事件追溯一個線上購物車的不錯選擇。
您的事件儲存技術的選擇將永遠取決於寫入的數量和您的資料庫的吞吐量。像Apache Kafka設計了精確的使用情況,卻要求我們承擔一些額外的工作責任去在生產中擴充套件,包括執行Apache ZooKeeper叢集。
生成聚合
下面我們回到購物車,購物車微服務提供一個REST API方法接受來自Web端的事件。Web端發出事件的控制器 ShoppingCartControllerV1.java
Example 2. ShoppingCartControllerV1.java
@RequestMapping(path = "/events", method = RequestMethod.POST)
public ResponseEntity addCartEvent(@RequestBody CartEvent cartEvent) throws Exception {
return Optional.ofNullable(shoppingCartService.addCartEvent(cartEvent))
.map(event -> new ResponseEntity(HttpStatus.NO_CONTENT))
.orElseThrow(() -> new Exception("Could not find shopping cart"));
}
在上面的程式碼示例,我們定義了一個用於收集來自客戶端新的CartEvent物件的控制器方法。這種方法的目的是在向事件日誌追加事件。當客戶端呼叫REST API檢索使用者的購物車,它將產生一個購物車聚合,使用Reactive流合併了所有購物車事件流。
下面在ShoppingCartServiceV1.java中使用Reactor產生購物車事件流:
Example 3. ShoppingCartServiceV1.java
public ShoppingCart aggregateCartEvents(User user, Catalog catalog) throws Exception {
// Create a reactive streams publisher by streaming ordered events from the database
Flux<CartEvent> cartEvents =
Flux.fromStream(cartEventRepository.getCartEventStreamByUser(user.getId()));
// Aggregate the current state of the shopping cart until arriving at a terminal state in the stream
ShoppingCart shoppingCart = cartEvents
.takeWhile(cartEvent -> !ShoppingCart.isTerminal(cartEvent.getCartEventType()))
.reduceWith(() -> new ShoppingCart(catalog), ShoppingCart::incorporate)
.get();
// Generate the list of line items in the cart from the aggregate
shoppingCart.getLineItems();
return shoppingCart;
}
在上面的程式碼示例中,我們可以看到三個步驟來生成購物車物件,然後返回到客戶端。第一步是從事件儲存的資料來源中建立一個Reactive流。一旦流建立,我們可以從事件流中產生我們的聚合。這些事件流不斷改變購物車狀態直至到最終狀態,然後就可以將最終購物返回給使用者客戶端。
為了減少reactive流的聚合,我們使用了一個稱為incorporate方法,這個方法是接受CartEvent物件,而CartEvent物件是用來改變購物車狀態的。
Example 4. ShoppingCart.java
public ShoppingCart incorporate(CartEvent cartEvent) {
// Remember that thing about safety properties in microservices?
Flux<CartEventType> validCartEventTypes =
Flux.fromStream(Stream.of(CartEventType.ADD_ITEM,
CartEventType.REMOVE_ITEM));
// The CartEvent's type must be either ADD_ITEM or REMOVE_ITEM
if (validCartEventTypes.exists(cartEventType ->
cartEvent.getCartEventType().equals(cartEventType)).get()) {
// Update the aggregate view of each line item's quantity from the event type
productMap.put(cartEvent.getProductId(),
productMap.getOrDefault(cartEvent.getProductId(), 0) +
(cartEvent.getQuantity() * (cartEvent.getCartEventType()
.equals(CartEventType.ADD_ITEM) ? 1 : -1)));
}
// Return the updated state of the aggregate to the reactive stream's reduce method
return this;
}
在上面程式碼中我們看到ShoppingCart的incorporate方法實現,我們接受一個CartEvent物件然後做最重要的一件事:確保事件型別是正確的。這是在微服務需要自由與他們的單元測試,在最終一致的架構,以確保狀態突變將確保資料的正確性。在本例中,我們確保事件型別是 ADD_ITEM 或 REMOVE_ITEM。
下一步是更新購物車中每個條目的聚合檢視,通過對映相應的事件型別到商品條目的數量遞增或遞減。最後我們返回這樣一個帶有最終可變狀態的購物車給客戶端。
Docker Compose演示
示例專案使用Docker Compose構建和執行,每個微服務的容器映象都作為Maven編譯過程的一部分。
入門
首先,訪問該示例專案的GitHub庫:
https://github.com/kbastani/spring-cloud-event-sourcing-example
克隆或複製專案並將儲存庫下載到您的機器上。下載後,您將需要使用Maven和Docker來編譯和構建本地映象。
下載Docker
首先,如果你還沒有下載Docker。可遵循這裡的指示https://www.docker.com/docker-toolbox,把Docker toolbox在你的開發機器上執行。
在你安裝了Docker toolbox以後,執行下面的命令來初始化這個示例應用程式的一個新的VirtualBox虛擬機器。
$ docker-machine create env event-source-demo --driver virtualbox --virtualbox-memory "11000" --virtualbox-disk-size "100000"
$ eval "$(docker-machine env event-source-demo)"
環境要求
能夠執行例項程式,需要在你的開發機上安裝下面的軟體:
- Maven 3
- Java 8
- Docker
- Docker Compose
構建專案
通過命令列方式來構建當前專案,在專案的根目錄中執行如下的命令:
$ sh run.sh
該專案將下載所有所需的依賴關係,並編譯每一個專案的元件。每個服務都將被構建,然後一個Maven Docker外掛會自動構建每個映象到你的本地Docker登錄檔裡。在根目錄執行命令之前,一定要保證Docker正常執行,只有這樣,你的sh run.sh命令才會構建成功。
Docker Compose啟動叢集
現在,每個映象都已經搭建成功,我們可以使用Docker Compose快速啟動叢集。run.sh指令碼將建立每個專案和Docker容器,並使用Docker Compose開啟每個服務。值得注意的是,叢集需要首先啟動的服務是配置服務和發現服務。其餘的服務在開始啟動,並最終開始相互溝通。
我強烈建議你執行此示例的機器上至少16GB的系統記憶體。
一旦啟動序列完成後,你可以瀏覽到Eureka主機和看到發現服務裡註冊的所有服務。複製並貼上以下命令到終端,Docker可以使用$DOCKER_HOST環境變數進行訪問。
$ open $(echo \"$(echo $DOCKER_HOST)\"|
\sed 's/tcp:\/\//http:\/\//g'|
\sed 's/[0-9]\{4,\}/8761/g'|
\sed 's/\"//g')
如果Eureka正確的啟動,瀏覽器將會啟動並開啟Eureka服務的儀表盤,如下圖所示:
當所有的應用程式在Eureka上已經完成啟動和註冊,你可以使用以下命令訪問網上商店的Web應用。
$ open $(echo \"$(echo $DOCKER_HOST)\"|
\sed 's/tcp:\/\//http:\/\//g'|
\sed 's/[0-9]\{4,\}/8787/g'|
\sed 's/\"//g')
應用程式啟動可能需要一些時間,所以請確保你每隔幾分鐘重新整理使用者介面,直到看見產品目錄。
要開始向購物車新增產品,您需要登入預設使用者。單擊Login按鈕,你將被重定向到身份驗證閘道器頁面,在此頁面你可以使用使用者的預設憑據和密碼進行登入。
登入成功後,你將被重定向到需要身份驗證才能呈現的主頁面上,並可以開始管理你的購物車中的專案。
總結
在這篇文章中,我們很難看到在微服務架構中的高可用性和資料一致性的挑戰。我們期待一個完整的網上商店原生雲應用程式,能作為一系列微服務的集合,使用事件追溯保持一致的世界觀,同時還保證高可用性。
在接下來的部落格,我將繼續探索如何在事件追溯中使用Spring Cloud Stream和用Apache Kafka對事件流處理。
特別感謝
省略。