Seata integrates multiple data sources dynamic-datasource-spring-boot-starter | Spring Cloud 60

I. Introduction

Through the following series of chapters:

docker-compose implements high-availability deployment of Seata Server | Spring Cloud 51

Seata AT mode theory study, transaction isolation and partial source code analysis | Spring Cloud 52

Spring Boot integrates Seata with AT mode distributed transaction example | Spring Cloud 53

Seata XA mode theory learning, use and precautions | Spring Cloud54

Seata TCC mode theory learning, production-level use example construction and precautions | Spring Cloud55

Solve the problems of idempotence, suspension and empty rollback in Seata TCC mode | Spring Cloud56

Seata Saga mode theory learning, production-level use example construction and precautions (1) | Spring Cloud57

Seata Saga mode theory learning, production-level use example construction and precautions (2) | Spring Cloud58

Comparison summary of Seata's four modes | Spring Cloud 59

We have completed the theoretical study and multi-dimensional comparison and summary of Seataits AT, XA, TCC, and transaction modes, and gained a deep understanding of their use through business examples. SagaIn this article, we use Seata to integrate multiple data source scenarios. We use the function of switching between multiple data sources dynamic-datasource-spring-boot-starterto complete, and this component can also Seatabe integrated with to realize the proxy of data sources.

dynamic-datasource-spring-boot-starterOfficial website address: https://github.com/baomidou/dynamic-datasource-spring-boot-starter

2. Example construction

2.1 Data source division

This example is a simple example of commodity purchase, involving three business tables, so it is divided into three data sources.

2.1.1 Order Form

DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
  `commodity_code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
  `count` int NULL DEFAULT 0,
  `money` decimal(10, 2) NULL DEFAULT 0.00,
  `business_key` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 23 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;

2.1.2 Inventory table

-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
  `count` int NULL DEFAULT 0,
  `business_key` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `commodity_code`(`commodity_code` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES (1, 'iphone', 5, '');

2.1.3 Account Table

-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '用户ID',
  `money` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '账户余额',
  `business_key` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '业务标识',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES (1, 'user1', 300.00, '');

2.1.4 Data snapshot undo-log table

SeataWhen using ATthe transaction mode, each business data source needs to be created

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

2.2 Overall project structure

insert image description here

2.3 Complete project dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>seata</artifactId>
        <groupId>com.gm</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>multiple-datasource</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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>

        <!-- 注意一定要引入对版本,要引入spring-cloud版本seata,而不是springboot版本的seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <!-- 排除掉springcloud默认的seata版本,以免版本不一致出现问题-->
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 上面排除掉了springcloud默认色seata版本,此处引入和seata-server版本对应的seata包-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!--<dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- 数据源切换 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.6.1</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.4 Complete configuration file

src/main/resources/bootstrap.yml

server:
  port: 3000

spring:
  application:
    name: @artifactId@
  cloud:
    nacos:
      username: @nacos.username@
      password: @nacos.password@
      discovery:
        server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848},${
    
    NACOS_HOST:nacos2kc}:${
    
    NACOS_PORT:8848},${
    
    NACOS_HOST:nacos3.kc}:${
    
    NACOS_PORT:8848}
  datasource:
    dynamic:
      primary: order # 设置默认的数据源或者数据源组,默认值即为master
      strict: false # 严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      seata: true # seata1.0之后支持自动代理 这里直接配置true,seata.enable-auto-data-source-proxy=false
      seata-mode: AT # seata模式使用的at
      datasource:
        order:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.0.35:3306/seata-at-demo?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
          username: root
          password: '1qaz@WSX'
        account:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.0.35:3306/seata-at-demo2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
          username: root
          password: '1qaz@WSX'
        storage:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.0.35:3306/seata-at-demo3?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
          username: root
          password: '1qaz@WSX'

seata:
  # 是否开启spring-boot自动装配,seata-spring-boot-starter 专有配置,默认true
  enabled: true
  # 是否开启数据源自动代理,seata-spring-boot-starter专有配置,默认会开启数据源自动代理,可通过该配置项关闭
  enable-auto-data-source-proxy: false
  # 配置自定义事务组名称,需与下方server.vgroupMapping配置一致,程序会通过用户配置的配置中心去寻找service.vgroupMapping
  tx-service-group: mygroup
  config: # 从nacos配置中心获取client端配置
    type: nacos
    nacos:
      server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848}
      group : DEFAULT_GROUP
      namespace: a4c150aa-fd09-4595-9afe-c87084b22105
      dataId: seataServer.properties
      username: @nacos.username@
      password: @nacos.username@
  registry: # 通过服务中心通过服务发现获取seata-server服务地址
    type: nacos
    nacos:
      # 注:客户端注册中心配置的serverAddr和namespace与Server端一致,clusterName与Server端cluster一致
      application: seata-server # 此处与seata-server的application一致,才能通过服务发现获取服务地址
      group : DEFAULT_GROUP
      server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848}
      userName: @nacos.username@
      password: @nacos.username@
      namespace: a4c150aa-fd09-4595-9afe-c87084b22105
  service:
    # 应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping.[事务分组配置项]
    vgroup-mapping:
      # 事务分组配置项[mygroup]对应的值为TC集群名[default],与Seata-Server中的seata.registry.nacos.cluster配置一致
      mygroup : default
    # 全局事务开关,默认false。false为开启,true为关闭
    disable-global-transaction: false
  client:
    rm:
      report-success-enable: true

management:
  endpoints:
    web:
      exposure:
        include: '*'

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Key analysis of the configuration file:

1. Configure three data sources to set the default data source, and through configuration spring.datasource.dynamic.seata=true, enable the support of dynamic-datasource-spring-boot-startercomponent pairs , and Seatasupport integrated transaction modes . The default is :ATXAAT

spring:
  datasource:
    dynamic:
      primary: order # 设置默认的数据源或者数据源组,默认值即为master
      strict: false # 严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      seata: true # seata1.0之后支持自动代理 这里直接配置true,seata.enable-auto-data-source-proxy=false
      seata-mode: AT # seata模式使用的at

Seata2. Turn off the default data source proxy through the following configuration :

seata:
  enable-auto-data-source-proxy: false

For more configuration introduction, please refer to: Seata AT mode production-level use example construction and precautions | Spring Cloud 53

2.5 Function Construction

2.5.1 Basic function class

2.5.1.1 Controller Unified Exception Handling

com/gm/seata/multiplee/datasource/handle/GlobalBizExceptionHandler.java

import com.gm.seata.multiplee.datasource.util.ErrorEnum;
import com.gm.seata.multiplee.datasource.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 */
@Slf4j
@Order(10000)
@RestControllerAdvice
public class GlobalBizExceptionHandler {
    
    

    /**
     * 全局异常.
     *
     * @param e the e
     * @return R
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public R handleGlobalException(Exception e) {
    
    
        log.error("全局异常信息 ex={}", e.getMessage(), e);
        R r = null;
        // 根据异常信息与已知异常进行匹配
        try {
    
    
            int code = Integer.parseInt(e.getLocalizedMessage());
            ErrorEnum errorEnum = ErrorEnum.getEnumByCode(code);
            if (errorEnum != null) {
    
    
                r = R.restResult(null, errorEnum.getCode(), errorEnum.getTitle());
            }
        } finally {
    
    
            if (r == null) {
    
    
                r = R.failed(e.getLocalizedMessage());
            }
        }
        return r;
    }
}

2.5.1.2 Known exception enumeration class

com/gm/seata/multiplee/datasource/util/ErrorEnum.java

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ErrorEnum {
    
    

    NO_SUCH_COMMODITY(3000, "无此商品"),
    STORAGE_LOW_PREPARE(3001, "库存不足,预扣库存失败"),
    STORAGE_LOW_COMMIT(3002, "库存不足,扣库存失败"),
    NO_SUCH_ACCOUNT(4000, "无此账户"),
    ACCOUNT_LOW_PREPARE(4001, "余额不足,预扣款失败"),
    ACCOUNT_LOW_COMMIT(4002, "余额不足,扣款失败"),
    UNKNOWN_EXCEPTION(9999, "远程方法调用异常");

    private final Integer code;
    private final String title;

    public static ErrorEnum getEnumByCode(int code) {
    
    
        for (ErrorEnum error : ErrorEnum.values()) {
    
    
            if (error.getCode().equals(code)) {
    
    
                return error;
            }
        }
        return null;
    }
}

2.5.1.3 Response information structure

com/gm/seata/multiplee/datasource/util/R.java

import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serializable;

/**
 * 响应信息主体
 *
 */
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    /**
     * 成功标记
     */
    private static final Integer SUCCESS = 0;

    /**
     * 失败标记
     */
    private static final Integer FAIL = 1;

    @Getter
    @Setter
    private int code;

    @Getter
    @Setter
    private String msg;

    @Getter
    @Setter
    private T data;

    public static <T> R<T> ok() {
    
    
        return restResult(null, SUCCESS, null);
    }

    public static <T> R<T> ok(T data) {
    
    
        return restResult(data, SUCCESS, null);
    }

    public static <T> R<T> ok(T data, String msg) {
    
    
        return restResult(data, SUCCESS, msg);
    }

    public static <T> R<T> failed() {
    
    
        return restResult(null, FAIL, null);
    }

    public static <T> R<T> failed(String msg) {
    
    
        return restResult(null, FAIL, msg);
    }

    public static <T> R<T> failed(T data) {
    
    
        return restResult(data, FAIL, null);
    }

    public static <T> R<T> failed(T data, String msg) {
    
    
        return restResult(data, FAIL, msg);
    }

    public static <T> R<T> restResult(T data, int code, String msg) {
    
    
        R<T> apiResult = new R<>();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        return apiResult;
    }
}

2.5.2 Startup class

com/gm/seata/multiplee/datasource/MultipleDatasourceApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class MultipleDatasourceApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(MultipleDatasourceApplication.class, args);
    }
}

2.5.3 Mapper class

2.5.3.1 Account Mapper class

com/gm/seata/multiplee/datasource/mapper/AccountMapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.multiplee.datasource.entity.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface AccountMapper extends BaseMapper<Account> {
    
    

    @Select("SELECT * FROM t_account WHERE user_id = #{userId} limit 1")
    Account getAccountByUserId(@Param("userId") String userId);
}

2.5.3.2 Inventory Mapper class

com/gm/seata/multiplee/datasource/mapper/StorageMapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.multiplee.datasource.entity.Storage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
    
    

    @Select("SELECT * FROM t_storage WHERE commodity_code = #{commodityCode} limit 1")
    Storage getStorageByCommodityCode(@Param("commodityCode") String commodityCode);
}

2.5.3.3 Order Mapper class

com/gm/seata/multiplee/datasource/mapper/OrderMapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.multiplee.datasource.entity.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    
    

}

2.5.4 Service class

2.5.4.1 Account Service Class

com/gm/seata/multiplee/datasource/service/AccountService.java

import java.math.BigDecimal;

public interface AccountService {
    
    

    /**
     * 扣除账户余额
     *
     * @param userId
     * @param money
     * @return
     */
    boolean deduct(String userId, BigDecimal money);
}

com/gm/seata/multiplee/datasource/service/impl/AccountServiceImpl.java

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gm.seata.multiplee.datasource.entity.Account;
import com.gm.seata.multiplee.datasource.mapper.AccountMapper;
import com.gm.seata.multiplee.datasource.service.AccountService;
import com.gm.seata.multiplee.datasource.util.ErrorEnum;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
    
    

    @Autowired
    AccountMapper accountMapper;

    // @DS注解切换数据源必须要在@Transaction之前执行
    @Override
    @DS("account")
    @Transactional(rollbackFor = Exception.class)
    public boolean deduct(String userId, BigDecimal money) {
    
    
        String xid = RootContext.getXID();
        log.info("全局事务 xid:{}", xid);

        Account account = accountMapper.getAccountByUserId(userId);
        if (account == null) {
    
    
            //throw new RuntimeException("账户不存在");
            throw new RuntimeException(String.valueOf(ErrorEnum.NO_SUCH_ACCOUNT.getCode()));
        }
        // 账户余额 与 本次消费金额进行 比较
        if (account.getMoney().compareTo(money) < 0) {
    
    
            //throw new RuntimeException("余额不足,预扣款失败");
            throw new RuntimeException(String.valueOf(ErrorEnum.ACCOUNT_LOW_PREPARE.getCode()));
        }
        account.setMoney(account.getMoney().subtract(money));

        QueryWrapper query = new QueryWrapper();
        query.eq("user_id", userId);
        int i = accountMapper.update(account, query);
        log.info("{} 账户余额扣除 {} 元", userId, money);

        return i == 1;
    }
}

technical details:

  • To start a transaction, you need to obtain a database connection, so using annotations @DSto switch data sources must be @Transactionexecuted before.
  • Annotations @DScan be applied to classes and methods. If classes and methods exist at the same time, @DSthe data source configured by the method will take effect at this time. If there is no o @DS, the default data source will be used.

2.5.4.2 Inventory Service class

com/gm/seata/multiplee/datasource/service/StorageService.java

public interface StorageService {
    
    

    /**
     * 扣除库存
     *
     * @param commodityCode
     * @param count
     * @return
     */
    boolean deduct(String commodityCode, Integer count);
}

com/gm/seata/multiplee/datasource/service/impl/StorageServiceImpl.java

import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gm.seata.multiplee.datasource.entity.Storage;
import com.gm.seata.multiplee.datasource.mapper.StorageMapper;
import com.gm.seata.multiplee.datasource.service.StorageService;
import com.gm.seata.multiplee.datasource.util.ErrorEnum;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
public class StorageServiceImpl implements StorageService {
    
    

    @Autowired
    StorageMapper storageMapper;

    // @DS注解切换数据源必须要在@Transaction之前执行
    @Override
    @DS("storage")
    @Transactional(rollbackFor = Exception.class)
    public boolean deduct(String commodityCode, Integer count) {
    
    
        String xid = RootContext.getXID();
        log.info("全局事务 xid:{}", xid);
        Storage storage = storageMapper.getStorageByCommodityCode(commodityCode);
        if (storage == null) {
    
    
            //throw new RuntimeException("商品不存在");
            throw new RuntimeException(String.valueOf(ErrorEnum.NO_SUCH_COMMODITY.getCode()));
        }
        if (storage.getCount() < count) {
    
    
            //throw new RuntimeException("库存不足,预扣库存失败");
            throw new RuntimeException(String.valueOf(ErrorEnum.STORAGE_LOW_PREPARE.getCode()));
        }
        storage.setCount(storage.getCount() - count);

        QueryWrapper query = new QueryWrapper();
        query.eq("commodity_code", commodityCode);
        Integer i = storageMapper.update(storage, query);
        log.info("{} 商品库存扣除 {} 个", commodityCode, count);
        return i == 1;
    }
}

2.5.4.3 Order Service class

com/gm/seata/multiplee/datasource/service/OrderService.java

public interface OrderService {
    
    

    /**
     * 创建订单
     *
     * @param userId
     * @param commodityCode
     * @param count
     * @return
     */
    boolean createOrder(String userId, String commodityCode, Integer count);
}

com/gm/seata/multiplee/datasource/service/impl/OrderServiceImpl.java

import com.gm.seata.multiplee.datasource.entity.Order;
import com.gm.seata.multiplee.datasource.mapper.OrderMapper;
import com.gm.seata.multiplee.datasource.service.AccountService;
import com.gm.seata.multiplee.datasource.service.OrderService;
import com.gm.seata.multiplee.datasource.service.StorageService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
    
    

    @Autowired
    OrderMapper orderMapper;

    @Autowired
    StorageService storageService;

    @Autowired
    AccountService accountService;

    @GlobalTransactional
    @Override
    public boolean createOrder(String userId, String commodityCode, Integer count) {
    
    

        String xid = RootContext.getXID();
        log.info("全局事务 xid:{}", xid);
        try {
    
    
            storageService.deduct(commodityCode, count);
        } catch (Exception e) {
    
    
            throw new RuntimeException(e.getMessage());
        }
        try {
    
    
           accountService.deduct(userId, new BigDecimal(count * 100.0));
        } catch (Exception e) {
    
    
            throw new RuntimeException(e.getMessage());
        }

        Order order = new Order();
        order.setCount(count);
        order.setCommodityCode(commodityCode);
        order.setUserId(userId);
        order.setMoney(new BigDecimal(count * 100.0));
        int i = orderMapper.insert(order);

        return i == 1;
    }
}

Precautions:

  • Add annotations to the calling method of TM (Transaction Manager)the role事务管理器@GlobalTransactional

2.5.5 Controller class

com/gm/seata/multiplee/datasource/controller/OrderController.java

mport com.gm.seata.multiplee.datasource.service.OrderService;
import com.gm.seata.multiplee.datasource.util.ErrorEnum;
import com.gm.seata.multiplee.datasource.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class OrderController {
    
    

    @Autowired
    OrderService orderService;

    /**
     * 商品下单购买
     *
     * @param userId
     * @param commodityCode
     * @param count
     * @return
     */
    @RequestMapping(value = "buy", method = RequestMethod.GET)
    public R<String> buy(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count) {
    
    
        try {
    
    
            orderService.createOrder(userId, commodityCode, count);
            return R.ok("下单成功", "");
        } catch (Exception e) {
    
    
            e.printStackTrace();
            int code = Integer.parseInt(e.getMessage());
            return R.restResult("下单失败", code, ErrorEnum.getEnumByCode(code).getTitle());
        }
    }
}

3. Example test

Request address: http://127.0.0.1:3000/buy?userId=user1&count=2&commodityCode=iphone

For each request, 200 yuan will be deducted from the balance and 2 inventory will be deducted

3.1 The global transaction is submitted successfully

  • system log

insert image description here

  • order form

insert image description here

  • inventory list

insert image description here

  • account statement

insert image description here

3.2 Global transaction rollback succeeds

  • system log

insert image description here

  • order form

insert image description here

  • inventory list

insert image description here

After the rollback of the global transaction, the two stocks that have been deducted are restored

  • account statement

insert image description here

Guess you like

Origin blog.csdn.net/ctwy291314/article/details/131474905