SpringBoot集成ServiceComb Pack

SpringBoot2.x集成ServiceComb pack

事務基本概念

​ 有過後端數據庫編程經驗的童鞋應該知道事務的基本理論知識同時網上有許多更爲規範的文檔參考,我在這裏大致簡單介紹一下。在數據庫編程中我們通常知道ACID的基本概念,爲什麼會存在這個理論知識的,我個人認爲人們在實踐的經驗中總結出來了對數據庫的基本範式和編程規範。

本地事務場景

​ 這裏簡單的那一個業務場景舉例,比如我們有一個這樣的積分兌換場景系統爲單體架構那麼這裏會設計到用戶積分表、商品表、交易訂單表,

積分兌換

當用戶發起積分兌換操作時步驟,這三個操作步驟需要保證數據的一致性、原子性、持久性 ,那麼就需要開啓事務,我們知道在同一個數據庫會話連接中就相當於一個事務那麼這三個操作步驟要麼全部成功、要麼全部失敗(數據的強一致性),在併發的場景下不同連接會話的事務是不會相互影響。

  • 產生交易訂單
  • 扣減庫存
  • 扣減積分

以上操作就涉及到數據庫的ACID基本概念:

A:即Atomic數據操作原子性

  • 在同一個事務中的操作要麼全部成功、要麼全部失敗。

C:即Consistency數據一致性

  • 可以這樣理解爲什麼會出現數據一致性問題,比如在一個事務操作數據的時候,其他的事務對此操作(commit、rollback)的看到的結果數據是一致的。

I:即Isolation事務的隔離性

  • 不同會話事務操作是互不影響

D:即Durability數據的持久性

  • 當事務被commit或者rollback對數據庫的操作是持久化的。

分佈式事務基本概念

爲什麼會出現分佈式事務?這個問題在當今微服務盛行的今天我想大家應該深有體會,一個單體應用一個DB在業務操作層Service同一個事務可以完成多表操作,但是如果按照領域模型進行服務拆分後不同的領域對於各自的服務以及DB那麼在單體應用中同一個場景下,服務化改造後的操作就會涉及到跨服務操作,就會涉及到不同服務有各自本地的事務操作,那麼怎麼來實現之前的本地事務ACID呢?顯示是一個非常困難的事情,那麼就出現了分佈式事務的一些模式。

分佈式事務場景

有個這樣一個場景用戶在購買商品&支付我們的架構可能是如下:

distributeservice

當用戶通過app或者pc打開我們的商城選好商品後下單,這裏涉及到產生交易訂單商品庫存的鎖定調用支付系統帳戶變更(混合支付=積分+第三方收單)等,由於我們的每一個服務都有自己DB本地事務操作只能保證本地事務的ACID,但對於整個交易場景來說會涉及到多個本地事務所有不能保證有一個統一的協同操作和回滾機制的保證。那麼這個時候就出現了分佈式事務的解決方案。

那麼在分佈式系統中CAP原則和base的基本理論大家可以自行掃盲下,一下介紹幾種常見的分佈式事務解決方案

  • 強一致模型

  • 2pc 典型的XA模型 擁有三個角色:TM(事務管理者)、RM(資源管理者)、AP(應用), 包括兩個階段:第一是資源的準備 、第二是事務提交,在第一階段當所有的資源管理者返回預提交成功後才發起第二階段事務提交,如果在第一階段存在一個返回預提交失敗則回滾。

    • 問題:同步阻塞、事務沒有超時機制(存在宕機後事務管理者一致等待資源管理者響應)
    • 3pc 在2pc的基礎上增加了一個預備階段和超時機制

    問題:

    • 極端條件存在數據不一致問題
    • 系統開銷大
    • 容易出現單點問題
    • 同步、阻塞性能低
  • 柔性事務

    • TCC 這其實和2pc類似都屬於兩階段提交不過這裏把過程拆分爲:try、confirm、cancel三個階段,try階段對資源check和預留,如果成功則進行confirm提交階段,如果失敗則進行cancel補償階段。當然這也需要事務的協調者角色參與
    • 問題 對業務入侵比較大 、同步、阻塞
    • sage 把整個分佈式事務拆分成多個本地事務,如果所有本地事務都成功那就成功,如果存在失敗那就進行補償,補償分爲:正向補償和反向補償。正向補償:不斷的重試失敗事務,最大努力嘗試保證最終一致性,如果重試多次失敗報警人工介入處理,反向補償:進行反向回滾操作,達到最終一致性。
    • 優點:相比TCC減少try階段、異步補償
  • 異步消息(可靠實踐模式)

    • 業務方提供本地操作成功回查功能

      在基於異步消息實現分佈式事務中當操作本地業務的時候先記錄一個消息到本地消息表消息狀態爲待發送,然後發送預half消息到MQ,此時MQ不會投遞消息到消費者,MQ立即返回隊列執行結果,如果失敗則不執行後面業務同時發送MQ一個rollback消息和修改本地消息狀態爲 完成,如果返回成功執行本地事務提交和修改本地消息狀態已發送併發送MQ一個commit消息表示可以投遞。事務回滾則發送MQ一個rollback消息、刪除或者修改本地消息表,當收到隊列的ack回執後刪除或者修改本地消息狀態爲完成

      • 發送端提供回查
      • 異步操作
      • 業務侵入大
      • 消費端消息去重
      • 消費端消息冪等性
    • 本地消息事務表

      基於消息隊列(MQ)+本地事務表的形式, 在基於異步消息實現分佈式事務中當操作本地業務的時候同時記錄本地事務消息表在同一個事務中進行commit和rollback,然後把本地事務消息發送到MQ,當MQ成功回執後刪除本地事務消息,未收到MQ回執需要重新嘗試也可以開啓一個定時任務去掃描發送MQ。當出現A->B->C 場景中消費者C事務異常則不斷重試C,如果重試達到上限還是失敗則需報警和人工介入。

      • 異步操作
      • 業務入侵小
      • 消費端消息冪等性

what‘s the ServiceComb pack?

Apache ServiceComb Pack is an eventually data consistency solution for micro-service applications. ServiceComb Pack currently provides TCC and Saga distributed transaction co-ordination solutions by using Alpha as a transaction coordinator and Omega as an transaction agent

也就是說Apache ServiceComb Saga 是一個微服務應用的數據最終一致性解決方案。

特性

  • 高可用。支持集羣模式。
  • 高可靠。所有的事務事件都持久存儲在數據庫中。
  • 高性能。事務事件是通過gRPC來上報的,且事務的請求信息是通過Kyro進行序列化和反序列化的。
  • 低侵入。僅需2-3個註解和編寫對應的補償方法即可進行分佈式事務。
  • 部署簡單。可通過Docker快速部署。
  • 支持前向恢復(重試)及後向恢復(補償)。
  • 擴展簡單。基於Pack架構很容實現多種協調機制。

架構

Saga Pack 架構是由 alphaomega組成,其中:

  • alpha充當協調者的角色,主要負責對事務進行管理和協調。
  • omega是微服務中內嵌的一個agent,負責對網絡請求進行攔截並向alpha上報事務事件。

下圖展示了alpha, omega以及微服務三者的關係:

pack

Github:https://github.com/apache/servicecomb-pack

SpringBoot集成ServiceComb pack 案例

服務架構

servicecomb-pack

服務搭建準備工作

  • 使用spring官方生成代碼腳手架https://start.spring.io生成springboot代碼這裏使用springboot2.x,需要依賴SpringWeb、SpringDta JDBC模塊,分別生成booking、car、hotel三個項目

buildbookingprojiect

  • 去Github https://github.com/apache/servicecomb-pack下載源碼編譯最新代碼使用0.6.0-SNAPSHOT版本當然如果不想編譯使用發行版本0.5.0 ,注意在編譯源碼的使用注意選擇相關的profies

    profiesset

    • 引入servicecomb pack依賴

      <dependency>
          <groupId>org.apache.servicecomb.pack</groupId>
          <artifactId>omega-spring-starter</artifactId>
          <version>0.5.0</version>
      </dependency>
      

      或者

      <dependency>
        <groupId>org.apache.servicecomb.pack</groupId>
        <artifactId>omega-spring-starter</artifactId>
        <version>0.6.0-SNAPSHOT</version>
      </dependency>
      
    • 其次這裏我們使用到數據操作所以需要引入數據庫連接池和相關驅動

      <!-- 數據庫連接池 -->
      <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.6</version>
      </dependency>
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>6.0.6</version>
      </dependency>
      
    • servicecomb實現的resttemplate

      		<dependency>
      			<groupId>org.apache.servicecomb.pack</groupId>
      			<artifactId>omega-transport-resttemplate</artifactId>
      			<version>0.6.0-SNAPSHOT</version>
      		</dependency>
      
    • 準備alpha-server數據庫腳本

      CREATE TABLE IF NOT EXISTS TxEvent (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        type varchar(50) NOT NULL,
        compensationMethod varchar(512) NOT NULL,
        expiryTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        payloads blob,
        retries int(11) NOT NULL DEFAULT '0',
        retryMethod varchar(512) DEFAULT NULL,
        PRIMARY KEY (surrogateId),
        INDEX saga_events_index (surrogateId, globalTxId, localTxId, type, expiryTime),
        INDEX saga_global_tx_index (globalTxId)
      ) DEFAULT CHARSET=utf8;
      
      CREATE TABLE IF NOT EXISTS Command (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        eventId bigint NOT NULL UNIQUE,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        compensationMethod varchar(512) NOT NULL,
        payloads blob,
        status varchar(12),
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        version bigint NOT NULL,
        PRIMARY KEY (surrogateId),
        INDEX saga_commands_index (surrogateId, eventId, globalTxId, localTxId, status)
      ) DEFAULT CHARSET=utf8;
      
      CREATE TABLE IF NOT EXISTS TxTimeout (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        eventId bigint NOT NULL UNIQUE,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        type varchar(50) NOT NULL,
        expiryTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        status varchar(12),
        version bigint NOT NULL,
        PRIMARY KEY (surrogateId),
        INDEX saga_timeouts_index (surrogateId, expiryTime, globalTxId, localTxId, status)
      ) DEFAULT CHARSET=utf8;
      
      CREATE TABLE IF NOT EXISTS tcc_global_tx_event (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        txType varchar(12),
        status varchar(12),
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (surrogateId),
        UNIQUE INDEX tcc_global_tx_event_index (globalTxId, localTxId, parentTxId, txType)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      CREATE TABLE IF NOT EXISTS tcc_participate_event (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        confirmMethod varchar(512) NOT NULL,
        cancelMethod varchar(512) NOT NULL,
        status varchar(50) NOT NULL,
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (surrogateId),
        UNIQUE INDEX tcc_participate_event_index (globalTxId, localTxId, parentTxId)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      CREATE TABLE IF NOT EXISTS tcc_tx_event (
        surrogateId bigint NOT NULL AUTO_INCREMENT,
        globalTxId varchar(36) NOT NULL,
        localTxId varchar(36) NOT NULL,
        parentTxId varchar(36) DEFAULT NULL,
        serviceName varchar(36) NOT NULL,
        instanceId varchar(36) NOT NULL,
        methodInfo varchar(512) NOT NULL,
        txType varchar(12),
        status varchar(12),
        creationTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lastModified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (surrogateId),
        UNIQUE INDEX tcc_tx_event_index (globalTxId, localTxId, parentTxId, txType)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      CREATE TABLE IF NOT EXISTS master_lock (
        serviceName varchar(36) not NULL,
        expireTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        lockedTime datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        instanceId  varchar(255) not NULL,
        PRIMARY KEY (serviceName)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      
      
    • 微服務sql腳本

      CREATE TABLE `booking` (
        `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
        `name` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
        `phone` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
        `price` double DEFAULT NULL,
        `uuid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
      
      
      CREATE TABLE `carbooking` (
        `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
        `name` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
        `amount` int(11) DEFAULT NULL,
        `confirmed` tinyint(1) DEFAULT NULL,
        `cancelled` tinyint(1) DEFAULT NULL,
        `uuid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
      
      CREATE TABLE `hotelbooking` (
        `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
        `name` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
        `amount` int(11) DEFAULT NULL,
        `confirmed` tinyint(4) DEFAULT NULL,
        `cancelled` tinyint(4) DEFAULT NULL,
        `uuid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
        PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
      
      
      

booking service

預定相關服務,需要操作car和hotel具有分佈式事務使用場景。預定car和hotel結果需要一致性(都成功or都失敗)

  • 編寫提供外部訪問的Controller層

    		@SagaStart //標誌這裏是全局事務的開始
        @PostMapping("/booking/{name}/{rooms}/{cars}")
        public String order(@PathVariable String name, @PathVariable Integer rooms,
                            @PathVariable Integer cars) throws Throwable {
    
            if (cars < 0) {
                throw new Exception("The cars order quantity must be greater than 0");
            }
    				//第一步本地事務方法
            saveBooking();
    
    				//第二步car服務
            postCarBooking(name, cars);
    
            if (rooms < 0) {
                throw new Exception("The rooms order quantity must be greater than 0");
            }
            //調用hotel服務
            postHotelBooking(name, rooms);
    
            return name + " booking " + rooms + " rooms and " + cars + " cars OK";
        }
    
  • 第一步saveBooking方法操作本地Service

      @Transactional
        @Compensable(compensationMethod = "cancel")//開啓子事務,並提供cancel補償方法
      @Override
        public boolean booking(Booking booking) {
    
            Assert.notNull(booking, "booking is not null");
            Assert.hasLength(booking.getPhone(), "phone is not null");
            //and so on ...
    
            bookingRepository.save(booking);
    
            return true;
        }
    
        @Transactional
        @Override
        public boolean cancel(Booking booking) {
            List<Booking> bookings = bookingRepository.findByUuid(booking.getUuid());
            if (bookings != null && bookings.size() > 0) {
                bookingRepository.delete(bookings.get(0));
            }
            return true;
        }
    

    注意:這裏的cancel方法前面必須和booking方法簽名一致,被標註@Compensable方法會被omega進行攔截並根據簽名和參數產生事務上下文通過grpc發送alpha持久化。當需要進行事務補償時候alpha異步調用cancel補償方法進行調用並注入之前的事務上下文。

  • 第二步調用car服務

        private void postCarBooking(String name, Integer cars) {
            template.postForEntity(
                    carServiceUrl + "/order/{name}/{cars}",
                    null, String.class, name, cars);
        }
    
  • 第三步調用hotel服務

     template.postForEntity(
                    hotelServiceUrl + "/order/{name}/{rooms}",
                    null, String.class, name, rooms);
    
  • 配置文件application.yaml

    spring:
      application:
        name: booking
      cloud:
        consul:
          enabled: false
        zookeeper:
          enabled: false
        nacos:
          discovery:
            enabled: false
    alpha:
      cluster:
        address: alpha-server.servicecomb.io:8080
    
    car:
      service:
        address: http://car.servicecomb.io:8082
    
    
    hotel:
      service:
        address: http://hotel.servicecomb.io:8083
    
    
    server:
      port: 8081
    

####car service

預定car服務

  • 編寫rest api接口Controller

     @PostMapping("/order/{name}/{cars}")
        CarBooking order(@PathVariable String name, @PathVariable Integer cars) {
          CarBooking booking = new CarBooking();
          booking.setId(id.incrementAndGet());
          booking.setName(name);
          booking.setAmount(cars);
          booking.setUuid(UUID.randomUUID().toString());
          carService.bookingCar(booking);
          return booking;
        }
    
  • Service邏輯

    @Transactional
          @Override
          @Compensable(compensationMethod = "cancel")//開啓子事務,並提供cancel補償方法
          public void bookingCar(CarBooking booking) {
              if (booking.getAmount() > 10) {
                  throw new IllegalArgumentException("can not order the cars large than ten");
              }
              booking.setId(null);
              booking.confirm();
              carBookingRepository.save(booking);
          }
      
          @Transactional
          @Override
          public void cancel(CarBooking booking) {
              List<CarBooking> cars = carBookingRepository.findByUuid(booking.getUuid());
              if (cars != null && cars.size()>0) {
                  CarBooking car = cars.get(0);
                  carBookingRepository.delete(car);
              }
          }
    

    注意:這裏的cancel方法前面必須和booking方法簽名一致,被標註@Compensable方法會被omega進行攔截並根據簽名和參數產生事務上下文通過grpc發送alpha持久化。當需要進行事務補償時候alpha異步調用cancel補償方法進行調用並注入之前的事務上下文。

  • 配置文件application.yaml

    spring:
      application:
        name: car
      cloud:
        consul:
          enabled: false
        zookeeper:
          enabled: false
        nacos:
          discovery:
            enabled: false
    alpha:
      cluster:
        address: alpha-server.servicecomb.io:8080
    
    
    server:
      port: 8082
    
    

hotel service

預定hotel服務

  • 編寫rest api接口Controller

      @PostMapping("/order/{name}/{rooms}")
      HotelBooking order(@PathVariable String name, @PathVariable Integer rooms) {
        HotelBooking booking = new HotelBooking();
        booking.setId(id.incrementAndGet());
        booking.setName(name);
        booking.setAmount(rooms);
        hotelService.order(booking);
        return booking;
      }
    
  • Service本地事務接口

        @Transactional
        @Compensable(compensationMethod = "cancel")//開啓子事務,並提供cancel補償方法
        @Override
        public void order(HotelBooking booking) {
            if (booking.getAmount() > 2) {
                throw new IllegalArgumentException("can not order the rooms large than two");
            }
            booking.setId(null);
            booking.confirm();
            booking.setUuid(UUID.randomUUID().toString());
            hotelRepository.save(booking);
        }
    
        @Transactional
        @Override
        public void cancel(HotelBooking booking) {
            List<HotelBooking> hotelBookings = hotelRepository.findByUuid(booking.getUuid());
            if (hotelBookings != null && hotelBookings.size() > 0) {
                hotelRepository.deleteAll(hotelBookings);
            }
        }
    

    注意:這裏的cancel方法前面必須和booking方法簽名一致,被標註@Compensable方法會被omega進行攔截並根據簽名和參數產生事務上下文通過grpc發送alpha持久化。當需要進行事務補償時候alpha異步調用cancel補償方法進行調用並注入之前的事務上下文。

  • 配置文件application.yaml

    spring:
      application:
        name: hotel
      cloud:
        consul:
          enabled: false
        zookeeper:
          enabled: false
        nacos:
          discovery:
            enabled: false
    alpha:
      cluster:
        address: alpha-server.servicecomb.io:8080
    server:
      port: 8083
    
    

Alpha service

ServiceComb pack 協調服務、事務上下文持久化等

直接在源碼中找到啓動類啓動Alpha service或者使用jar啓動,在源碼中啓動的時候增加一個啓動參數

-Dspring.profiles.active=mysql使用mysql數據庫

alpha

測試

  • 場景一 car service和hotel service服務以及本地服務調用全部成功

    http://127.0.0.1:8081/booking/ouwen/1/2

  • 場景二 car service服務調用失敗,booking service事務回滾

    http://127.0.0.1:8081/booking/ouwen/1/20

  • 場景三 hotel service服務調用失敗 bookng service 和car service服務事務回滾

    http://127.0.0.1:8081/booking/ouwen/10/2

我的博客地址

相關資料

參考文章:

https://docs.servicecomb.io/saga

示例代碼:

https://gitee.com/newitman/itman-blog.git

關注我

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章