一、前言
上篇文章介绍用户购买商品的多数据源分布式事务 Spring Boot 单体应用,本文拆成分多个 Spring Boot 应用,通过 Apache HttpClient 来实现 HTTP 远程调用每个 Spring Boot 应用提供的 Restful API 接口。整体如下图所示:
早期的微服务架构,会采用 Nginx 对后端的服务进行负载均衡,而服务提供者使用 HttpClient 进行远程 HTTP 调用。如下调用链路:
Seata 提供了 seata-http
项目,对 Apache HttpClient 进行集成。实现原理是:
- 服务消费者,使用 Seata 封装的 AbstractHttpExecutor 执行器,在使用HttpClient 发起 HTTP 调用时,将 Seata 全局事务 XID 通过 Header 传递。
- 服务提供者,使用 Seata 提供的 SpringMVC TransactionPropagationIntercepter 拦截器,将 Header 中的 Seata 全局事务 XID 解析出来,设置到 Seata 上下文 中。
如此,我们便实现了多个 Spring Boot 应用的 Seata 全局事务的传播。
本文的源代码可从Gitee下载.
二、创建Module
该项目包含三个 Spring Boot模块。
三、初始化数据库
使用 data.sql
脚本,创建 seata_order
、seata_storage
、seata_amount
三个库。脚本内容如下:
# Order
DROP DATABASE IF EXISTS seata_order;
CREATE DATABASE seata_order;
CREATE TABLE seata_order.orders
(
id INT(11) NOT NULL AUTO_INCREMENT,
user_id INT(11) DEFAULT NULL,
product_id INT(11) DEFAULT NULL,
pay_amount DECIMAL(10, 0) DEFAULT NULL,
add_time DATETIME DEFAULT CURRENT_TIMESTAMP,
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
CREATE TABLE seata_order.undo_log
(
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
# Storage
DROP DATABASE IF EXISTS seata_storage;
CREATE DATABASE seata_storage;
CREATE TABLE seata_storage.product
(
id INT(11) NOT NULL AUTO_INCREMENT,
stock INT(11) DEFAULT NULL,
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
INSERT INTO seata_storage.product (id, stock) VALUES (1, 10); # 插入一条产品的库存
CREATE TABLE seata_storage.undo_log
(
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
# Amount
DROP DATABASE IF EXISTS seata_amount;
CREATE DATABASE seata_amount;
CREATE TABLE seata_amount.account
(
id INT(11) NOT NULL AUTO_INCREMENT,
balance DOUBLE DEFAULT NULL,
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
CREATE TABLE seata_amount.undo_log
(
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
INSERT INTO seata_amount.account (id, balance) VALUES (1, 1);
其中,每个库中的 undo_log
表,是 Seata AT 模式必须创建的表,主要用于分支事务的回滚。
另外,考虑到测试方便,我们插入了一条 id = 1
的 account
记录,和一条 id = 1
的 product
记录。
四、 订单服务
作为订单服务。它主要提供 /order/create 接口,实现下单逻辑。
4.1 引入依赖
创建 [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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>lab-52-seata-at-httpclient-demo-account-service</artifactId>
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对数据库连接池的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <!-- 本示例,我们使用 MySQL -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<!-- 实现对 MyBatis 的自动化配置 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- 实现对 Seata 的自动化配置 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
<!-- 实现 Seata 对 HttpClient 的集成支持 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-http</artifactId>
<version>1.1.0</version>
</dependency>
<!-- Apache HttpClient 依赖 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.8</version>
</dependency>
</dependencies>
</project>
① 引入 seata-spring-boot-starter
依赖,实现对 Seata 的自动配置。
② 引入 seata-http
依赖,实现 Seata 对 HttpClient 的集成支持。
4.2 配置文件
创建 [application.yaml
]配置文件,添加相关的配置项。内容如下:
server:
port: 8081 # 端口
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://127.0.0.1:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
username: root
password:
# Seata 配置项,对应 SeataProperties 类
seata:
application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
# 服务配置项,对应 ServiceProperties 类
service:
# 虚拟组和分组的映射
vgroup-mapping:
order-service-group: default
# 分组和 Seata 服务的映射
grouplist:
default: 127.0.0.1:8091
① spring.datasource
配置项,设置连接 seata_order
库。
② seata
配置项,设置 Seata 的配置项目,对应 SeataProperties 类。
-
application-id
配置项,对应 Seata 应用编号,默认为${spring.application.name}
。实际上,可以不进行设置。 -
tx-service-group
配置项,Seata 事务组编号,用于 TC 集群名。
③ seata.service
配置项,Seata 服务配置项,对应 ServiceProperties 类。它主要用于 Seata 在事务分组的特殊设计,可见《Seata 文档 —— 事务分组专题》。如果不能理解,可以见如下图:
简单来说,就是多了一层虚拟映射。这里,我们直接设置 TC Server 的地址,为 127.0.0.1:8091
。
4.3 OrderController
创建 [OrderController]类,提供 order/create
下单 HTTP API。代码如下:
/**
* @ClassName: OrderController
* @Description: 下单操作
* @author: 郭秀志 jbcode@126.com
* @date: 2020/6/19 15:59
* @Copyright:
*/
@RestController
@RequestMapping("/order")
public class OrderController {
private Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowired
private OrderService orderService;
@PostMapping("/create")
public Integer createOrder(@RequestParam("userId") Long userId,
@RequestParam("productId") Long productId,
@RequestParam("price") Integer price) throws Exception {
logger.info("[createOrder] 收到下单请求,用户:{}, 商品:{}, 价格:{}", userId, productId, price);
return orderService.createOrder(userId, productId, price);
}
}
- 该 API 中,会调用 OrderService 进行下单。
4.4 OrderService
创建 [OrderService]接口,定义了创建订单的方法。代码如下:
/**
* @ClassName: OrderService
* @Description: 订单 Service
* @author: 郭秀志 jbcode@126.com
* @date: 2020/6/19 16:00
* @Copyright:
*/
public interface OrderService {
/**
* 创建订单
*
* @param userId 用户编号
* @param productId 产品编号
* @param price 价格
* @return 订单编号
* @throws Exception 创建订单失败,抛出异常
*/
Integer createOrder(Long userId, Long productId, Integer price) throws Exception;
}
4.5 OrderServiceImpl
创建 [OrderServiceImpl] 类,实现创建订单的方法,全局事务的核心入口在这里,使用了@GlobalTransactional
注解,其他子服务(扣减库存、扣减余额)使用的都是本地事务注解@Transactional // 开启事物
。代码如下:
/**
* @ClassName: OrderServiceImpl
* @Description: 核心入口方法通过httpclient调用其他服务。
* @author: 郭秀志 jbcode@126.com
* @date: 2020/6/19 16:01
* @Copyright:
*/
@Service
public class OrderServiceImpl implements OrderService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private OrderDao orderDao;
@Override
@GlobalTransactional
public Integer createOrder(Long userId, Long productId, Integer price) throws Exception {
Integer amount = 1; // 购买数量,暂时设置为 1。
logger.info("[createOrder] 当前 XID: {}", RootContext.getXID());
// 扣减库存
this.reduceStock(productId, amount);
// 扣减余额
this.reduceBalance(userId, price);
// 保存订单
OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);
orderDao.saveOrder(order);
logger.info("[createOrder] 保存订单: {}", order.getId());
// 返回订单编号
return order.getId();
}
private void reduceStock(Long productId, Integer amount) throws IOException {
// 参数拼接
JSONObject params = new JSONObject().fluentPut("productId", String.valueOf(productId))
.fluentPut("amount", String.valueOf(amount));
// 执行调用
HttpResponse response = DefaultHttpExecutor.getInstance().executePost("http://127.0.0.1:8082", "/product/reduce-stock",
params, HttpResponse.class);
// 解析结果
Boolean success = Boolean.valueOf(EntityUtils.toString(response.getEntity()));
if (!success) {
throw new RuntimeException("扣除库存失败");
}
}
private void reduceBalance(Long userId, Integer price) throws IOException {
// 参数拼接
JSONObject params = new JSONObject().fluentPut("userId", String.valueOf(userId))
.fluentPut("price", String.valueOf(price));
// 执行调用
HttpResponse response = DefaultHttpExecutor.getInstance().executePost("http://127.0.0.1:8083", "/account/reduce-balance",
params, HttpResponse.class);
// 解析结果
Boolean success = Boolean.valueOf(EntityUtils.toString(response.getEntity()));
if (!success) {
throw new RuntimeException("扣除余额失败");
}
}
}
<1>
处,在类上,添加 Seata @GlobalTransactional
注解,声明全局事务。
<2>
处,调用 #reduceStock(productId, amount)
方法,通过 Apache HttpClient 远程 HTTP 调用商品服务,进行扣除库存。
其中,DefaultHttpExecutor 是 Seata 封装,在使用个 HttpClient 发起 HTTP 调用时,将 Seata 全局事务 XID 通过 Header 传递。不过有两点要注意:
- 在使用 POST 请求时,DefaultHttpExecutor 暂时只支持
application/json
请求参数格式。所以,如果想要application/x-www-form-urlencoded
等格式,需要自己重新封装~ - 针对返回结果的转换,DefaultHttpExecutor 暂时没有实现完成,代码如下图所示:实现代码
另外,商品服务提供的 /product/reduce-stock
接口,通过返回 true
或 false
来表示扣除库存是否成功。因此,我们在 false
扣除失败时,抛出 RuntimeException 异常,从而实现全局事务的回滚。
<3>
处,调用 #reduceBalance(userId, price)
方法,通过 Apache HttpClient 远程 HTTP 调用账户服务,进行扣除余额。整体逻辑和 <2>
一致。
<4>
处,在全部调用成功后,调用 OrderDao 保存订单。
4.6 OrderDao
创建 [OrderDao]接口,定义保存订单的操作。代码如下:
@Mapper
@Repository
public interface OrderDao {
/**
* 插入订单记录
*
* @param order 订单
* @return 影响记录数量
*/
@Insert("INSERT INTO orders (user_id, product_id, pay_amount) VALUES (#{userId}, #{productId}, #{payAmount})")
@Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
int saveOrder(OrderDO order);
}
其中,[OrderDO]实体类,对应 orders
表。代码如下:
package cn.iocoder.springboot.lab53.orderservice.entity;
/**
* 订单实体
*/
public class OrderDO {
/** 订单编号 **/
private Integer id;
/** 用户编号 **/
private Long userId;
/** 产品编号 **/
private Long productId;
/** 支付金额 **/
private Integer payAmount;
public Integer getId() {
return id;
}
public OrderDO setId(Integer id) {
this.id = id;
return this;
}
public Long getUserId() {
return userId;
}
public OrderDO setUserId(Long userId) {
this.userId = userId;
return this;
}
public Long getProductId() {
return productId;
}
public OrderDO setProductId(Long productId) {
this.productId = productId;
return this;
}
public Integer getPayAmount() {
return payAmount;
}
public OrderDO setPayAmount(Integer payAmount) {
this.payAmount = payAmount;
return this;
}
}
其他2个服务的代码略,可参考Order模块的结构。
五、测试
下面,我们将测试两种情况:
- 分布式事务正常提交
- 分布式事务异常回滚
5.1 步骤
- 启动Nacos、Seata。
- Debug 执行 OrderServiceApplication 启动订单服务。
- ProductServiceApplication 启动商品服务。
- 执行 AccountServiceApplication 启动账户服务。
5.2 正常提交下单请求
使用 Postman 模拟调用 http://127.0.0.1:8081/order/create 创建订单的接口,如下图所示:
此时,在控制台打印日志如下图所示:
2020-06-19 15:46:34.052 INFO 10628 --- [nio-8081-exec-3] c.i.s.l.o.controller.OrderController : [createOrder] 收到下单请求,用户:1, 商品:1, 价格:1
2020-06-19 15:46:34.137 INFO 10628 --- [nio-8081-exec-3] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.1.104:8091:2014744463]
2020-06-19 15:46:37.957 INFO 10628 --- [nio-8081-exec-3] c.i.s.l.o.service.OrderServiceImpl : [createOrder] 当前 XID: 192.168.1.104:8091:2014744463
2020-06-19 15:46:47.681 INFO 10628 --- [nio-8081-exec-3] c.i.s.l.o.service.OrderServiceImpl : [createOrder] 保存订单: 4
2020-06-19 15:47:00.055 INFO 10628 --- [lector_TMROLE_1] i.s.c.r.netty.AbstractRpcRemotingClient : channel [id: 0x8ddec010, L:/127.0.0.1:50867 - R:/127.0.0.1:8091] read idle.
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0x8ddec010, L:/127.0.0.1:50867 - R:/127.0.0.1:8091]
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x8ddec010, L:/127.0.0.1:50867 - R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.c.r.netty.NettyClientChannelManager : return to pool, rm channel:[id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : channel valid false,channel:[id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.c.r.netty.AbstractRpcRemotingClient : channel inactive: [id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : channel valid false,channel:[id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.056 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x8ddec010, L:/127.0.0.1:50867 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.c.r.netty.AbstractRpcRemotingClient : channel [id: 0x0feaf046, L:/127.0.0.1:50865 - R:/127.0.0.1:8091] read idle.
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0x0feaf046, L:/127.0.0.1:50865 - R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x0feaf046, L:/127.0.0.1:50865 - R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : channel valid false,channel:[id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.c.r.netty.AbstractRpcRemotingClient : channel [id: 0xe544158f, L:/127.0.0.1:50866 - R:/127.0.0.1:8091] read idle.
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0xe544158f, L:/127.0.0.1:50866 - R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe544158f, L:/127.0.0.1:50866 - R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : channel valid false,channel:[id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.c.r.netty.AbstractRpcRemotingClient : channel inactive: [id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : channel valid false,channel:[id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.057 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0x0feaf046, L:/127.0.0.1:50865 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.058 INFO 10628 --- [lector_TMROLE_1] i.s.c.r.netty.AbstractRpcRemotingClient : channel inactive: [id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.058 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : channel valid false,channel:[id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.058 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.058 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.058 INFO 10628 --- [lector_TMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe544158f, L:/127.0.0.1:50866 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.232 INFO 10628 --- [lector_RMROLE_1] i.s.c.r.netty.AbstractRpcRemotingClient : channel inactive: [id: 0xe226f5fd, L:/127.0.0.1:50859 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.233 INFO 10628 --- [lector_RMROLE_1] i.s.c.r.netty.NettyClientChannelManager : return to pool, rm channel:[id: 0xe226f5fd, L:/127.0.0.1:50859 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.233 INFO 10628 --- [lector_RMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : channel valid false,channel:[id: 0xe226f5fd, L:/127.0.0.1:50859 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.233 INFO 10628 --- [lector_RMROLE_1] i.s.core.rpc.netty.NettyPoolableFactory : will destroy channel:[id: 0xe226f5fd, L:/127.0.0.1:50859 ! R:/127.0.0.1:8091]
2020-06-19 15:47:00.233 INFO 10628 --- [lector_RMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe226f5fd, L:/127.0.0.1:50859 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:00.234 INFO 10628 --- [lector_RMROLE_1] i.s.core.rpc.netty.AbstractRpcRemoting : ChannelHandlerContext(AbstractRpcRemotingClient$ClientHandler#0, [id: 0xe226f5fd, L:/127.0.0.1:50859 ! R:/127.0.0.1:8091]) will closed
2020-06-19 15:47:03.890 INFO 10628 --- [imeoutChecker_1] i.s.c.r.netty.NettyClientChannelManager : will connect to 127.0.0.1:8091
2020-06-19 15:47:03.890 INFO 10628 --- [imeoutChecker_1] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:TMROLE,address:127.0.0.1:8091,msg:< RegisterTMRequest{applicationId='order-service', transactionServiceGroup='order-service-group'} >
2020-06-19 15:47:03.898 INFO 10628 --- [imeoutChecker_1] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 5 ms, version:1.2.0,role:TMROLE,channel:[id: 0xaac9d379, L:/127.0.0.1:50892 - R:/127.0.0.1:8091]
2020-06-19 15:47:03.902 INFO 10628 --- [imeoutChecker_2] i.s.c.r.netty.NettyClientChannelManager : will connect to 127.0.0.1:8091
2020-06-19 15:47:03.902 INFO 10628 --- [imeoutChecker_2] io.seata.core.rpc.netty.RmRpcClient : RM will register :jdbc:mysql://101.133.227.13:3306/seata_order
2020-06-19 15:47:03.903 INFO 10628 --- [imeoutChecker_2] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:127.0.0.1:8091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://101.133.227.13:3306/seata_order', applicationId='order-service', transactionServiceGroup='order-service-group'} >
2020-06-19 15:47:03.913 INFO 10628 --- [imeoutChecker_2] io.seata.core.rpc.netty.RmRpcClient : register RM success. server version:1.2.0,channel:[id: 0x592a6d40, L:/127.0.0.1:50893 - R:/127.0.0.1:8091]
2020-06-19 15:47:03.914 INFO 10628 --- [imeoutChecker_2] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 8 ms, version:1.2.0,role:RMROLE,channel:[id: 0x592a6d40, L:/127.0.0.1:50893 - R:/127.0.0.1:8091]
2020-06-19 15:47:30.056 ERROR 10628 --- [nio-8081-exec-3] i.s.core.rpc.netty.AbstractRpcRemoting : wait response error:cost 30001 ms,ip:127.0.0.1:8091,request:xid=192.168.1.104:8091:2014744463,extraData=null
2020-06-19 15:47:30.057 ERROR 10628 --- [nio-8081-exec-3] i.seata.tm.api.DefaultGlobalTransaction : Failed to report global commit [192.168.1.104:8091:2014744463],Retry Countdown: 5, reason: RPC timeout
2020-06-19 15:47:30.454 INFO 10628 --- [nio-8081-exec-3] i.seata.tm.api.DefaultGlobalTransaction : [192.168.1.104:8091:2014744463] commit status: Committed
2020-06-19 15:47:31.667 INFO 10628 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.1.104:8091:2014744463,branchId=2014744480,branchType=AT,resourceId=jdbc:mysql://101.133.227.13:3306/seata_order,applicationData=null
2020-06-19 15:47:31.668 INFO 10628 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.1.104:8091:2014744463 2014744480 jdbc:mysql://101.133.227.13:3306/seata_order null
2020-06-19 15:47:31.668 INFO 10628 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
ProductService(产品服务)控制台信息:
2020-06-19 15:44:55.501 INFO 1756 --- [nio-8082-exec-3] c.i.s.l.p.controller.ProductController : [reduceStock] 收到减少库存请求, 商品:1, 价格:1
2020-06-19 15:44:55.614 INFO 1756 --- [nio-8082-exec-3] c.i.s.l.p.service.ProductServiceImpl : [reduceStock] 当前 XID: 192.168.1.104:8091:2014744375
2020-06-19 15:44:55.615 INFO 1756 --- [nio-8082-exec-3] c.i.s.l.p.service.ProductServiceImpl : [checkStock] 检查 1 库存
2020-06-19 15:44:55.684 INFO 1756 --- [nio-8082-exec-3] c.i.s.l.p.service.ProductServiceImpl : [reduceStock] 开始扣减 1 库存
2020-06-19 15:44:55.939 INFO 1756 --- [nio-8082-exec-3] c.i.s.l.p.service.ProductServiceImpl : [reduceStock] 扣除 1 库存成功
2020-06-19 15:45:55.713 INFO 1756 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.1.104:8091:2014744375,branchId=2014744387,branchType=AT,resourceId=jdbc:mysql://101.133.227.13:3306/seata_product,applicationData=null
2020-06-19 15:45:55.714 INFO 1756 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.1.104:8091:2014744375 2014744387 jdbc:mysql://101.133.227.13:3306/seata_product
2020-06-19 15:45:56.104 INFO 1756 --- [tch_RMROLE_1_16] i.s.r.d.undo.AbstractUndoLogManager : xid 192.168.1.104:8091:2014744375 branch 2014744387, undo_log deleted with GlobalFinished
2020-06-19 15:45:56.160 INFO 1756 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-06-19 15:46:38.479 INFO 1756 --- [nio-8082-exec-5] c.i.s.l.p.controller.ProductController : [reduceStock] 收到减少库存请求, 商品:1, 价格:1
2020-06-19 15:46:38.577 INFO 1756 --- [nio-8082-exec-5] c.i.s.l.p.service.ProductServiceImpl : [reduceStock] 当前 XID: 192.168.1.104:8091:2014744463
2020-06-19 15:46:38.577 INFO 1756 --- [nio-8082-exec-5] c.i.s.l.p.service.ProductServiceImpl : [checkStock] 检查 1 库存
2020-06-19 15:46:38.630 INFO 1756 --- [nio-8082-exec-5] c.i.s.l.p.service.ProductServiceImpl : [reduceStock] 开始扣减 1 库存
2020-06-19 15:46:38.868 INFO 1756 --- [nio-8082-exec-5] c.i.s.l.p.service.ProductServiceImpl : [reduceStock] 扣除 1 库存成功
2020-06-19 15:47:31.422 INFO 1756 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.1.104:8091:2014744463,branchId=2014744470,branchType=AT,resourceId=jdbc:mysql://101.133.227.13:3306/seata_product,applicationData=null
2020-06-19 15:47:31.425 INFO 1756 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.1.104:8091:2014744463 2014744470 jdbc:mysql://101.133.227.13:3306/seata_product null
2020-06-19 15:47:31.427 INFO 1756 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
5.4 异常回滚下单请求
在 OrderServiceImpl 的 createOrder(...)
方法打上断点如下图,方便我们看到 product 表的 stock 被减少:
现在的product
表的 stock
是8 个。
使用 Postman 模拟调用 http://127.0.0.1:8081/order/create 创建订单的接口,如下图所示:
扣减了库存,并回滚
2020-06-19 09:45:43.334 INFO 11664 --- [nio-8081-exec-1] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.1.104:8091:2014720090]
2020-06-19 09:45:43.338 INFO 11664 --- [nio-8081-exec-1] c.i.s.l.s.service.impl.OrderServiceImpl : [createOrder] 当前 XID: 192.168.1.104:8091:2014720090
2020-06-19 09:45:43.406 INFO 11664 --- [nio-8081-exec-1] c.i.s.l.s.s.impl.ProductServiceImpl : [reduceStock] 当前 XID: 192.168.1.104:8091:2014720090
2020-06-19 09:45:43.406 INFO 11664 --- [nio-8081-exec-1] c.i.s.l.s.s.impl.ProductServiceImpl : [checkStock] 检查 1 库存
2020-06-19 09:45:43.420 INFO 11664 --- [nio-8081-exec-1] i.s.common.loader.EnhancedServiceLoader : load SQLRecognizerFactory[druid] extension by class[io.seata.sqlparser.druid.DruidDelegatingSQLRecognizerFactory]
2020-06-19 09:45:43.450 INFO 11664 --- [nio-8081-exec-1] i.s.common.loader.EnhancedServiceLoader : load SQLOperateRecognizerHolder[mysql] extension by class[io.seata.sqlparser.druid.mysql.MySQLOperateRecognizerHolder]
2020-06-19 09:45:43.642 INFO 11664 --- [nio-8081-exec-1] c.i.s.l.s.s.impl.ProductServiceImpl : [reduceStock] 开始扣减 1 库存
2020-06-19 09:45:43.689 INFO 11664 --- [nio-8081-exec-1] i.s.common.loader.EnhancedServiceLoader : load KeywordChecker[mysql] extension by class[io.seata.rm.datasource.undo.mysql.keyword.MySQLKeywordChecker]
2020-06-19 09:45:43.690 INFO 11664 --- [nio-8081-exec-1] i.s.common.loader.EnhancedServiceLoader : load TableMetaCache[mysql] extension by class[io.seata.rm.datasource.sql.struct.cache.MysqlTableMetaCache]
2020-06-19 09:45:44.661 INFO 11664 --- [nio-8081-exec-1] c.i.s.l.s.s.impl.ProductServiceImpl : [reduceStock] 扣除 1 库存成功
2020-06-19 09:45:46.182 INFO 11664 --- [nio-8081-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [192.168.1.104:8091:2014720090] rollback status: Rollbacked
库存没有减少,但是更新时间变化了