SpringCloud與Seata分布式事務(wù)初體驗(yàn)

在本篇文章中我們?cè)?code>SpringCloud環(huán)境下通過(guò)使用Seata來(lái)模擬用戶(hù)購(gòu)買(mǎi)商品時(shí)由于用戶(hù)余額不足導(dǎo)致本次訂單提交失敗,來(lái)驗(yàn)證下在MySQL數(shù)據(jù)庫(kù)內(nèi)事務(wù)是否會(huì)回滾桩皿。

免費(fèi)教程專(zhuān)題

恒宇少年在博客整理三套免費(fèi)學(xué)習(xí)教程專(zhuān)題豌汇,由于文章偏多特意添加了閱讀指南,新文章以及之前的文章都會(huì)在專(zhuān)題內(nèi)陸續(xù)填充泄隔,希望可以幫助大家解惑更多知識(shí)點(diǎn)拒贱。

本章文章只涉及所需要測(cè)試的服務(wù)列表以及Seata配置部分。

用戶(hù)提交訂單購(gòu)買(mǎi)商品大致分為以下幾個(gè)步驟:

  1. 減少庫(kù)存
  2. 扣除金額
  3. 提交訂單

1. 準(zhǔn)備環(huán)境

2. 準(zhǔn)備測(cè)試服務(wù)

為了方便學(xué)習(xí)的同學(xué)查看源碼,我們本章節(jié)源碼采用Maven Module(多模塊)的方式進(jìn)行構(gòu)建斜做。

我們用于測(cè)試的服務(wù)所使用的第三方依賴(lài)都一致苞氮,各個(gè)服務(wù)的pom.xml文件內(nèi)容如下所示:

<dependencies>
  <!--Web-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!--openfeign接口定義-->
  <dependency>
    <groupId>org.minbox.chapter</groupId>
    <artifactId>openfeign-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </dependency>
  <!--公共依賴(lài)-->
  <dependency>
    <groupId>org.minbox.chapter</groupId>
    <artifactId>common-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </dependency>

  <!--seata-->
  <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>

  <!--Eureka Client-->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>

  <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <dependency>
    <groupId>org.minbox.framework</groupId>
    <artifactId>api-boot-starter-mybatis-enhance</artifactId>
  </dependency>
</dependencies>

2.1 Openfeign接口定義模塊

由于我們服務(wù)之間采用的Openfeign方式進(jìn)行相互調(diào)用,所以創(chuàng)建了一個(gè)模塊openfeign-service來(lái)提供服務(wù)接口的定義瓤逼。

  • 賬戶(hù)服務(wù)提供的接口定義

賬戶(hù)服務(wù)對(duì)外所提供的Openfeign接口定義如下所示:

/**
 * 賬戶(hù)服務(wù)接口
 *
 * @author 恒宇少年
 */
@FeignClient(name = "account-service")
@RequestMapping(value = "/account")
public interface AccountClient {
    /**
     * 扣除指定賬戶(hù)金額
     *
     * @param accountId 賬戶(hù)編號(hào)
     * @param money     金額
     */
    @PostMapping
    void deduction(@RequestParam("accountId") Integer accountId, @RequestParam("money") Double money);
}
  • 商品服務(wù)提供的接口定義

    商品服務(wù)對(duì)外所提供的Openfeign接口定義如下所示:

  /**
   * 商品服務(wù)接口定義
   *
   * @author 恒宇少年
   */
  @FeignClient(name = "good-service")
  @RequestMapping(value = "/good")
  public interface GoodClient {
      /**
       * 查詢(xún)商品基本信息
       *
       * @param goodId {@link Good#getId()}
       * @return {@link Good}
       */
      @GetMapping
      Good findById(@RequestParam("goodId") Integer goodId);
  
      /**
       * 減少商品的庫(kù)存
       *
       * @param goodId {@link Good#getId()}
       * @param stock  減少庫(kù)存的數(shù)量
       */
      @PostMapping
      void reduceStock(@RequestParam("goodId") Integer goodId, @RequestParam("stock") int stock);
  }

2.2 公共模塊

公共模塊common-service內(nèi)所提供的類(lèi)是共用的笼吟,各個(gè)服務(wù)都可以調(diào)用,其中最為重要的是將Seata所提供的數(shù)據(jù)源代理(DataSourceProxy)實(shí)例化配置放到了這個(gè)模塊中霸旗,數(shù)據(jù)庫(kù)代理相關(guān)配置代碼如下所示:

/**
 * Seata所需數(shù)據(jù)庫(kù)代理配置類(lèi)
 *
 * @author 恒宇少年
 */
@Configuration
public class DataSourceProxyAutoConfiguration {
    /**
     * 數(shù)據(jù)源屬性配置
     * {@link DataSourceProperties}
     */
    private DataSourceProperties dataSourceProperties;

    public DataSourceProxyAutoConfiguration(DataSourceProperties dataSourceProperties) {
        this.dataSourceProperties = dataSourceProperties;
    }

    /**
     * 配置數(shù)據(jù)源代理贷帮,用于事務(wù)回滾
     *
     * @return The default datasource
     * @see DataSourceProxy
     */
    @Primary
    @Bean("dataSource")
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dataSourceProperties.getUrl());
        dataSource.setUsername(dataSourceProperties.getUsername());
        dataSource.setPassword(dataSourceProperties.getPassword());
        dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        return new DataSourceProxy(dataSource);
    }
}

該配置類(lèi)在所需要的服務(wù)中使用@Import注解進(jìn)行導(dǎo)入使用。

2.3 賬戶(hù)服務(wù)

  • 服務(wù)接口實(shí)現(xiàn)

    賬戶(hù)服務(wù)用于提供接口的服務(wù)實(shí)現(xiàn)诱告,通過(guò)實(shí)現(xiàn)openfeign-service內(nèi)提供的AccountClient服務(wù)定義接口來(lái)對(duì)應(yīng)提供服務(wù)實(shí)現(xiàn)撵枢,實(shí)現(xiàn)接口如下所示:

  /**
   * 賬戶(hù)接口實(shí)現(xiàn)
   *
   * @author 恒宇少年
   */
  @RestController
  public class AccountController implements AccountClient {
      /**
       * 賬戶(hù)業(yè)務(wù)邏輯
       */
      @Autowired
      private AccountService accountService;
  
      @Override
      public void deduction(Integer accountId, Double money) {
          accountService.deduction(accountId, money);
      }
  }
  • 服務(wù)配置(application.yml)
  # 服務(wù)名
  spring:
    application:
      name: account-service
    # seata分組
    cloud:
      alibaba:
        seata:
          tx-service-group: minbox-seata
    # 數(shù)據(jù)源
    datasource:
      url: jdbc:mysql://localhost:3306/test
      username: root
      password: 123456
      type: com.zaxxer.hikari.HikariDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
  
  # eureka
  eureka:
    client:
      service-url:
        defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/

通過(guò)spring.cloud.alibaba.seata.tx-service-group我們可以指定服務(wù)所屬事務(wù)的分組,該配置非必填精居,默認(rèn)為spring.application.name配置的內(nèi)容加上字符串-fescar-service-group诲侮,如:account-service-fescar-service-group,詳見(jiàn)com.alibaba.cloud.seata.GlobalTransactionAutoConfiguration配置類(lèi)源碼箱蟆。

在我本地測(cè)試環(huán)境的Eureka Server10.180.98.83服務(wù)器上沟绪,這里需要修改成你們自己的地址,數(shù)據(jù)庫(kù)連接信息也需要修改成你們自己的配置空猜。

  • 導(dǎo)入Seata數(shù)據(jù)源代理配置
  /**
   * @author 恒宇少年
   */
  @SpringBootApplication
  @Import(DataSourceProxyAutoConfiguration.class)
  public class AccountServiceApplication {
      /**
       * logger instance
       */
      static Logger logger = LoggerFactory.getLogger(AccountServiceApplication.class);
  
      public static void main(String[] args) {
          SpringApplication.run(AccountServiceApplication.class, args);
          logger.info("賬戶(hù)服務(wù)啟動(dòng)成功.");
      }
  }

通過(guò)@Import導(dǎo)入我們common-service內(nèi)提供的Seata數(shù)據(jù)源代理配置類(lèi)DataSourceProxyAutoConfiguration绽慈。

2.4 商品服務(wù)

  • 服務(wù)接口實(shí)現(xiàn)

    商品服務(wù)提供商品的查詢(xún)以及庫(kù)存扣減接口服務(wù),實(shí)現(xiàn)openfeign-service提供的GoodClient服務(wù)接口定義如下所示:

  /**
   * 商品接口定義實(shí)現(xiàn)
   *
   * @author 恒宇少年
   */
  @RestController
  public class GoodController implements GoodClient {
      /**
       * 商品業(yè)務(wù)邏輯
       */
      @Autowired
      private GoodService goodService;
  
      /**
       * 查詢(xún)商品信息
       *
       * @param goodId {@link Good#getId()}
       * @return
       */
      @Override
      public Good findById(Integer goodId) {
          return goodService.findById(goodId);
      }
  
      /**
       * 扣減商品庫(kù)存
       *
       * @param goodId {@link Good#getId()}
       * @param stock  減少庫(kù)存的數(shù)量
       */
      @Override
      public void reduceStock(Integer goodId, int stock) {
          goodService.reduceStock(goodId, stock);
      }
  }
  • 服務(wù)配置(application.yml)
  spring:
    application:
      name: good-service
    cloud:
      alibaba:
        seata:
          tx-service-group: minbox-seata
    datasource:
      url: jdbc:mysql://localhost:3306/test
      username: root
      password: 123456
      type: com.zaxxer.hikari.HikariDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
  
  
  eureka:
    client:
      service-url:
        defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
  server:
    port: 8081
  • 導(dǎo)入Seata數(shù)據(jù)源代理配置
  /**
   * @author 恒宇少年
   */
  @SpringBootApplication
  @Import(DataSourceProxyAutoConfiguration.class)
  public class GoodServiceApplication {
      /**
       * logger instance
       */
      static Logger logger = LoggerFactory.getLogger(GoodServiceApplication.class);
  
      public static void main(String[] args) {
          SpringApplication.run(GoodServiceApplication.class, args);
          logger.info("商品服務(wù)啟動(dòng)成功.");
      }
  }

2.5 訂單服務(wù)

  • 服務(wù)接口

    訂單服務(wù)提供了下單的接口辈毯,通過(guò)調(diào)用該接口完成下單功能坝疼,下單接口會(huì)通過(guò)Openfeign調(diào)用account-servicegood-service所提供的服務(wù)接口來(lái)完成數(shù)據(jù)驗(yàn)證谆沃,如下所示:

  /**
   * @author 恒宇少年
   */
  @RestController
  @RequestMapping(value = "/order")
  public class OrderController {
      /**
       * 賬戶(hù)服務(wù)接口
       */
      @Autowired
      private AccountClient accountClient;
      /**
       * 商品服務(wù)接口
       */
      @Autowired
      private GoodClient goodClient;
      /**
       * 訂單業(yè)務(wù)邏輯
       */
      @Autowired
      private OrderService orderService;
  
      /**
       * 通過(guò){@link GoodClient#reduceStock(Integer, int)}方法減少商品的庫(kù)存钝凶,判斷庫(kù)存剩余數(shù)量
       * 通過(guò){@link AccountClient#deduction(Integer, Double)}方法扣除商品所需要的金額,金額不足由account-service拋出異常
       *
       * @param goodId    {@link Good#getId()}
       * @param accountId {@link Account#getId()}
       * @param buyCount  購(gòu)買(mǎi)數(shù)量
       * @return
       */
      @PostMapping
      @GlobalTransactional
      public String submitOrder(
              @RequestParam("goodId") Integer goodId,
              @RequestParam("accountId") Integer accountId,
              @RequestParam("buyCount") int buyCount) {
  
          Good good = goodClient.findById(goodId);
  
          Double orderPrice = buyCount * good.getPrice();
  
          goodClient.reduceStock(goodId, buyCount);
  
          accountClient.deduction(accountId, orderPrice);
  
          Order order = toOrder(goodId, accountId, orderPrice);
          orderService.addOrder(order);
          return "下單成功.";
      }
  
      private Order toOrder(Integer goodId, Integer accountId, Double orderPrice) {
          Order order = new Order();
          order.setGoodId(goodId);
          order.setAccountId(accountId);
          order.setPrice(orderPrice);
          return order;
      }
  }
  • 服務(wù)配置(application.yml)
  spring:
    application:
      name: order-service
    cloud:
      alibaba:
        seata:
          tx-service-group: minbox-seata
    datasource:
      url: jdbc:mysql://localhost:3306/test
      username: root
      password: 123456
      type: com.zaxxer.hikari.HikariDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
  
  
  eureka:
    client:
      service-url:
        defaultZone: http://service:nodev2@10.180.98.83:10001/eureka/
  server:
    port: 8082
  • 啟用Openfeign & 導(dǎo)入Seata數(shù)據(jù)源代理配置
  /**
   * @author 恒宇少年
   */
  @SpringBootApplication
  @EnableFeignClients(basePackages = "org.minbox.chapter.seata.openfeign")
  @Import(DataSourceProxyAutoConfiguration.class)
  public class OrderServiceApplication {
      /**
       * logger instance
       */
      static Logger logger = LoggerFactory.getLogger(OrderServiceApplication.class);
  
      public static void main(String[] args) {
          SpringApplication.run(OrderServiceApplication.class, args);
          logger.info("訂單服務(wù)啟動(dòng)成功.");
      }
  }

我們僅在order-service調(diào)用了其他服務(wù)的Openfeign接口,所以我們只需要在order-service內(nèi)通過(guò)@EnableFeignClients注解啟用Openfeign接口實(shí)現(xiàn)代理尔邓。

3. 服務(wù)連接Seata Server

服務(wù)想要連接到Seata Server需要添加兩個(gè)配置文件字旭,分別是registry.conffile.conf哟沫。

  • registry.conf

    注冊(cè)到Seata Server的配置文件,里面包含了注冊(cè)方式锌介、配置文件讀取方式嗜诀,內(nèi)容如下所示:

  registry {
    # file猾警、nacos、eureka隆敢、redis发皿、zk、consul
    type = "file"
  
    file {
      name = "file.conf"
    }
  
  }
  
  config {
    type = "file"
  
    file {
      name = "file.conf"
    }
  }
  • file.conf

    該配置文件內(nèi)包含了使用file方式連接到Eureka Server的配置信息以及存儲(chǔ)分布式事務(wù)信息的方式拂蝎,如下所示:

  transport {
    # tcp udt unix-domain-socket
    type = "TCP"
    #NIO NATIVE
    server = "NIO"
    #enable heartbeat
    heartbeat = true
    #thread factory for netty
    thread-factory {
      boss-thread-prefix = "NettyBoss"
      worker-thread-prefix = "NettyServerNIOWorker"
      server-executor-thread-prefix = "NettyServerBizHandler"
      share-boss-worker = false
      client-selector-thread-prefix = "NettyClientSelector"
      client-selector-thread-size = 1
      client-worker-thread-prefix = "NettyClientWorkerThread"
      # netty boss thread size,will not be used for UDT
      boss-thread-size = 1
      #auto default pin or 8
      worker-thread-size = 8
    }
  }
  ## transaction log store
  store {
    ## store mode: file雳窟、db
    mode = "file"
  
    ## file store
    file {
      dir = "sessionStore"
  
      # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
      max-branch-session-size = 16384
      # globe session size , if exceeded throws exceptions
      max-global-session-size = 512
      # file buffer size , if exceeded allocate new buffer
      file-write-buffer-cache-size = 16384
      # when recover batch read size
      session.reload.read_size = 100
      # async, sync
      flush-disk-mode = async
    }
  
    ## database store
    db {
      datasource = "druid"
      db-type = "mysql"
      driver-class-name = "com.mysql.jdbc.Driver"
      url = "jdbc:mysql://10.180.98.83:3306/iot-transactional"
      user = "dev"
      password = "dev2019."
    }
  
  }
  service {
    vgroup_mapping.minbox-seata = "default"
    default.grouplist = "10.180.98.83:8091"
    enableDegrade = false
    disable = false
  }
  client {
    async.commit.buffer.limit = 10000
    lock {
      retry.internal = 10
      retry.times = 30
    }
  }

配置文件內(nèi)service部分需要注意,我們?cè)?code>application.yml配置文件內(nèi)配置了事務(wù)分組為minbox-seata匣屡,在這里需要進(jìn)行對(duì)應(yīng)配置vgroup_mapping.minbox-seata = "default",通過(guò)default.grouplist = "10.180.98.83:8091"配置Seata Server的服務(wù)列表封救。

將上面兩個(gè)配置文件在各個(gè)服務(wù)resources目錄下創(chuàng)建。

4. 編寫(xiě)下單邏輯

在前面說(shuō)了那么多捣作,只是做了準(zhǔn)備工作誉结,我們要為每個(gè)參與下單的服務(wù)添加對(duì)應(yīng)的業(yè)務(wù)邏輯。

  • 賬戶(hù)服務(wù)

    account-service內(nèi)添加賬戶(hù)余額扣除業(yè)務(wù)邏輯類(lèi)券躁,AccountService如下所示:

  /**
   * 賬戶(hù)業(yè)務(wù)邏輯處理
   *
   * @author 恒宇少年
   */
  @Service
  @Transactional(rollbackFor = Exception.class)
  public class AccountService {
      
      @Autowired
      private EnhanceMapper<Account, Integer> mapper;
  
      /**
       * {@link EnhanceMapper} 具體使用查看ApiBoot官網(wǎng)文檔http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html
       *
       * @param accountId {@link Account#getId()}
       * @param money     扣除的金額
       */
      public void deduction(Integer accountId, Double money) {
          Account account = mapper.selectOne(accountId);
          if (ObjectUtils.isEmpty(account)) {
              throw new RuntimeException("賬戶(hù):" + accountId + "惩坑,不存在.");
          }
          if (account.getMoney() - money < 0) {
              throw new RuntimeException("賬戶(hù):" + accountId + ",余額不足.");
          }
          account.setMoney(account.getMoney().doubleValue() - money);
          mapper.update(account);
      }
  }
  • 商品服務(wù)

    good-service內(nèi)添加查詢(xún)商品也拜、扣減商品庫(kù)存的邏輯類(lèi)以舒,GoodService如下所示:

  /**
   * 商品業(yè)務(wù)邏輯實(shí)現(xiàn)
   *
   * @author 恒宇少年
   */
  @Service
  @Transactional(rollbackFor = Exception.class)
  public class GoodService {
  
      @Autowired
      private EnhanceMapper<Good, Integer> mapper;
  
      /**
       * 查詢(xún)商品詳情
       *
       * @param goodId {@link Good#getId()}
       * @return {@link Good}
       */
      public Good findById(Integer goodId) {
          return mapper.selectOne(goodId);
      }
  
      /**
       * {@link EnhanceMapper} 具體使用查看ApiBoot官網(wǎng)文檔http://apiboot.minbox.io/zh-cn/docs/api-boot-mybatis-enhance.html
       * 扣除商品庫(kù)存
       *
       * @param goodId {@link Good#getId()}
       * @param stock  扣除的庫(kù)存數(shù)量
       */
      public void reduceStock(Integer goodId, int stock) {
          Good good = mapper.selectOne(goodId);
          if (ObjectUtils.isEmpty(good)) {
              throw new RuntimeException("商品:" + goodId + ",不存在.");
          }
          if (good.getStock() - stock < 0) {
              throw new RuntimeException("商品:" + goodId + "庫(kù)存不足.");
          }
          good.setStock(good.getStock() - stock);
          mapper.update(good);
  
      }
  }

5. 提交訂單測(cè)試

我們?cè)趫?zhí)行測(cè)試之前在數(shù)據(jù)庫(kù)內(nèi)的seata_accountseata_good表內(nèi)對(duì)應(yīng)添加兩條測(cè)試數(shù)據(jù)慢哈,如下所示:

-- seata_good
INSERT INTO `seata_good` VALUES (1,'華為Meta 30',10,5000.00); 

-- seata_account
INSERT INTO `seata_account` VALUES (1,10000.00,'2019-10-11 02:37:35',NULL);

5.1 啟動(dòng)服務(wù)

將我們本章所使用good-server蔓钟、order-serviceaccount-service三個(gè)服務(wù)啟動(dòng)卵贱。

5.2 測(cè)試點(diǎn):正常購(gòu)買(mǎi)

我們添加的賬戶(hù)余額測(cè)試數(shù)據(jù)夠我們購(gòu)買(mǎi)兩件商品滥沫,我們先來(lái)購(gòu)買(mǎi)一件商品驗(yàn)證下接口訪(fǎng)問(wèn)是否成功,通過(guò)如下命令訪(fǎng)問(wèn)下單接口:

~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=1
下單成功.

通過(guò)我們?cè)L問(wèn)/order下單接口键俱,根據(jù)響應(yīng)的內(nèi)容我們確定商品已經(jīng)購(gòu)買(mǎi)成功兰绣。

通過(guò)查看order-service控制臺(tái)內(nèi)容:

2019-10-11 16:52:15.477  INFO 13142 --- [nio-8082-exec-4] i.seata.tm.api.DefaultGlobalTransaction  : [10.180.98.83:8091:2024417333] commit status:Committed
2019-10-11 16:52:16.412  INFO 13142 --- [atch_RMROLE_2_8] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=10.180.98.83:8091:2024417333,branchId=2024417341,branchType=AT,resourceId=jdbc:mysql://localhost:3306/test,applicationData=null
2019-10-11 16:52:16.412  INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler            : Branch committing: 10.180.98.83:8091:2024417333 2024417341 jdbc:mysql://localhost:3306/test null
2019-10-11 16:52:16.412  INFO 13142 --- [atch_RMROLE_2_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed

我們可以看到本次事務(wù)已經(jīng)成功Committed

再去驗(yàn)證下數(shù)據(jù)庫(kù)內(nèi)的賬戶(hù)余額编振、商品庫(kù)存是否有所扣減缀辩。

5.3 測(cè)試點(diǎn):庫(kù)存不足

測(cè)試商品添加了10個(gè)庫(kù)存,在之前測(cè)試已經(jīng)銷(xiāo)售掉了一件商品踪央,我們測(cè)試購(gòu)買(mǎi)數(shù)量超過(guò)庫(kù)存數(shù)量時(shí)臀玄,是否有回滾日志,執(zhí)行如下命令:

~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=10
{"timestamp":"2019-10-11T08:57:13.775+0000","status":500,"error":"Internal Server Error","message":"status 500 reading GoodClient#reduceStock(Integer,int)","path":"/order"}

在我們good-service服務(wù)控制臺(tái)已經(jīng)打印了商品庫(kù)存不足的異常信息:

java.lang.RuntimeException: 商品:1庫(kù)存不足.
    at org.minbox.chapter.seata.service.GoodService.reduceStock(GoodService.java:42) ~[classes/:na]
    ....

我們?cè)倏?code>order-service的控制臺(tái)打印日志:

Begin new global transaction [10.180.98.83:8091:2024417350]
2019-10-11 16:57:13.771  INFO 13142 --- [nio-8082-exec-5] i.seata.tm.api.DefaultGlobalTransaction  : [10.180.98.83:8091:2024417350] rollback status:Rollbacked

通過(guò)日志可以查看本次事務(wù)進(jìn)行了回滾杯瞻。

由于庫(kù)存的驗(yàn)證在賬戶(hù)余額扣減之前镐牺,所以我們本次并不能從數(shù)據(jù)庫(kù)的數(shù)據(jù)來(lái)判斷事務(wù)是真的回滾。

5.4 測(cè)試點(diǎn):余額不足

既然商品庫(kù)存不足我們不能直接驗(yàn)證數(shù)據(jù)庫(kù)事務(wù)回滾魁莉,我們從賬戶(hù)余額不足來(lái)下手,在之前成功購(gòu)買(mǎi)了一件商品,賬戶(hù)的余額還夠購(gòu)買(mǎi)一件商品旗唁,商品庫(kù)存目前是9件畦浓,我們本次測(cè)試購(gòu)買(mǎi)5件商品,這樣就會(huì)出現(xiàn)購(gòu)買(mǎi)商品庫(kù)存充足余額不足的應(yīng)用場(chǎng)景检疫,執(zhí)行如下命令發(fā)起請(qǐng)求:

~ curl -X POST http://localhost:8082/order\?goodId\=1\&accountId\=1\&buyCount\=5
{"timestamp":"2019-10-11T09:03:00.794+0000","status":500,"error":"Internal Server Error","message":"status 500 reading AccountClient#deduction(Integer,Double)","path":"/order"}

我們通過(guò)查看account-service控制臺(tái)日志可以看到:

java.lang.RuntimeException: 賬戶(hù):1讶请,余額不足.
    at org.minbox.chapter.seata.service.AccountService.deduction(AccountService.java:33) ~[classes/:na]

已經(jīng)拋出了余額不足的異常。

通過(guò)查看good-service屎媳、order-serivce控制臺(tái)日志夺溢,可以看到事務(wù)進(jìn)行了回滾操作。

接下來(lái)查看seata_account表數(shù)據(jù)烛谊,我們發(fā)現(xiàn)賬戶(hù)余額沒(méi)有改變风响,賬戶(hù)服務(wù)的事務(wù)回滾驗(yàn)證成功

查看seata_good表數(shù)據(jù)丹禀,我們發(fā)現(xiàn)商品的庫(kù)存也沒(méi)有改變状勤,商品服務(wù)的事務(wù)回滾驗(yàn)證成功

6. 總結(jié)

本章主要來(lái)驗(yàn)證分布式事務(wù)框架SeataMySQL下提交與回滾有效性双泪,是否能夠完成我們預(yù)期的效果持搜,Seata作為SpringCloud Alibaba的核心框架,更新頻率比較高焙矛,快速的解決使用過(guò)程中遇到的問(wèn)題葫盼,是一個(gè)潛力股,不錯(cuò)的選擇村斟。

由于本章設(shè)計(jì)的代碼比較多剪返,請(qǐng)結(jié)合源碼進(jìn)行學(xué)習(xí)。

7. 本章源碼

請(qǐng)?jiān)L問(wèn)<a target="_blank">https://gitee.com/hengboy/spring-cloud-chapter</a>查看本章源碼邓梅,建議使用git clone https://gitee.com/hengboy/spring-cloud-chapter.git將源碼下載到本地脱盲。

作者個(gè)人 博客
使用開(kāi)源框架 ApiBoot 助你成為Api接口服務(wù)架構(gòu)師

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市日缨,隨后出現(xiàn)的幾起案子钱反,更是在濱河造成了極大的恐慌,老刑警劉巖匣距,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件面哥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡毅待,警方通過(guò)查閱死者的電腦和手機(jī)尚卫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)尸红,“玉大人吱涉,你說(shuō)我怎么就攤上這事刹泄。” “怎么了怎爵?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵特石,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我鳖链,道長(zhǎng)姆蘸,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任芙委,我火速辦了婚禮逞敷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灌侣。我一直安慰自己推捐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布顶瞳。 她就那樣靜靜地躺著玖姑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪慨菱。 梳的紋絲不亂的頭發(fā)上焰络,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音符喝,去河邊找鬼闪彼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛协饲,可吹牛的內(nèi)容都是我干的畏腕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼茉稠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼描馅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起而线,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤铭污,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后膀篮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體嘹狞,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年誓竿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了磅网。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡筷屡,死狀恐怖涧偷,靈堂內(nèi)的尸體忽然破棺而出簸喂,到底是詐尸還是另有隱情,我是刑警寧澤嫂丙,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布娘赴,位于F島的核電站规哲,受9級(jí)特大地震影響跟啤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唉锌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一隅肥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袄简,春花似錦腥放、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至吕粹,卻和暖如春种柑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背匹耕。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工聚请, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稳其。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓驶赏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親既鞠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子煤傍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344