spring-cloud-alibaba 分佈式事務seata

一、seata 是什麼

Seata 是一款開源的分佈式事務解決方案,致力於在微服務架構下提供高性能和簡單易用的分佈式事務服務。
官網介紹: seata 是什麼

本次代碼github:https://github.com/LH-0811/zcloudparent
中的 cloud-alibaba-seata-example 模塊

二、AT模式

基於支持本地 ACID 事務的關係型數據庫。
Java 應用,通過 JDBC 訪問數據庫。

三、獲取seata並配置啓動

本次是基於 seata-1.0.0

3.1 下載seata

下載地址: https://seata.io/zh-cn/blog/download.html

3.2 保證nacos正常運行

參考: https://blog.csdn.net/qq_30110435/article/details/105000289

3.3 配置seata

3.3.1 修改file.conf

主要修改: 自定義事務組名稱 -> 事務日誌存儲模式 -> 數據庫連接信息

3.3.1.1. 修改server模塊,自定義 事務分組名稱

service{
	vgroup_mapping.my_test_tx_group = "default"  這個value可以自定義修改
}

3.3.1.2 修改store模塊 修改爲 數據庫存儲 默認是文件存儲

修改

store{
	
	mode = "db"

	db {
		url = "jdbc:myslq://xxxx:3306/seata"
		user = "root"
		password = "123456"
	}
}

3.3.1.3 自己在本地創建seata數據庫

3.3.1.4 執行數據庫初始化腳本

數據庫腳本在~/seata/conf/db_store.sql。如果1.0.0版本沒有該腳本,則可以下載0.9.0版本獲取該腳本

3.3.1.5 seata註冊 修改 registry.conf

registry{
	type = "nacos"

	nacos{
		serverAddr = "localhost:8848"
		namespace = ""
		cluster = "default"
	}
}

3.3.1.6 nacos 啓動

3.3.1.7 啓動seata

啓動腳本在~/seata/bin/seata-server.sh

四、準備測試用的業務數據庫

4.1 初始化數據庫

因爲業務數據庫要支持seata的業務回滾 所以需要增加undo_log表,腳本在~/seata/conf/undo_log.sql
業務數據 與seata官網的demo相似 增加了 幾個簡單數字段

4.1.1 seata_storage庫存庫創建初始化

在msql中增加庫存庫:seata_storage 並執行數據庫腳本

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for storage_tbl
-- ----------------------------
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL COMMENT '產品code',
  `count` int(11) DEFAULT '0' COMMENT '庫存',
  `price` decimal(10,2) DEFAULT NULL COMMENT '價格',
  PRIMARY KEY (`id`),
  UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of storage_tbl
-- ----------------------------
BEGIN;
INSERT INTO `storage_tbl` VALUES (1, '1', 1000, 2000.00);
COMMIT;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

SET FOREIGN_KEY_CHECKS = 1;

4.1.2 seata_account賬戶數據庫庫創建初始化

在msql中增加庫存庫:seata_account 並執行數據庫腳本

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account_tbl
-- ----------------------------
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
  `money` int(11) DEFAULT '0' COMMENT '用戶餘額',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of account_tbl
-- ----------------------------
BEGIN;
INSERT INTO `account_tbl` VALUES (1, '1', 1000);
INSERT INTO `account_tbl` VALUES (2, '2', 5000);
COMMIT;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

SET FOREIGN_KEY_CHECKS = 1;

4.1.3 seata_order訂單數據庫創建初始化

在msql中增加庫存庫:seata_order 並執行數據庫腳本

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for order_tbl
-- ----------------------------
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
  `commodity_code` varchar(255) DEFAULT NULL COMMENT '產品code',
  `count` int(11) DEFAULT '0' COMMENT '數量',
  `money` decimal(11,2) DEFAULT '0.00' COMMENT '金額',
  `order_status` tinyint(1) DEFAULT NULL COMMENT '0-創建中 1-創建成功',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1241897922458800129 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

SET FOREIGN_KEY_CHECKS = 1;

4.2 創建業務工程

這裏就只寫關鍵代碼了
工程的模塊劃分
源碼可以去 github查看

  • spring-cloud-seate-example [父工程 | pom]
    • storage-module[庫存服務 pom]
      • storage-api [庫存api對外提供的pojo、api、vo | jar ]
      • storage-service [庫存服務 | jar ]
    • account-module[賬戶服務 pom]
      • account-api[賬戶api對外提供的pojo、api、vo | jar ]
      • account-service [賬戶服務 | jar ]
    • order-module [訂單服務 pom]
      • order-api[訂單api對外提供的pojo、api、vo | jar ]
      • order-service [訂單服務 | jar ]

因爲每個服務都有支持seata所以每個服務都需要添加seata需要的配置文件
在微服務中添加seata配置文件
處理 file.conf 文件
將file.conf文件 複製到微服務的resources目錄下
修改 file.conf文件中
service.vgroup_mapping.my_test_tx_group
將後面my_test_tx_group 替換成我們在seata/conf/file.conf中 自定義的事務分組名稱
其value 可以隨意修改
修改後 微服務下的file.conf 文件爲

service {
  #transaction service group mapping
  vgroup_mapping.seata_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #disable seata
  disableGlobalTransaction = false
}

## transaction log store, only used in seata-server
store {
  ## store mode: file、db
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "123456"
  }
}

處理 register.conf 文件
同樣將seata/conf/register.conf 文件複製到微服務的resources下

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"

  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

這裏要注意,因爲是要存儲到nacos 所以 nacos的鏈接地址要正確

4.2.1 父工程POM

添加版本管理

 <dependencyManagement>
        <dependencies>
            <!-- 版本對照 https://spring.io/projects/spring-cloud-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.2.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- 版本對照 https://start.spring.io/actuator/info-->
            <!-- "Hoxton.SR3": "Spring Boot >=2.2.0.M4 and <2.2.6.BUILD-SNAPSHOT",-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <!--SR3 版本 與openfeign整合sentinel會報錯-->
                <version>Hoxton.SR1</version>

                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--cloud alibaba https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md-->
            <!--2.2.x 版本適用於 Spring Boot 2.2.x-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- 數據庫連接池 -->
</dependencyManagement>

4.2.2 storage-module 庫存模塊

4.2.2.1 storage-service

增加pom依賴 這裏只貼 需要新增的pom節點

 <!--本工程需要使用到的pojo param 等實體類-->
 <dependency>
     <groupId>com.lhit.spirngcloud3</groupId>
     <artifactId>storage-api</artifactId>
     <version>1.0-SNAPSHOT</version>
 </dependency>


 <!-- 引入nacos服務發現 -->
 <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-seata</artifactId>
 </dependency>

 <!-- 微服務調用客戶端 -->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
 </dependency>

修改yml配置

server:
  port: 9202
spring:
  application:
    name: cloud-seata-storage-server
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&useSSL=true&serverTimezone=GMT%2B8
  jackson:
    time-zone: GMT+8
    generator:
      WRITE_NUMBERS_AS_STRINGS: true
  cloud:
    nacos: # 配置nacos
      discovery: # 服務發現地址
        server-addr: 127.0.0.1:8848 # nacos 啓動地址
    alibaba:
      seata:
        tx-service-group: default # 自定義事務組名稱 與file.conf 中seata-server 定義的一致
mybatis:
  configuration:
    map-underscore-to-camel-case: true
  type-aliases-package: com.lhit.springcloud3.seata
mapper:
  not-empty: false
  identity: MYSQL
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql

management: # 健康檢查暴露端點
  endpoints:
    web:
      exposure:
        include: "*"


這個模塊寫一個減庫存的服務 service


import com.lhit.springcloud3.common.entity.CommonMethod;
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.common.exception.CommonException;
import com.lhit.springcloud3.seata.storage.dao.StorageDao;
import com.lhit.springcloud3.seata.storage.param.SubtractionStorageParam;
import com.lhit.springcloud3.seata.storage.pojo.Storage;
import com.lhit.springcloud3.seata.storage.service.StorageService;
import com.lhit.springcloud3.seata.storage.vo.SubtractionStorageVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;

import java.math.BigDecimal;

@Slf4j
@Service("storageService")
public class StorageServiceImpl implements StorageService {

    @Autowired
    private StorageDao storageDao;

    // 減庫存
    @Override
    public SubtractionStorageVo subtractionStorageVo(SubtractionStorageParam param) throws Exception {
        // 檢查參數
        CommonMethod.checkParam("StorageServiceImpl", "subtractionStorageVo", param);

        // 查詢數據合法性
        Storage storage = null;
        try {
            Storage selectStorage = new Storage();
            selectStorage.setCommodityCode(param.getCommodityCode());
            storage = storageDao.selectOne(selectStorage);
        } catch (Exception e) {
            throw CommonException.createWithNotFound_404(e, ResponseEntityBody.create("沒有找打庫存信息"));
        }

        // 扣減庫存
        try {
            Storage storageUpdate = new Storage();
            storageUpdate.setId(storage.getId());
            storageUpdate.setCount(storage.getCount() - param.getCount());
            storageDao.updateByPrimaryKeySelective(storageUpdate);
        } catch (Exception e) {
            throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("扣減庫存失敗"));
        }

        log.info("扣減庫存完成");
        return new SubtractionStorageVo(param.getCommodityCode(), param.getCount(), storage.getPrice().multiply(new BigDecimal(param.getCount())));
    }

}

公開一個controller

@Slf4j
@RestController
@RequestMapping("/storage")
@Api(description = "庫存接口")
public class StorageController {


    @Autowired
    private StorageService storageService;


    @PostMapping("/subtraction")
    @ApiOperation("扣減庫存")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "body", dataType = "SubtractionStorageParam", dataTypeClass = SubtractionStorageParam.class, name = "param", value = "參數"),
    })
    public ResponseEntity<ResponseEntityBody<SubtractionStorageVo>> subtractionStorage(@RequestBody SubtractionStorageParam param) throws Exception {
        log.info(">>>>>>>>>>>進入創建訂單接口OrderController.createOrder>>>>>>>>>>>>>>>>>>>>>>");
        SubtractionStorageVo vo = storageService.subtractionStorageVo(param);
        return ResponseEntity.ok(ResponseEntityBody.create("扣減成功",vo));
    }

}

4.2.2.2 storage-api

這個模塊主要是公開庫存服務的api和實體類以及請求參數和vo
暴露減庫存api 方便消費者直接使用

import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.seata.storage.param.SubtractionStorageParam;
import com.lhit.springcloud3.seata.storage.vo.SubtractionStorageVo;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

public interface StorageApi {

    @PostMapping("/storage/subtraction")
    @ApiOperation("扣減庫存")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "body", dataType = "SubtractionStorageParam", dataTypeClass = SubtractionStorageParam.class, name = "param", value = "參數"),
    })
    ResponseEntity<ResponseEntityBody<SubtractionStorageVo>> subtractionStorage(@RequestBody SubtractionStorageParam param) throws Exception;
}

4.2.3 account-module 賬戶模塊

4.2.3.1 account-service

修改pom


<!--本工程需要使用到的pojo param 等實體類-->
 <dependency>
     <groupId>com.lhit.spirngcloud3</groupId>
     <artifactId>account-api</artifactId>
     <version>1.0-SNAPSHOT</version>
 </dependency>

 <!-- 引入nacos服務發現 -->
 <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-seata</artifactId>
 </dependency>

 <!-- 微服務調用客戶端 -->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
 </dependency>

修改yml

server:
  port: 9203
spring:
  application:
    name: cloud-seata-account-server
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&useSSL=true&serverTimezone=GMT%2B8
  jackson:
    time-zone: GMT+8
    generator:
      WRITE_NUMBERS_AS_STRINGS: true
  cloud:
    nacos: # 配置nacos
      discovery: # 服務發現地址
        server-addr: 127.0.0.1:8848 # nacos 啓動地址
    alibaba:
      seata:
        tx-service-group: default # 自定義事務組名稱 與file.conf 中seata-server 定義的一致
mybatis:
  configuration:
    map-underscore-to-camel-case: true
  type-aliases-package: com.lhit.springcloud3.seata
mapper:
  not-empty: false
  identity: MYSQL
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql
management: # 健康檢查暴露端點
  endpoints:
    web:
      exposure:
        include: "*"

編寫service

import com.lhit.springcloud3.common.entity.CommonMethod;
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.common.exception.CommonException;
import com.lhit.springcloud3.seata.account.dao.AccountDao;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import com.lhit.springcloud3.seata.account.pojo.Account;
import com.lhit.springcloud3.seata.account.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;



    // 扣減餘額
    @Override
    public void accountSubtraction(AccountSubtractionParam param) throws Exception {

        // 檢查參數
        CommonMethod.checkParam("AccountServiceImpl", "accountSubtraction", param);

        // 檢查參數有效性
        Account account = null;
        try {
            Account selectAccount = new Account();
            selectAccount.setUserId(param.getUserId());
            account = accountDao.selectOne(selectAccount);
        } catch (Exception e) {
            throw CommonException.createWithNotFound_404(e, ResponseEntityBody.create("沒有獲取到賬戶信息"));
        }

        if (param.getAmt().compareTo(account.getMoney()) > 0){
            throw CommonException.createWithMethodNotAcceptable_406(ResponseEntityBody.create("餘額不足"));
        }

        // 扣減餘額
        try {
            account.setMoney(account.getMoney().subtract(param.getAmt()));
            accountDao.updateByPrimaryKeySelective(account);
        }catch (Exception e){
            throw CommonException.createWithServerError_500(e,ResponseEntityBody.create("扣減餘額失敗"));
        }

        log.info("扣減餘額完成");
    }

}

通過controller公開接口

import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import com.lhit.springcloud3.seata.account.service.AccountService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/account")
@Api(description = "賬戶接口")
public class AccountController {

    @Autowired
    private AccountService accountService;

    @PostMapping("/subtraction")
    @ApiOperation("扣減餘額")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "body", dataType = "AccountSubtractionParam", dataTypeClass = AccountSubtractionParam.class, name = "param", value = "參數"),
    })
    public ResponseEntity<ResponseEntityBody<AccountSubtractionParam>> accountSubtraction(@RequestBody AccountSubtractionParam param) throws Exception {
        log.info(">>>>>>>>>>>進入創建訂單接口OrderController.createOrder>>>>>>>>>>>>>>>>>>>>>>");
        accountService.accountSubtraction(param);
        return ResponseEntity.ok(ResponseEntityBody.create("扣減成功"));
    }
}

4.2.2.2 account-api

這個模塊主要是公開庫存服務的api和實體類以及請求參數和vo
暴露減庫存api 方便消費者直接使用

i
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

public interface AccountApi {

    @PostMapping("/account/subtraction")
    @ApiOperation("扣減餘額")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "body", dataType = "AccountSubtractionParam", dataTypeClass = AccountSubtractionParam.class, name = "param", value = "參數"),
    })
    ResponseEntity<ResponseEntityBody<AccountSubtractionParam>> accountSubtraction(@RequestBody AccountSubtractionParam param) throws Exception;
}

4.2.3 order-module訂單模塊

不在贅述 api和pom以及yml的修改 與前面兩個模塊一樣
這裏只寫關鍵的service和feign客戶端
使用feign來創建調用 庫存好賬號服務的客戶端

@FeignClient(serviceId = "cloud-seata-account-server")
public interface AccountService extends AccountApi {
}
@FeignClient(serviceId = "cloud-seata-storage-server")
public interface StorageService extends StorageApi {
}

自身需要實現的service

public interface OrderService {
    // 創建訂單
    Order createOrder(OrderCreateParam param) throws Exception;

    // 設置訂單到創建完成狀態
    void setOrderCreateFinish(OrderSetFinishParam param) throws Exception;
}

service 實現代碼
這裏要 創建訂單 -> 扣庫存 -> 扣賬戶餘額 ->更新訂單狀態

import com.lhit.springcloud3.common.entity.CommonMethod;
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.common.exception.CommonException;
import com.lhit.springcloud3.common.util.IdWorker;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import com.lhit.springcloud3.seata.order.dao.OrderDao;
import com.lhit.springcloud3.seata.order.param.OrderCreateParam;
import com.lhit.springcloud3.seata.order.param.OrderSetFinishParam;
import com.lhit.springcloud3.seata.order.pojo.Order;
import com.lhit.springcloud3.seata.order.service.AccountService;
import com.lhit.springcloud3.seata.order.service.OrderService;
import com.lhit.springcloud3.seata.order.service.StorageService;
import com.lhit.springcloud3.seata.storage.param.SubtractionStorageParam;
import com.lhit.springcloud3.seata.storage.vo.SubtractionStorageVo;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.util.Map;

@Slf4j
@Service("orderService")
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;

    @Autowired
    private IdWorker idWorker;

    @Autowired
    private StorageService storageService;

    @Autowired
    private AccountService accountService;

    // 創建訂單
    @Override
    // 添加seata 全局事務註解 name 唯一
    @GlobalTransactional(name = "create_order",rollbackFor = Exception.class)
    public Order createOrder(OrderCreateParam param) throws Exception {
        // 檢查參數
        CommonMethod.checkParam("進入OrderServiceImpl", "createOrder", param);

        // 拼接實體類
        Order order = null;
        try {
            order = new Order();
            BeanUtils.copyProperties(param, order);
            // 補充屬性
            order.setId(idWorker.nextId());
            order.setOrderStatus(false);
        } catch (Exception e) {
            throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("拼裝實體類失敗"));
        }


        log.info("----> 創建訂單");
        orderDao.insertSelective(order);
        log.info("----> 調用 庫存服務 扣減庫存");
        ResponseEntity<ResponseEntityBody<SubtractionStorageVo>> subtractionStorageResponse = null;
        try {
            SubtractionStorageParam subtractionStorageParam = new SubtractionStorageParam();
            subtractionStorageParam.setCommodityCode(param.getCommodityCode());
            subtractionStorageParam.setCount(param.getCount());
            subtractionStorageResponse = storageService.subtractionStorage(subtractionStorageParam);
        } catch (Exception e) {
            throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("扣減庫存失敗"));
        }
        if (!subtractionStorageResponse.getStatusCode().is2xxSuccessful()) {
            throw CommonException.createWithServerError_500(subtractionStorageResponse.getBody());
        }
        log.info("----> 完成 庫存服務 扣減庫存");

        log.info("----> 調用 賬戶服務 扣減餘額");
        // 獲取到庫存扣減後的信息
        ResponseEntityBody<SubtractionStorageVo> subtractionStorageVo = subtractionStorageResponse.getBody();
        ResponseEntity<ResponseEntityBody<AccountSubtractionParam>> accountSubtractionResponse = null;
        try {
            AccountSubtractionParam accountSubtractionParam = new AccountSubtractionParam();
            accountSubtractionParam.setAmt(subtractionStorageVo.getData().getTotalPrice());
            accountSubtractionParam.setUserId(param.getUserId());
            accountSubtractionResponse = accountService.accountSubtraction(accountSubtractionParam);
        } catch (Exception e) {
            throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("扣減賬戶餘額失敗"));
        }
        if (!accountSubtractionResponse.getStatusCode().is2xxSuccessful()) {
            throw CommonException.createWithServerError_500(subtractionStorageResponse.getBody());
        }
        log.info("----> 完成 賬戶服務 扣減餘額");

        log.info("----> 修改訂單狀態 開始");
        OrderSetFinishParam orderSetFinishParam = new OrderSetFinishParam();
        orderSetFinishParam.setUserId(param.getUserId());
        orderSetFinishParam.setOrderId(order.getId());
        orderSetFinishParam.setMoney(subtractionStorageVo.getData().getTotalPrice());
        setOrderCreateFinish(orderSetFinishParam);
        log.info("----> 修改訂單狀態 完成");

        log.info("完成 訂單創建");
        return order;
    }

    // 設置訂單到創建完成狀態
    @Override
    public void setOrderCreateFinish(OrderSetFinishParam param) throws Exception {
        // 檢查參數
        CommonMethod.checkParam("進入OrderServiceImpl", "setOrderCreateFinish", param);

        // 檢查數據是否有效
        Order order = orderDao.selectByPrimaryKey(param.getOrderId());
        if (order == null) {
            throw CommonException.createWithBadRequest_400(ResponseEntityBody.create("訂單不存在"));
        }

        // 檢查數據是否越界
        if (!order.getUserId().equals(param.getUserId())) {
            throw CommonException.createWithForbidden_403(ResponseEntityBody.create("非法操作,不可修改不屬於用戶的訂單"));
        }

        // 拼接修改參數
        try {
            Order orderUpdate = new Order();
            orderUpdate.setId(param.getOrderId());
            orderUpdate.setOrderStatus(true);
            orderUpdate.setMoney(param.getMoney());
            orderDao.updateByPrimaryKeySelective(orderUpdate);
        } catch (Exception e) {
            throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("修改訂單狀態失敗"));
        }

        log.info("完成訂單狀態修改");
    }

}

五 、 總結

通過seata 可以很友好的實現分佈式事務的控制。
只需要在業務代碼添加 @GlobalTransactional(name = “create_order”,rollbackFor = Exception.class) 註解

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