一、前言
通过以下系列章节:
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
及其AT
、XA
、TCC
、Saga
事务模式的理论学习和多维度对比总结,并通过业务示例搭建对其使用也有了深入的了解。在这篇文章中,我们使用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 表
在使用
Seata
的AT
事务模式时,各业务数据源均需创建
-- 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 项目总体结构
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: 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
配置文件重点解析:
1.配置三个数据源设置默认数据源,并通过配置spring.datasource.dynamic.seata=true
,开启dynamic-datasource-spring-boot-starter
组件对Seata
的支持,支持整合的事务模式有AT
和XA
,默认为AT
:
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
2.通过以下配置关闭Seata
默认的数据源代理:
seata:
enable-auto-data-source-proxy: false
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
@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 启动类
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 {
@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;
}
}
技术细节:
- 开启事务,是需要获取一个数据库连接的,那么使用注解
@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 {
@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 订单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 {
@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;
}
}
注意事项:
- 在
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 {
@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());
}
}
}
三、示例测试
请求地址:http://127.0.0.1:3000/buy?userId=user1&count=2&commodityCode=iphone
每请求一次,扣除余额200元,扣除库存2个
3.1 全局事务提交成功
- 系统日志
- 订单表
- 库存表
- 账户表
3.2 全局事务回滚成功
- 系统日志
- 订单表
- 库存表
在全局事务发生回滚后,已扣除的两个库存被还原
- 账户表