原文地址:http://skaka.me/blog/2016/08/25/springcloud4/
在上一篇文章中我們開發了一個用戶註冊服務. 這篇文章我將介紹如何開發mysteam訂單服務中的下單功能, 下單功能會涉及服務之間的交互與事件的處理, 並且我會對開發過程中用到的框架和類庫進行簡單地講解. 開始寫代碼之前, 我們先來看看下單的處理流程:
其中1,2,3,4,11步的黑色箭頭代表是同步操作, 5,6,7,8,9,10步是異步操作. 下單接口接收要訂購的產品ID, 數量和要使用的優惠券ID, 然後調用產品服務的接口查詢產品信息, 調用優惠券接口校驗優惠券是否有效, 以及調用賬戶接口判斷賬戶金額是否足夠(mysteam是一個虛擬物品商城, 採用先充值後購買的形式). 如果這些校驗都成功, 訂單服務會發送賬戶扣款事件和優惠券使用事件到MQ, 賬戶服務和優惠券服務會從MQ讀取事件進行處理,
如果處理成功, 訂單服務將能接收到結果, 並且將訂單狀態置爲下單成功
,
如果處理失敗或超時, 訂單狀態會被置爲下單失敗
.
1. 實現Model
流程清楚了, 現在我們來看代碼. 訂單類是$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/domain/Order.java
,
訂單類和前面的用戶類類似, 其中有兩個字段需要注意一下:
OrderStatus是一個枚舉, 表示訂單狀態. OrderItem是訂單項.
2. 實現DAO
DAO層基本沒有實際的代碼, 就不貼了.
3. 實現Service
下單的業務邏輯都在service內, 打開$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/service/OrderService.java
,
找到placeOrder
方法:
代碼略長, 我略過了其中一部分代碼. placeOrder
方法主要做的事情有:
1.根據訂購的產品ID, 向產品服務查詢產品信息, 並計算訂單金額.
2.如果有使用優惠券, 向優惠券服務查詢優惠券是否有效(請求REST接口).
3.根據訂單金額, 向賬戶服務查詢用戶餘額是否足夠(請求REST接口).
4.如果上述步驟都成功完成, 發送賬戶扣款事件以及優惠券使用事件(如果有優惠券), 並註冊回調方法等待事件結果.
如果事件處理成功會調用markCreateSuccess
方法,
處理失敗會調用markCreateFail
, markCreateSuccess
方法的代碼如下:
這個方法只是將訂單狀態置爲下單成功
,
流程就完成了. markCreateFail
處理過程類似,
只不過是將訂單狀態改爲下單失敗
.
看到這裏, 先不去管服務調用的實現細節, 細心的你可能會產生一些疑問:
1.第4步爲什麼要使用事件的形式去扣款和處理優惠券, 不能和前面的查詢操作一樣使用REST接口來處理嗎?
2.爲什麼要先查詢用戶餘額是否足夠, 再發送扣款事件, 直接發送扣款事件不好嗎? 如果查詢餘額返回成功之後, 其他業務修改了餘額, 處理扣款事件的時候餘額不足怎麼辦?
3.第4步同時發送了扣款事件和優惠券使用事件, 如果扣款成功了, 但是優惠券使用失敗了怎麼處理?
其實這些問題都指向同一個問題域: 分佈式事務. 分佈式事務是開發微服務首先要解決的問題. 分佈式事務是一個很大的話題, 這裏我只簡單介紹一下eBay的Dan Pritchard提出的BASE原則:
基本可用(Basically Available)
軟狀態(Soft state)
最終一致(Eventually consistent)
BASE其實和傳統數據庫的ACID是兩個不同的思想, 以我們上面的訂單系統爲例. 訂單服務向賬戶服務發送扣款事件, 賬戶服務接收到事件並且處理成功, 但是還沒有將處理結果發送到訂單服務, 這時候系統數據就處於短暫的不一致狀態: 用戶的賬戶餘額已經被扣減掉了, 但是訂單狀態還是正在下單
.
過了一段時間, 訂單服務獲取到扣款事件的處理結果並且將訂單狀態置爲下單成功
.
這個時候系統才達到最終一致的狀態. 這種事務處理方法並不是適用於所有業務, 如果需要強一致性, 還是得使用2PC或者3PC來完成.
瞭解了mysteam的事務處理原則, 我們回頭看看剛纔提出的3個問題:
1.mysteam是使用事件的方式來進行事務處理的, REST接口一般只用來實現查詢或者其他不需要事務的操作. 所以只要涉及到數據修改, 一般都通過事件來完成.
2.發送扣款之前先查詢餘額是爲了減少不必要的事件操作, 因爲如果事件處理失敗會涉及到事件撤銷, 是比較耗時的操作, 先進行餘額查詢, 餘額不足直接流程就中止了. 根據我們的經驗, 一般來說查詢餘額成功後續扣款失敗的機率比較小, 所以收益大於付出. 3.這涉及到事件的撤銷處理. 在mysteam的訂單服務中, 如果接收到了扣款成功和優惠券使用失敗這兩個事件結果, 訂單服務會啓動事件撤銷流程, 向賬戶服務發送扣款撤銷事件, 並且將訂單狀態置爲下單失敗.
綜上, mysteam的事務處理遵循BASE, 實現方式是使用事件. 關於事務的其他細節以及事件如何實現, 我後面會用單獨的文章來介紹. 這裏我們先回到本篇的主題, 如何調用REST接口. 在這裏, 我先簡單介紹一下Eureka, Ribbon和Feign這三個組件.
Eureka:
服務註冊中心. 我們的REST服務在啓動的時候會將自己的地址註冊到Eureka, 其他需要該服務的應用會請求Eureka進行服務尋址, 得到目標服務的ip地址之後就會使用該地址直連目標服務.
Ribbon:
客戶端負載均衡類庫. 當客戶端請求的目標服務存在多個實例時, Ribbon會將請求分散到各個實例. 一般會結合Eureka一起使用.
Feign:
HTTP客戶端類庫. 我們使用Feign提供的註解編寫HTTP接口的客戶端代碼非常簡單, 只需要聲明一個Java接口加上少量註解就完成了.
接下來我們看代碼實例. 以賬戶服務的接口爲例, 之前我們在placeOrder
方法內查詢賬戶餘額的代碼如下:
打開$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/service/gateway/AccountGateway.java
,
代碼如下:
AccountClient是一個加了Feign註解的接口:
@FeignClient
註解需要聲明一個service
id, 這個service id就是我們在YAML配置文件中配的spring.application.name
的值,
比如account.yml
中的spring.application.name
值是account
.
我們請求的REST接口需要一個url路徑參數userId, 以及一個查詢參數balance. 我們在代碼中不需要直接調用Ribbon的代碼, Feign會幫我們處理好一切. 根據我們的AccountClient
接口聲明,
Feign會在Spring容器啓動之後, 將生成的代理類注入AccountGateway
,
所以我們不需要寫HTTP調用的實現代碼就能完成REST接口的調用.
到這裏下單的邏輯就完成了. 我們知道在分佈式環境下, 服務之間的依賴都是脆弱而且不穩定的, 極有可能因爲一個服務實例的延遲或宕機造成所有服務不可用. 所以mysteam中引入了hystrix. 細心的同學可能已經在AccountGateway
中發現@HystrixCommand
註解了,
下篇文章我將介紹hystrix的基本用法, 以及如何使用hystrix board和turbine來監控hystrix服務.