天天看點

【精品】seata綜合示例:訂單-庫存-扣款

有關seata的安裝,請參看部落格:https://blog.51cto.com/lianghecai/5759100

業務需求:下單—減庫存—扣錢—改訂單狀态

當使用者下單時,會在訂單服務中建立一個訂單,然後通過遠端調用庫存服務來扣減下單商品的庫存;再通過遠端調用賬戶服務來扣減使用者賬戶裡面的餘額;最後訂單服務中修改訂單狀态。

需要涉及到三個子產品:

子產品名 資料庫名 port Context-path Application-name
fafu-account seata_account 3001 fafu Fafu-account-3001
fafu-storage seata_storage 4001 fafu Fafu-storage-4001
fafu-order seata_order 5001 fafu Fafu-order-5001

可以看出:該操作需要跨域三個資料庫,有兩次遠端調用,需要用到分布式事務技術。

資料庫準備

建立seata_order資料庫,然後在其中建立表tb_order:

CREATE TABLE `tb_order`  (
  `id` bigint UNSIGNED NOT NULL COMMENT '訂單編号',
  `user_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '使用者編号',
  `sku_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '商品編号',
  `count` int UNSIGNED NULL DEFAULT NULL COMMENT '數量',
  `money` decimal(10, 3) UNSIGNED NULL DEFAULT NULL COMMENT '金額',
  `state` int UNSIGNED NULL DEFAULT NULL COMMENT '狀态',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
           
上面表的設計隻是模拟性質的,大概對應的是拼多多的業務,一個訂單中隻能購買1種商品,但這種商品可以購買多個。
           

建立seata_storage資料庫,然後在其中建立表tb_storage

CREATE TABLE `tb_storage`  (
  `id` bigint UNSIGNED NOT NULL COMMENT '編号',
  `sku_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '商品編号',
  `total` int UNSIGNED NULL DEFAULT NULL COMMENT '總庫存',
  `used` int UNSIGNED NULL DEFAULT NULL COMMENT '已用庫存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
           

建立seata_account資料庫,然後在其中建立表tb_account:

CREATE TABLE `tb_account`  (
  `id` bigint UNSIGNED NOT NULL COMMENT '編号',
  `total` decimal(11, 3) UNSIGNED NULL DEFAULT NULL COMMENT '總額度',
  `used` decimal(11, 3) UNSIGNED NULL DEFAULT NULL COMMENT '已用額度',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
           

依次在三個資料庫建立undo_log 表,用于儲存需要復原的資料:

CREATE TABLE `undo_log`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid` ASC, `branch_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
           

注:每個服務對應的業務資料庫都需要建立這個日志表。

項目共同部分

Nacos中

在Nacos添加給用戶端加載使用的配置seata-client-demo.yml:

  • Data Id:seata-client-demo.yml
  • Group:SEATA_GROUP
    【精品】seata綜合示例:訂單-庫存-扣款

注意:上面配置中的group和nampespace都是在Nacos中建立命名空間時建立的。

上面配置的具體内容如下:

# seata配置
seata:
  enabled: true
  application-id: seata-cilent-deme
  # Seata 事務組編号,此處需于 seata 相同
  tx-service-group: default-tx-group
  config:
    type: nacos
    nacos:
      # nacos ip位址
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: 99970600-6f67-43fd-a2a2-4e9795947bf4
      data-id: seata-server.properties
  registry:
    type: nacos
    nacos:
      application: seata-server
      # nacos ip位址
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: 99970600-6f67-43fd-a2a2-4e9795947bf4
           

依賴

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- 持久層相關 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--  nacos自從2020版本之後不再整合的是Netflix,也就沒有ribbon了 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- sentinel配置持久化到Nacos -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
           

Result

@Getter
@ToString
public class Result<T> {
    /**
     * 請求響應狀态碼
     */
    private int code;
    /**
     * 請求結果描述資訊
     */
    private String msg;
    /**
     * 請求結果資料
     */
    private T data;
    public Result<T> setCode(int code) {
        this.code = code;
        return this;
    }

    public Result<T> setMsg(String msg) {
        this.msg = msg;
        return this;
    }

    public Result<T> setData(T data) {
        this.data = data;
        return this;
    }

    public static void main(String[] args) {
        Result<Object> result = new Result<>().addData("a", "aaa", "b", 123, "c", new Date());
        System.out.println(result);
    }

    /**
     * 将key-value形式的成對出現的參數轉換為JSON
     *
     * @param objs
     * @return
     */
    public Result<T> addData(Object... objs) {
        if (objs.length % 2 != 0) {
            throw new RuntimeException("參數個數不對");
        }
        for (int i = 0; i < objs.length; i += 2) {
            if (!(objs[i] instanceof String)) {
                throw new RuntimeException("奇數參數必須為字元串");
            }
        }

        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < objs.length; i += 2) {
            map.put((String) objs[i], objs[i + 1]);
        }

        this.data = (T) map;
        return this;
    }
}
           

ResultUtil

public class ResultUtil {
    public static <T> Result<T> success() {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("成功");
        return result;
    }

    public static <T> Result<T> success(Integer code, String msg) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("成功");
        result.setData(data);
        return result;
    }

    public static <T> Result<T> error() {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMsg("失敗");
        return result;
    }

    public static <T> Result<T> error(Integer code, String msg) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }


    public static void main(String[] args) {
        Result error = ResultUtil.error().addData("a", "aaaa", "b", LocalDateTime.now(), "c", true);
        System.out.println(error);

        //ResultBean res = ResultBeanUtil.success(DataUtil.build(UserVO.class));
        //System.out.println(res);
    }
}
           

druid資料源代理配置

  • MyBatis版本
@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:/mapper/*.xml"));
        return factoryBean.getObject();
    }
}
           
  • MyBatisPlus版本
@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSource druidDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
}
           

在2.2.0.RELEASE及以後,資料源代理自動實作了,不需要再手動去配置一個代理類。

seata-account-3001

第一步;建立項目,添加依賴,修改application.yml檔案:

server:
  port: 3001
  servlet:
    context-path: /seata

spring:
  application:
    name: seata-account-3001
  #配置資料源
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/seata_account?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&failOverReadOnly=false
      username: root
      password: root
      # 連接配接池的配置資訊:初始化大小,最小,最大
      initial-size: 8
      min-idle: 1
      max-active: 20
      # 配置擷取連接配接等待逾時的時間
      max-wait: 60000
      # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接配接,機關是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一個連接配接在池中最小生存的時間,機關是毫秒
      min-evictable-idle-time-millis: 300000
      #驗證庫是否正常sql
      validation-query: select 'x' from dual
      #空閑時驗證,防止連接配接斷開
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打開PSCache,并且指定每個連接配接上PSCache的大小
      pool-prepared-statements: true
      max-open-prepared-statements: 20
      max-pool-prepared-statement-per-connection-size: 20
      # 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用于防火牆
      filters: stat,wall,slf4j
      # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 合并多個DruidDataSource的監控資料
      use-global-data-source-stat: true
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # 配置nacos位址
    sentinel:
      transport:
        # 配置sentinel dashboard位址
        dashboard: 127.0.0.1:8080
        # 預設端口8719,假如被占用會自動從8719開始依次+1掃描,直至找到未被占用的端口
        port: 8719
  config:
    import:
      - optional:nacos:seata-client-demo.yml

seata:
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

management:
  endpoints:
    web:
      exposure:
        include: '*' #暴露出所有的端點

#MyBatisPlus相關配置
mybatis-plus:
  #mapper映射檔案位置,多個目錄用逗号或者分号分隔(告訴 Mapper 所對應的 XML 檔案位置)
  mapper-locations: classpath:mapper/*.xml
  #實體掃描,多個package用逗号或者分号分隔
  typeAliasesPackage: com.seata.account.domain
  #  以下配置均有預設值,可以不設定
  global-config:
    db-config:
      #主鍵類型
      id-type: auto

      table-underline: true

      #資料庫大寫下劃線轉換
      capital-mode: true
  configuration:
    # 是否開啟自動駝峰命名規則映射:從資料庫列名到Java屬性駝峰命名的類似映射
    map-underscore-to-camel-case: true

    cache-enabled: false
    #配置JdbcTypeForNull
    jdbc-type-for-null: 'null'
    # 如果查詢結果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段
    call-setters-on-nulls: true
    # 将執行的sql列印出來,在開發或測試的時候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
           

第二步:項目主啟動類:

@EnableDiscoveryClient
@SpringBootApplication
public class SeataAccountApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataAccountApplication.class, args);
    }
}
           

第三步:利用MyBatisCodeHelperPro生成Account表對應的實體類、Mapper、Service代碼。

第四步:控制器AccountController

@RestController
@RequestMapping("/account")
public class AccountController {
    @Resource
    private AccountService accountService;

    @PostMapping("/decrease")
    Result decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
        //模拟異常,全局事務復原
        // System.out.println(3/0);      //--------------------①
        accountService.update(Wrappers.<Account>lambdaUpdate()
                .eq(Account::getId, userId)
                .setSql("used = used + " + money)
        );
        return ResultUtil.success().setMsg("扣款成功");
    }
}
           

啟動項目,在Nacos中會看到:

【精品】seata綜合示例:訂單-庫存-扣款

post方式請求:http://localhost:3001/seata/account/decrease?userId=1&money=15

能夠正常通路。

seata-storage-4001

第一步;建立項目,添加依賴,修改application.yml檔案:

server:
  port: 4001
  servlet:
    context-path: /seata

spring:
  application:
    name: seata-storage-4001
  #配置資料源
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/seata_storage?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&failOverReadOnly=false
      username: root
      password: root
      # 連接配接池的配置資訊:初始化大小,最小,最大
      initial-size: 8
      min-idle: 1
      max-active: 20
      # 配置擷取連接配接等待逾時的時間
      max-wait: 60000
      # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接配接,機關是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一個連接配接在池中最小生存的時間,機關是毫秒
      min-evictable-idle-time-millis: 300000
      #驗證庫是否正常sql
      validation-query: select 'x' from dual
      #空閑時驗證,防止連接配接斷開
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打開PSCache,并且指定每個連接配接上PSCache的大小
      pool-prepared-statements: true
      max-open-prepared-statements: 20
      max-pool-prepared-statement-per-connection-size: 20
      # 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用于防火牆
      filters: stat,wall,slf4j
      # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 合并多個DruidDataSource的監控資料
      use-global-data-source-stat: true
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # 配置nacos位址
    sentinel:
      transport:
        # 配置sentinel dashboard位址
        dashboard: 127.0.0.1:8080
        # 預設端口8719,假如被占用會自動從8719開始依次+1掃描,直至找到未被占用的端口
        port: 8719
  config:
    import:
      - optional:nacos:seata-client-demo.yml

seata:
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

management:
  endpoints:
    web:
      exposure:
        include: '*' #暴露出所有的端點

#MyBatisPlus相關配置
mybatis-plus:
  #mapper映射檔案位置,多個目錄用逗号或者分号分隔(告訴 Mapper 所對應的 XML 檔案位置)
  mapper-locations: classpath:mapper/*.xml
  #實體掃描,多個package用逗号或者分号分隔
  typeAliasesPackage: com.seata.storage.domain
  #  以下配置均有預設值,可以不設定
  global-config:
    db-config:
      #主鍵類型
      id-type: auto

      table-underline: true

      #資料庫大寫下劃線轉換
      capital-mode: true
  configuration:
    # 是否開啟自動駝峰命名規則映射:從資料庫列名到Java屬性駝峰命名的類似映射
    map-underscore-to-camel-case: true

    cache-enabled: false
    #配置JdbcTypeForNull
    jdbc-type-for-null: 'null'
    # 如果查詢結果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段
    call-setters-on-nulls: true
    # 将執行的sql列印出來,在開發或測試的時候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
           

第二步:項目主啟動類添加注解

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消資料源自動建立
public class SeataStorageApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageApplication.class, args);
    }
}
           

第三步:利用MyBatisCodeHelperPro生成Storage表對應的實體類、Mapper、Service代碼。

第四步:控制器StorageController

@RestController
@RequestMapping("/storage")
public class StorageController {
    @Resource
    private StorageService storageService;

    @PostMapping("/decrease")
    Result decrease(@RequestParam("skuId") Long skuId, @RequestParam("count") Integer count) {
        storageService.update(Wrappers.<Storage>lambdaUpdate()
                .eq(Storage::getSkuId, skuId)
                .setSql("used= used+" + count)
        );
        return ResultUtil.success().setMsg("庫存扣減成功");
    }
}
           

啟動項目,在Nacos中會看到:

【精品】seata綜合示例:訂單-庫存-扣款

post方式請求:http://localhost:4001/seata/storage/decrease?skuId=1&money=3

能夠正常通路。

seata-order-5001

第一步;建立項目,添加依賴,修改application.yml檔案:

server:
  port: 5001
  servlet:
    context-path: /seata

spring:
  application:
    name: seata-order-5001
  #配置資料源
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/seata_order?useSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&failOverReadOnly=false
      username: root
      password: root
      # 連接配接池的配置資訊:初始化大小,最小,最大
      initial-size: 8
      min-idle: 1
      max-active: 20
      # 配置擷取連接配接等待逾時的時間
      max-wait: 60000
      # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接配接,機關是毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一個連接配接在池中最小生存的時間,機關是毫秒
      min-evictable-idle-time-millis: 300000
      #驗證庫是否正常sql
      validation-query: select 'x' from dual
      #空閑時驗證,防止連接配接斷開
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打開PSCache,并且指定每個連接配接上PSCache的大小
      pool-prepared-statements: true
      max-open-prepared-statements: 20
      max-pool-prepared-statement-per-connection-size: 20
      # 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用于防火牆
      filters: stat,wall,slf4j
      # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 合并多個DruidDataSource的監控資料
      use-global-data-source-stat: true
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # 配置nacos位址
    sentinel:
      transport:
        # 配置sentinel dashboard位址
        dashboard: 127.0.0.1:8080
        # 預設端口8719,假如被占用會自動從8719開始依次+1掃描,直至找到未被占用的端口
        port: 8719
  config:
    import:
      - optional:nacos:seata-client-demo.yml

seata:
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

management:
  endpoints:
    web:
      exposure:
        include: '*' #暴露出所有的端點

#MyBatisPlus相關配置
mybatis-plus:
  #mapper映射檔案位置,多個目錄用逗号或者分号分隔(告訴 Mapper 所對應的 XML 檔案位置)
  mapper-locations: classpath:mapper/*.xml
  #實體掃描,多個package用逗号或者分号分隔
  typeAliasesPackage: com.seata.order.domain
  #  以下配置均有預設值,可以不設定
  global-config:
    db-config:
      #主鍵類型
      id-type: auto

      table-underline: true

      #資料庫大寫下劃線轉換
      capital-mode: true
  configuration:
    # 是否開啟自動駝峰命名規則映射:從資料庫列名到Java屬性駝峰命名的類似映射
    map-underscore-to-camel-case: true

    cache-enabled: false
    #配置JdbcTypeForNull
    jdbc-type-for-null: 'null'
    # 如果查詢結果中包含空值的列,則 MyBatis 在映射的時候,不會映射這個字段
    call-setters-on-nulls: true
    # 将執行的sql列印出來,在開發或測試的時候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
           

第二步:項目主啟動類上添加注解:

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消資料源自動建立
public class SeataOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderApplication.class, args);
    }
}
           

第三步:利用MyBatisCodeHelperPro生成Order表對應的實體類、Mapper、Service代碼。

第四步:在feign包下建立:

  • AccountFeignService.java
@FeignClient(name = "seata-account-3001", path = "/seata")
@RequestMapping("/account")
public interface AccountFeignService {
    /**
     * 将skuId對應的商品的庫存減少count
     * @param userId
     * @param money
     * @return
     */
    @PostMapping("/decrease")
    Result decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
           
  • StorageFeignService.java
@FeignClient(name = "seata-storage-4001", path = "/seata")
@RequestMapping("/storage")
public interface StorageFeignService {
    /**
     * 将skuId對應的商品的庫存減少count
     * @param skuId
     * @param count
     * @return
     */
    @PostMapping("/decrease")
    Result decrease(@RequestParam("skuId") Long skuId, @RequestParam("count") Integer count);
}
           

第五步:修改OrderServiceImpl的代碼如下所示:

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StorageFeignService storageFeignService;
    @Resource
    private AccountFeignService accountFeignService;

    @Override
    //  @GlobalTransactional(rollbackFor = Exception.class)  //全局異常   //----------------②
    public void create(Order order) {
        System.out.println("user建訂單");
        orderMapper.insert(order);

        System.out.println("扣減庫存");
        storageFeignService.decrease(order.getSkuId(),order.getCount());

        System.out.println("賬戶餘額扣減");
        accountFeignService.decrease(order.getUserId(),order.getMoney());

        System.out.println("修改訂單的狀态");
        //1表示完成狀态
        orderMapper.updateStateByUserId(order.getUserId(),1);

        System.out.println("下單結束");
    }
}
           

第六步:控制器OrderController

@RestController
@RequestMapping("/order")
public class OrderController {
    @Resource
    private OrderService orderService;

    @GetMapping("/create")
    public Result create(Order order){
        orderService.create(order);
        return ResultUtil.success().setMsg("下單成功!");
    }
}
           

測試

  • 保持編号①和編号②處于注釋狀态:請求url,tb_order表增加了一條記錄,使用者賬戶用掉30,庫存用掉10。
  • 保持編号②處于注釋狀态,去掉編号①前的注釋:請求url,tb_order表增加了一條記錄但狀态值為null,使用者賬戶沒有發生變化,庫存用掉10。沒有能夠保證資料的完整性
  • 去掉編号①和編号②前的注釋:請求url,三個表的資料都沒有發生變化。