Spring Cloud綜合實(shí)戰(zhàn) - 基于TCC補(bǔ)償模式的分布式事務(wù)

本文通過使用Spring Cloud和Docker構(gòu)建了一個常見的Microservice體系.

Spring Cloud為開發(fā)者提供了快速構(gòu)建分布式系統(tǒng)中的一些常見工具, 如分布式配置中心, 服務(wù)發(fā)現(xiàn)與注冊中心, 智能路由, 服務(wù)熔斷及降級, 消息總線等.

而Spring Cloud Sleuth為Spring Cloud提供了分布式追蹤方案, 可視化地分析服務(wù)調(diào)用鏈路和服務(wù)間的依賴關(guān)系

本次實(shí)戰(zhàn)以模擬下單流程作為實(shí)戰(zhàn)演示, 使用Try-Confirm-Cancel即TCC模式為分布式事務(wù)提供最終一致性.

完整的代碼示例已經(jīng)上傳至Github, 喜歡的話可以給我一顆star, 這樣能激勵我寫出更好的文章

Try Confirm Cancel補(bǔ)償模式

本實(shí)例遵循的是Atomikos公司對微服務(wù)的分布式事務(wù)所提出的RESTful TCC解決方案

RESTful TCC模式分3個階段執(zhí)行

  1. Trying階段主要針對業(yè)務(wù)系統(tǒng)檢測及作出預(yù)留資源請求, 若預(yù)留資源成功, 則返回確認(rèn)資源的鏈接與過期時(shí)間
  2. Confirm階段主要是對業(yè)務(wù)系統(tǒng)的預(yù)留資源作出確認(rèn), 要求TCC服務(wù)的提供方要對確認(rèn)預(yù)留資源的接口實(shí)現(xiàn)冪等性, 若Confirm成功則返回204, 資源超時(shí)則證明已經(jīng)被回收且返回404
  3. Cancel階段主要是在業(yè)務(wù)執(zhí)行錯誤或者預(yù)留資源超時(shí)后執(zhí)行的資源釋放操作, Cancel接口是一個可選操作, 因?yàn)橐骉CC服務(wù)的提供方實(shí)現(xiàn)自動回收的功能, 所以即便是不認(rèn)為進(jìn)行Cancel, 系統(tǒng)也會自動回收資源

對RESTful TCC事務(wù)更為詳細(xì)的解釋可以點(diǎn)擊這里進(jìn)行閱讀

系統(tǒng)結(jié)構(gòu)

基礎(chǔ)組件

Zuul Gateway

Zuul在本實(shí)例中僅作為路由所使用, 配置降低Ribbon的讀取與連接超時(shí)上限

Eureka H.A.

多個對等Eureka節(jié)點(diǎn)組成高可用集群, 并將注冊列表的自我保護(hù)的閾值適當(dāng)降低

Config Server

  • 如果遠(yuǎn)程配置中有密文{cipher}*, 那么該密文的解密將會延遲至客戶端啟動的時(shí)候. 因此客戶端需要配置AES的對稱密鑰encrypt.key, 并且客戶端所使用的JRE需要安裝Java 8 JCE, 否則將會拋出Illegal key size相關(guān)的異常.
    (本例中Docker Compose構(gòu)建的容器已經(jīng)安裝了JCE, 如果遠(yuǎn)程配置文件沒有使用{cipher}*也不必進(jìn)行JCE的安裝)

    spring:
      cloud:
        config:
          server:
            git:
              uri: 'https://git.oschina.net/witless/conf-repo.git'
              clone-on-start: true
            encrypt:
              enabled: false
      application:
        name: 'config-server'
    
  • 為了達(dá)到開箱即用, 選用公開倉庫Github或者GitOsc

  • 本項(xiàng)目中有兩個自定義注解
    @com.github.prontera.Delay 控制方法的延時(shí)返回時(shí)間

    @com.github.prontera.RandomlyThrowsException 隨機(jī)拋出異常, 人為地制造異常

    默認(rèn)的遠(yuǎn)程配置如下

    solar:
      delay:
        time-in-millseconds: 0
      exception:
        enabled: false
        factor: 7
    

    這些自定義配置正是控制方法返回的時(shí)延, 隨機(jī)異常的因子等

    我在服務(wù)order, product, accounttcc中的所有Controller上都添加了以上兩個注解, 當(dāng)遠(yuǎn)程配置的更新時(shí)候, 可以手工刷新/refresh或通過webhook等方法自動刷新本地配置. 以達(dá)到模擬微服務(wù)繁忙或熔斷等情況.

RabbitMQ

原本作為可靠性事件投遞的Broker, 如今被TCC模式所替代. 可為日后的Spring Cloud Steam或Spring Cloud Bus的集成作為基礎(chǔ)組件而保留

監(jiān)控服務(wù)

Spring Boot Admin

此應(yīng)用提供了管理Spring Boot服務(wù)的簡單UI, 下圖是在容器中運(yùn)行時(shí)的服務(wù)健康檢測頁

Hystrix Dashboard

提供近實(shí)時(shí)依賴的統(tǒng)計(jì)和監(jiān)控面板, 以監(jiān)測服務(wù)的超時(shí), 熔斷, 拒絕, 降級等行為

Zipkin Server

Zipkin是一款開源的分布式實(shí)時(shí)數(shù)據(jù)追蹤系統(tǒng), 其主要功能是聚集來自各個異構(gòu)系統(tǒng)的實(shí)時(shí)監(jiān)控?cái)?shù)據(jù), 用來追蹤微服務(wù)架構(gòu)下的系統(tǒng)時(shí)延問題. 下圖是對order服務(wù)的請求進(jìn)行追蹤的情況

業(yè)務(wù)服務(wù)

首次啟動時(shí)通過Flyway自動初始化數(shù)據(jù)庫

對spring cloud config server采用fail fast策略, 一旦遠(yuǎn)程配置服務(wù)無法連接則無法啟動業(yè)務(wù)服務(wù)

account

用于獲取用戶信息, 用戶注冊, 修改用戶余額, 預(yù)留余額資源, 確認(rèn)預(yù)留余額, 撤銷預(yù)留余額

product

用于獲取產(chǎn)品信息, 變更商品庫存, 預(yù)留庫存資源, 確認(rèn)預(yù)留庫存, 撤銷預(yù)留庫存

tcc coordinator

TCC資源協(xié)調(diào)器, 其職責(zé)如下

  • 對所有參與者發(fā)起Confirm請求
  • 無論是協(xié)調(diào)器發(fā)生的錯誤還是調(diào)用參與者所產(chǎn)生的錯誤, 協(xié)調(diào)器都必須有自動恢復(fù)重試功能, 尤其是在確認(rèn)的階段, 以防止網(wǎng)絡(luò)抖動的情況

order

order服務(wù)是本項(xiàng)目的入口, 盡管所提供的功能很簡單

  • 下單. 即生成預(yù)訂單, 為了更好地測試TCC功能, 在下單時(shí)就通過Feign向服務(wù)accountproduct發(fā)起預(yù)留資源請求, 并且記錄入庫
  • 確認(rèn)訂單. 確認(rèn)訂單時(shí)根據(jù)訂單ID從庫中獲取訂單, 并獲取預(yù)留資源確認(rèn)的URI, 交由服務(wù)tcc統(tǒng)一進(jìn)行確認(rèn), 如果發(fā)生沖突即記錄入庫, 等待人工處理

與其他服務(wù)進(jìn)行通訊, 我們選擇使用Feign

/**
 * @author Zhao Junjian
 */
@FeignClient(name = TccClient.SERVICE_ID, fallback = TccClientFallback.class)
public interface TccClient {
    /**
     * eureka service name
     */
    String SERVICE_ID = "tcc";
    /**
     * api prefix
     */
    String API_PATH = "/api/v1/coordinator";

    @RequestMapping(value = API_PATH + "/confirmation", method = RequestMethod.PUT, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    void confirm(@RequestBody TccRequest request);

    @RequestMapping(value = API_PATH + "/cancellation", method = RequestMethod.PUT, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    void cancel(@RequestBody TccRequest request);

}

Swagger UI

Swagger的目標(biāo)是為REST APIs 定義一個標(biāo)準(zhǔn)的, 與語言無關(guān)的接口, 使人和計(jì)算機(jī)在看不到源碼或者看不到文檔或者不能通過網(wǎng)絡(luò)流量檢測的情況下能發(fā)現(xiàn)和理解各種服務(wù)的功能. 當(dāng)服務(wù)通過Swagger定義, 消費(fèi)者就能與遠(yuǎn)程的服務(wù)互動通過少量的實(shí)現(xiàn)邏輯. 類似于低級編程接口, Swagger去掉了調(diào)用服務(wù)時(shí)的很多猜測.

運(yùn)行

Docker Compose運(yùn)行

在項(xiàng)目根路徑下執(zhí)行腳本build.sh, 該腳本會執(zhí)行Maven的打包操作, 并會迭代目錄下的*-compose.yml進(jìn)行容器構(gòu)建

構(gòu)建完成后需要按照指定的順序啟動

  1. 啟動MySQL, RabbitMQ等基礎(chǔ)組件

    ?  solar git:(feature/cleanup) ? docker-compose -f infrastructure-compose.yml up -d
    
  2. 啟動Eureka Server與Config Server

    ?  solar git:(feature/cleanup) ? docker-compose -f basic-ms-compose.yml up -d
    
  3. 啟動監(jiān)控服務(wù)

    ?  solar git:(feature/cleanup) ? docker-compose -f monitor-ms-compose.yml up -d
    
  4. 啟動業(yè)務(wù)服務(wù)

    ?  solar git:(feature/cleanup) ? docker-compose -f business-ms-compose.yml up -d
    

IDE運(yùn)行

因?yàn)槌绦虮旧戆凑誅ocker啟動, 所以對于hostname需要在hosts文件中設(shè)置正確才能正常運(yùn)行

## solar
127.0.0.1 eureka1
127.0.0.1 eureka2
127.0.0.1 rabbitmq
127.0.0.1 zipkin_server
127.0.0.1 solar_mysql
127.0.0.1 gitlab

根據(jù)依賴關(guān)系, 程序最好按照以下的順序執(zhí)行

docker mysql > docker rabbitmq > eureka server > config server > zipkin server > 其他微服務(wù)

示例

根據(jù)附表中的服務(wù)字典, 我們通過Zuul或Swagge對order服務(wù)進(jìn)行預(yù)訂單生成操作

POST http://localhost:7291/order/api/v1/orders
Content-Type: application/json;charset=UTF-8

{
  "product_id": 7,
  "user_id": 1
}

成功后我們將得到預(yù)訂單的結(jié)果

{
  "data": {
    "id": 15,
    "create_time": "2017-03-28T18:18:02.206+08:00",
    "update_time": "1970-01-01T00:00:00+08:00",
    "delete_time": "1970-01-01T00:00:00+08:00",
    "user_id": 1,
    "product_id": 7,
    "price": 14,
    "status": "PROCESSING"
  },
  "code": 20000
}

此時(shí)我們再確認(rèn)訂單

(如果想測試預(yù)留資源的補(bǔ)償情況, 那么就等15s后過期再發(fā)請求, 注意容器與宿主機(jī)的時(shí)間)

POST http://localhost:7291/order/api/v1/orders/confirmation
Content-Type: application/json;charset=UTF-8

{
  "order_id": 15
}

如果成功確認(rèn)則返回如下結(jié)果

{
  "data": {
    "id": 15,
    "create_time": "2017-03-28T18:18:02.206+08:00",
    "update_time": "2017-03-28T18:21:32.78+08:00",
    "delete_time": "1970-01-01T00:00:00+08:00",
    "user_id": 1,
    "product_id": 7,
    "price": 14,
    "status": "DONE"
  },
  "code": 20000
}

至此就完成了一次TCC事務(wù), 當(dāng)然你也可以測試超時(shí)和沖突的情況, 這里就不再贅述

拓展

使用Gitlab作為遠(yuǎn)程配置倉庫

本例中默認(rèn)使用Github或GitOsc中的公開倉庫, 出于自定義的需要, 我們可以在本地構(gòu)建Git倉庫, 這里選用Gitlab為例.

將以下配置添加至docker compose中的文件中并啟動Docker Gitlab容器

gitlab:
    image: daocloud.io/daocloud/gitlab:8.16.7-ce.0
    ports:
        - "10222:22"
        - "80:80"
        - "10443:443"
    volumes:
        - "./docker-gitlab/config/:/etc/gitlab/"
        - "./docker-gitlab/logs/:/var/log/gitlab/"
        - "./docker-gitlab/data/:/var/opt/gitlab/"
    environment:
        - TZ=Asia/Shanghai

將項(xiàng)目的config-repo添加至Gitlab中, 并修改config-ms中g(shù)it倉庫的相關(guān)驗(yàn)證等參數(shù)即可

結(jié)語

更為詳細(xì)的說明及代碼示例已經(jīng)上傳至Github

https://github.com/prontera/spring-cloud-rest-tcc

如有對本項(xiàng)目中的Spring Cloud的使用或者對本人的編碼風(fēng)格有更好的想法或者建議, 希望能在下方給我留言, 再次感謝你的耐心閱讀

作者:Chris
原博客:http://blog.chriscs.com
Github:https://github.com/prontera

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容