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
Solve the problems of idempotence, suspension and empty rollback in Seata TCC mode | Spring Cloud56
Comparison summary of Seata's four modes | Spring Cloud 59
We have completed the theoretical study and multi-dimensional comparison and summary of Seata
its AT
, XA
, TCC
, and transaction modes, and gained a deep understanding of their use through business examples. Saga
In 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-starter
to complete, and this component can also Seata
be integrated with to realize the proxy of data sources.
dynamic-datasource-spring-boot-starter
Official 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
Seata
When usingAT
the 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
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-starter
component pairs , and Seata
support integrated transaction modes . The default is :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
Seata
2. 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
@DS
to switch data sources must be@Transaction
executed before.- Annotations
@DS
can be applied to classes and methods. If classes and methods exist at the same time,@DS
the 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
- order form
- inventory list
- account statement
3.2 Global transaction rollback succeeds
- system log
- order form
- inventory list
After the rollback of the global transaction, the two stocks that have been deducted are restored
- account statement