Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
1.seata
seata实现分布式事务的处理过程为:一ID+三组件模型,一个ID即Transaction ID XID,全局唯一的事务ID。
三组件:
处理过程如下图:

2.seata-server安装
以windows版本seata为例。
1)修改registry.conf配置文档。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.2.31:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "192.168.2.31:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
2)启动seata-server,只需运行%SEATA_HOME%\bin\seata-server.bat 即可。
3.实例
总共有四个微服务模块,将四个微服务模块都注册到nacos server 上面。
seata-buy: 用户购买商品,所以要和seata-order,seata-stock和seata-user三个微服务交互。 seata-order: 用户购买商品的时候,将订单保存到orderform数据库 seata-stock:用户购买商品的时候,stock数据库里指定ID的商品数量减去用户购买的数量 seata-user: 用户购买商品的时候,user数据库里指定用户ID的用户减去购买所花的钱(默认所有的商品单价都是1元)。
项目结构图如下:

组件版本关系:nacos2.1.0+seata1.3.0+mysql8.0.26
建表
创建名字为:seatademo的数据库。导入以下数据。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for orderform
-- ----------------------------
DROP TABLE IF EXISTS `orderform`;
CREATE TABLE `orderform` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NULL DEFAULT NULL,
`product_id` int NULL DEFAULT NULL,
`number` int NULL DEFAULT 0,
`money` int NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of orderform
-- ----------------------------
-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
`id` int NOT NULL AUTO_INCREMENT,
`number` int NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of stock
-- ----------------------------
INSERT INTO `stock` VALUES (1, 5);
INSERT INTO `stock` VALUES (2, 10);
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of undo_log
-- ----------------------------
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`money` int NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 10);
INSERT INTO `user` VALUES (2, 6);
SET FOREIGN_KEY_CHECKS = 1;
parent模块
父项目pom.xml文件规定依赖版本 我使用的数据库是mysql 8,其他组件版本下面都有,下面是一个父module,用于管理各组件版本,便于管理下面的子module(各微服务模块的组件版本统一比较好)
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.simoniu</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>seata-buy</module>
<module>seata-order</module>
<module>seata-stock</module>
<module>seata-user</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring-cloud-alibaba-version>2.2.7.RELEASE</spring-cloud-alibaba-version>
<spring-cloud-version>Hoxton.SR12</spring-cloud-version>
<mysql-connector-java-version>8.0.26</mysql-connector-java-version>
<lombok-version>1.18.22</lombok-version>
<mybatis-plus-boot-starter-version>3.4.2</mybatis-plus-boot-starter-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
seata-buy子项目
交易请求的接口,与下面三个微服务项目交互。

pom.xml
<?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>parent</artifactId>
<groupId>com.simoniu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-buy</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!--排除-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 9000
spring:
application:
name: seata-buy
cloud:
nacos:
# 将这个服务注册到nacos 上面去
discovery:
server-addr: your nacos ip:8848
alibaba:
seata:
tx-service-group: my_test_tx_group # 配置事务分组
service:
vgroup-mapping:
my_test_tx_group: default
seata:
registry:
#配置seata 的注册中心,告诉seata client 怎么去访问seata server(TC,里面运行着这个事务协调器)
type: nacos
nacos:
server-addr: your nacos ip:8848 # seata-server 所在的nacos服务地址
application: seata-server # 服务名
username: nacos
password: nacos
group: SEATA_GROUP # seata-server 所在的分组
config:
type: nacos
nacos:
server-addr: your nacos ip:8848
BuyController
package com.simoniu.controller;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@RestController
public class BuyController {
@Autowired
private final RestTemplate restTemplate;
public BuyController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@PostMapping("/buy")
public String buy(@RequestParam("userId") Integer userId,
@RequestParam("productId") Integer productId,
@RequestParam("number") Integer count) {
// 请求参数
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("userId", userId.toString());
queryParams.add("productId", productId.toString());
queryParams.add("number", count.toString());
queryParams.add("money", count.toString());
// 构造请求
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://seata-order/orderform/create").queryParams(queryParams);
restTemplate.postForObject(builder.toUriString(), null, Void.class);
// 构造请求
builder = UriComponentsBuilder.fromHttpUrl("http://seata-stock/stock/deduct").queryParams(queryParams);
restTemplate.postForObject(builder.toUriString(), null, Void.class);
// 构造请求 restTemplate 加上了@LoadBalanced注解以后,必须使用应用名来访问指定服务,而不是IP地址
builder = UriComponentsBuilder.fromHttpUrl("http://seata-user/user/debit").queryParams(queryParams);
restTemplate.postForObject(builder.toUriString(), null, Void.class);
return "success";
}
}
SeataBuyApplication
package com.simoniu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient
public class SeataBuyApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(SeataBuyApplication.class);
}
}
seata-order子项目
交易进行时,生成用户购买指定商品的订单,然后入库,结构图:

pom.xml
<?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>parent</artifactId>
<groupId>com.simoniu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<!--排除-->
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 9001
spring:
application:
name: seata-order
cloud:
nacos:
# 将这个服务注册到nacos 上面去
discovery:
server-addr: your nacos ip:8848
alibaba:
seata:
tx-service-group: my_test_tx_group # 配置事务分组
service:
vgroup-mapping:
my_test_tx_group: default
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/seatademo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
hikari:
max-lifetime: 30000
# jpa:
# show-sql: true
# 配置日志输出 使用默认控制台打印
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
seata:
registry:
#配置seata 的注册中心,告诉seata client 怎么去访问seata server(TC,里面运行着这个事务协调器)
type: nacos
nacos:
server-addr: your nacos ip:8848 # seata-server 所在的nacos服务地址
application: seata-server # 服务名
username: nacos
password: nacos
group: SEATA_GROUP # seata-server 所在的分组
config:
type: nacos
nacos:
server-addr: your nacos ip:8848
OrderForm
package com.simoniu.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@TableName("orderform")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderForm implements Serializable {
// 订单id
@TableId(type = IdType.AUTO)
private Integer id;
// 用户id
private Integer userId;
// 商品id
private Integer productId;
// 商品购买数量
private Integer number;
// 订单金额
private Integer money;
}
OrderFormMapper
package com.simoniu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.simoniu.entity.OrderForm;
import org.springframework.stereotype.Repository;
@Repository
public interface OrderFormMapper extends BaseMapper<OrderForm> {
}
OrderFormService和OrderFormServiceImpl
package com.simoniu.service;
public interface OrderService {
void create(int userId, int productId, int number, int money);
}
package com.simoniu.service.impl;
import com.simoniu.entity.OrderForm;
import com.simoniu.mapper.OrderFormMapper;
import com.simoniu.service.OrderService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderFormMapper orderFormMapper;
@Override
public void create(int userId, int productId, int number, int money) {
// 生成订单
OrderForm order = new OrderForm();
order.setUserId(userId);
order.setProductId(productId);
order.setNumber(number);
order.setMoney(money);
System.out.println(order.toString());
orderFormMapper.insert(order);
}
}
OrderFormController
package com.simoniu.controller;
import com.simoniu.service.OrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/orderform")
public class OrderFormController {
@Resource
private OrderService orderService;
@PostMapping("/create")
public void create(@RequestParam("userId") Integer userId,
@RequestParam("productId") Integer productId,
@RequestParam("number") Integer count,
@RequestParam("money") Integer money) {
orderService.create(userId, productId, count, money);
}
}
SeataOrderFormApplication
package com.simoniu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.simoniu.mapper")
public class SeataOrderFormApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderFormApplication.class);
}
}
seata-stock子项目 交易进行时,扣除指定商品的库存数量,项目结构图:

pom.xml 和上一个子项目的依赖都一样,我就不贴了
application.yml 也和上一个差不多,改一下服务端口号和应用名称就行。
server:
port: 9002
spring:
application:
name: seata-stoc
Stock
package com.simoniu.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@TableName("stock")
@Data
public class Stock implements Serializable {
// 商品id
private Integer id;
// 库存
private Integer number;
}
StockMapper
package com.simoniu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.simoniu.entity.Stock;
import org.springframework.stereotype.Repository;
@Repository
public interface StockMapper extends BaseMapper<Stock> {
}
StockService和StockServiceImpl
package com.simoniu.service;
public interface StockService {
void deduct(int productId, int number);
}
package com.simoniu.service.impl;
import com.simoniu.entity.Stock;
import com.simoniu.mapper.StockMapper;
import com.simoniu.service.StockService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
@Service
public class StockServiceImpl implements StockService {
@Resource
private StockMapper stockMapper;
@Override
public void deduct(int productId, int number) {
Optional<Stock> byId = Optional.ofNullable(stockMapper.selectById(productId));
if(byId.isPresent()) {
Stock storage = byId.get();
if(storage.getNumber() >= number) {
// 减库存
storage.setNumber(storage.getNumber() - number);
//stockMapper.insert(storage);
stockMapper.updateById(storage);
}
else {
throw new RuntimeException("该商品库存不足!");
}
}
else {
throw new RuntimeException("该商品不存在!");
}
}
}
StockController
package com.simoniu.controller;
import com.simoniu.service.StockService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/stock")
public class StockController {
@Resource
private StockService storageService;
@PostMapping("/deduct")
public void deduct(@RequestParam("productId") Integer productId,
@RequestParam("number") Integer count) {
storageService.deduct(productId, count);
}
}
SeataStockApplication
package com.simoniu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.simoniu.mapper")
public class SeataStockApplication {
public static void main(String[] args) {
SpringApplication.run(SeataStockApplication.class);
}
}
seata-user子项目 交易进行时,扣除指定用户账户里面的钱。项目结构图:

pom.xml文件和上一子项目依赖都一样
application.yml 和上一子项目也差不多,就改一下服务端口号和应用名。
server:
port: 9003
spring:
application:
name: seata-user
User
package com.simoniu.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@TableName("user")
@Data
public class User implements Serializable {
// 用户id
private Integer id;
// 用户余额
private Integer money;
}
UserMapper
package com.simoniu.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.simoniu.entity.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
UserService和UserServiceImpl
package com.simoniu.service;
public interface UserService {
void debit(int userId, int money);
}
package com.simoniu.service.impl;
import com.simoniu.entity.User;
import com.simoniu.mapper.UserMapper;
import com.simoniu.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public void debit(int userId, int money) {
Optional<User> byId = Optional.ofNullable(userMapper.selectById(userId));
if(byId.isPresent()) {
User user = byId.get();
if(user.getMoney() >= money) {
// 减余额
user.setMoney(user.getMoney() - money);
//userMapper.insert(user);
userMapper.updateById(user);
}
else {
throw new RuntimeException("该用户余额不足!");
}
}
else {
throw new RuntimeException("没有该用户!");
}
}
}
UserController
package com.simoniu.controller;
import com.simoniu.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/debit")
public void debit(@RequestParam("userId") Integer userId,
@RequestParam("money") Integer money) {
userService.debit(userId, money);
}
}
SeataUserApplication
package com.simoniu;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.simoniu.mapper")
public class SeataUserApplication {
public static void main(String[] args) {
SpringApplication.run(SeataUserApplication.class);
}
}
在nacos服务器添加配置:
选择:配置管理->配置列表->新建。

测试(不使用分布式事务)
依次启动seata-buy->seata-order->seata-stock->seata-user四个项目。在nacos server观察四个微服务已经注册成功。如下图:

使用postman测试2号用户购买6件1号商品。测试接口如下:

我们发现seata-stock微服务抛出异常,而但是seata-order微服务却正常运行。因为用户购买的数量超过了库存数量,因此seata-stock微服务肯定会抛出异常,但是由于没有分布式事务,操作数据不一致。出现以下情况。

测试(使用分布式事务)
在BuyController的buy方法上添加@GlobalTransactional。以@GlobalTransactional为入口,GlobalTransactionalInterceptor为切入点,TM会向TC发起一个请求(服务端使用的netty)开启一个全局事务,生成全局事务的XID,通过服务调用链路传播,测试分布式事务。
再次测试,使用postman测试2号用户购买6件1号商品。测试接口如下:

我们发现seata-stock微服务因库存不足抛出异常,而eata-order微服务本来执行了sql 语句插入订单数据,后来还是回滚了,因为seata-stock微服务未执行成功,抛出了异常。
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@d09ebb9] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@55286307] will not be managed by Spring
==> Preparing: INSERT INTO orderform ( user_id, product_id, number, money ) VALUES ( ?, ?, ?, ? )
==> Parameters: 2(Integer), 1(Integer), 6(Integer), 6(Integer)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@d09ebb9]
2022-05-16 20:52:31.584 INFO 9028 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=203.6.234.202:8091:269569373063090176,branchId=269569376791826433,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seatademo,applicationData=null
由于使用了分布式事务,不会破坏数据的一致性。回滚后数据如下:
