Seata 整合多数据源dynamic-datasource-spring-boot-starter | Spring Cloud 60

news/2024/11/17 3:25:36/

一、前言

通过以下系列章节:

docker-compose 实现Seata Server高可用部署 | Spring Cloud 51

Seata AT 模式理论学习、事务隔离及部分源码解析 | Spring Cloud 52

Spring Boot集成Seata利用AT模式分布式事务示例 | Spring Cloud 53

Seata XA 模式理论学习、使用及注意事项 | Spring Cloud54

Seata TCC 模式理论学习、生产级使用示例搭建及注意事项 | Spring Cloud55

Seata TCC 模式下解决幂等、悬挂、空回滚问题 | Spring Cloud56

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(一) | Spring Cloud57

Seata Saga 模式理论学习、生产级使用示例搭建及注意事项(二) | Spring Cloud58

Seata 四种模式对比总结 | Spring Cloud 59

我们完成了对Seata及其ATXATCCSaga事务模式的理论学习和多维度对比总结,并通过业务示例搭建对其使用也有了深入的了解。在这篇文章中,我们使用Seata整合一下多数据源的场景。多数据源切换的功能我们使用dynamic-datasource-spring-boot-starter来完成,并且这个组件还可以和Seata进行整合,实现数据源的代理。

dynamic-datasource-spring-boot-starter官网地址:https://github.com/baomidou/dynamic-datasource-spring-boot-starter

二、示例搭建

2.1 数据源划分

本示例为一个商品购买的简单示例,共涉及三张业务表,因此分为三个数据源。

2.1.1 订单表

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 库存表

-- ----------------------------
-- 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 账户表

-- ----------------------------
-- 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 数据快照 undo-log 表

在使用SeataAT事务模式时,各业务数据源均需创建

-- 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 = InnoDBAUTO_INCREMENT = 1DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

2.2 项目总体结构

在这里插入图片描述

2.3 完整项目依赖

<?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 完整配置文件

src/main/resources/bootstrap.yml

server:port: 3000spring: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 # 设置默认的数据源或者数据源组,默认值即为masterstrict: false # 严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源seata: true # seata1.0之后支持自动代理 这里直接配置true,seata.enable-auto-data-source-proxy=falseseata-mode: AT # seata模式使用的atdatasource:order:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.0.35:3306/seata-at-demo?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: '1qaz@WSX'account:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.0.35:3306/seata-at-demo2?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: '1qaz@WSX'storage:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.0.35:3306/seata-at-demo3?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghaiusername: rootpassword: '1qaz@WSX'seata:# 是否开启spring-boot自动装配,seata-spring-boot-starter 专有配置,默认trueenabled: true# 是否开启数据源自动代理,seata-spring-boot-starter专有配置,默认会开启数据源自动代理,可通过该配置项关闭enable-auto-data-source-proxy: false# 配置自定义事务组名称,需与下方server.vgroupMapping配置一致,程序会通过用户配置的配置中心去寻找service.vgroupMappingtx-service-group: mygroupconfig: # 从nacos配置中心获取client端配置type: nacosnacos:server-addr: ${NACOS_HOST:nacos1.kc}:${NACOS_PORT:8848}group : DEFAULT_GROUPnamespace: a4c150aa-fd09-4595-9afe-c87084b22105dataId: seataServer.propertiesusername: @nacos.username@password: @nacos.username@registry: # 通过服务中心通过服务发现获取seata-server服务地址type: nacosnacos:# 注:客户端注册中心配置的serverAddr和namespace与Server端一致,clusterName与Server端cluster一致application: seata-server # 此处与seata-server的application一致,才能通过服务发现获取服务地址group : DEFAULT_GROUPserver-addr: ${NACOS_HOST:nacos1.kc}:${NACOS_PORT:8848}userName: @nacos.username@password: @nacos.username@namespace: a4c150aa-fd09-4595-9afe-c87084b22105service:# 应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping.[事务分组配置项]vgroup-mapping:# 事务分组配置项[mygroup]对应的值为TC集群名[default],与Seata-Server中的seata.registry.nacos.cluster配置一致mygroup : default# 全局事务开关,默认false。false为开启,true为关闭disable-global-transaction: falseclient:rm:report-success-enable: truemanagement:endpoints:web:exposure:include: '*'mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

配置文件重点解析:

1.配置三个数据源设置默认数据源,并通过配置spring.datasource.dynamic.seata=true,开启dynamic-datasource-spring-boot-starter组件对Seata的支持,支持整合的事务模式有ATXA,默认为AT

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

2.通过以下配置关闭Seata默认的数据源代理:

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

更多配置介绍请参考:Seata AT模式生产级使用示例搭建及注意事项 | Spring Cloud 53

2.5 功能搭建

2.5.1 基础功能类

2.5.1.1 Controller 统一异常处理

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 已知异常枚举类

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 响应信息结构体

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@Setterprivate int code;@Getter@Setterprivate String msg;@Getter@Setterprivate 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 启动类

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类

2.5.3.1 账户Mapper类

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 库存Mapper类

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 订单Mapper类

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类

2.5.4.1 账户Service类

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 {@AutowiredAccountMapper 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;}
}

技术细节:

  • 开启事务,是需要获取一个数据库连接的,那么使用注解 @DS切换数据源必须要在 @Transaction 之前执行。
  • 注解@DS可以作用在类和方法上,如类和方法上同时存在,此时方法@DS配置的数据源生效,若o都没有@DS,则使用默认数据源。

2.5.4.2 库存Service类

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 {@AutowiredStorageMapper 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 订单Service类

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 {@AutowiredOrderMapper orderMapper;@AutowiredStorageService storageService;@AutowiredAccountService accountService;@GlobalTransactional@Overridepublic 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;}
}

注意事项:

  • TM (Transaction Manager) - 事务管理器 角色 的调用方法上添加@GlobalTransactional注解

2.5.5 Controller类

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 {@AutowiredOrderService 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());}}
}

三、示例测试

请求地址:http://127.0.0.1:3000/buy?userId=user1&count=2&commodityCode=iphone

每请求一次,扣除余额200元,扣除库存2个

3.1 全局事务提交成功

  • 系统日志

在这里插入图片描述

  • 订单表

在这里插入图片描述

  • 库存表

在这里插入图片描述

  • 账户表

在这里插入图片描述

3.2 全局事务回滚成功

  • 系统日志

在这里插入图片描述

  • 订单表

在这里插入图片描述

  • 库存表

在这里插入图片描述

在全局事务发生回滚后,已扣除的两个库存被还原

  • 账户表

在这里插入图片描述


http://www.ppmy.cn/news/668389.html

相关文章

i9-11900K跑分曝光

CPU-Z检测结果显示器为8核心16线程&#xff0c;二级缓存4MB&#xff0c;三级缓存16MB&#xff0c;最高加速频率能达到5.3GHz。依旧还是14nm工艺&#xff0c;热设计功耗应该还是125W。单核跑分为708.7&#xff0c;多核跑分为6443.9&#xff0c;是目前已知11代酷睿里的最高水平。…

龙芯3A5000LL与i7-10700的UnixBench跑分对比

龙芯 架构&#xff1a; loongarch64 字节序&#xff1a; Little Endian CPU: 4 在线 CPU 列表&#xff1a; 0-3 每个核的线程数&#xff1a; 1 每个座的核数&#xff1a; 4 座&#xff1a; 1 NUMA 节点&#xff1a; 1 CPU …

beautifulSoup爬取网络数据

beautifulSoup可以方便的爬取网络数据&#xff0c;下面代码为获取历史天气数据&#xff1a; import requests from bs4 import BeautifulSoup import pandas as pd#获取天气数据 urlhttp://lishi.tianqi.com/chengdu/202209.html headersheaders {user-agent: Mozilla/5.0 (M…

微软128核服务器,王思聪的百万元电脑:64核心128线程/跑分世界第四

王思聪装电脑了&#xff0c;哦不&#xff0c;是服务器。 近日&#xff0c;电丸科技受邀&#xff0c;到上海王思聪家&#xff0c;和校长一起搭建服务器&#xff0c;架设网络。整系列视频将有3到4期&#xff0c;第一期已经上线&#xff0c;主要是介绍校长价值百万的服务器以及如何…

王思聪花100万组装电脑!跑分97000,亚洲排名第一,全世界第四。

????????关注后回复 “进群” &#xff0c;拉你进程序员交流群???????? 来源丨人工智能那点事 8月4日&#xff0c;博主电丸科技AK 发布最新视频&#xff0c;记录其受邀帮助王思聪组装电脑的经历。 据视频介绍&#xff0c;王思聪组装的这台电脑预计花费100万元&…

蔚来真题:Redis跳跃表是如何添加元素的?

今天分享的这道题来自于蔚来的真实面试题。 Java 面试不可能不问 Redis&#xff0c;问到 Redis 不可能不问 Redis 的常用数据类型&#xff0c;问到 Redis 的常用数据类型&#xff0c;不可能不问跳跃表&#xff0c;当问到跳跃表经常会被问到跳跃表的查询和添加流程&#xff0c;…

学习node.js模块机制

一、CommonJS的模块规范 Node与浏览器以及 W3C组织、CommonJS组织、ECMAScript之间的关系 Node借鉴CommonJS的Modules规范实现了一套模块系统&#xff0c;所以先来看看CommonJS的模块规范。 CommonJS对模块的定义十分简单&#xff0c;主要分为模块引用、模块定义和模块标识3…

30强争夺战即将开启 巅峰对决一触即发

在飞速发展的技术和激烈的市场竞争中&#xff0c;企业必须不断的进步才有机会在市场中续存下去。为了帮助国内的电子产业链条当中的相关企业&#xff0c;在面对经济全球化及日新月异的用户需求中能够不断的做大做强&#xff0c;慧聪电子网、慧聪芯城联合举办了第三届HCFT智能硬…