一、前言
上篇文章介紹用戶購買商品的多數(shù)據(jù)源分布式事務 Spring Boot 單體應用,本文拆成分多個 Spring Boot 應用,通過 Apache HttpClient 來實現(xiàn) HTTP 遠程調(diào)用每個 Spring Boot 應用提供的 Restful API 接口。整體如下圖所示:

早期的微服務架構,會采用 Nginx 對后端的服務進行負載均衡,而服務提供者使用 HttpClient 進行遠程 HTTP 調(diào)用。如下調(diào)用鏈路:

Seata 提供了 seata-http 項目,對 Apache HttpClient 進行集成。實現(xiàn)原理是:
- 服務消費者,使用 Seata 封裝的 AbstractHttpExecutor 執(zhí)行器,在使用HttpClient 發(fā)起 HTTP 調(diào)用時,將 Seata 全局事務 XID 通過 Header 傳遞。
- 服務提供者,使用 Seata 提供的 SpringMVC TransactionPropagationIntercepter 攔截器,將 Header 中的 Seata 全局事務 XID 解析出來,設置到 Seata 上下文 中。
如此,我們便實現(xiàn)了多個 Spring Boot 應用的 Seata 全局事務的傳播。
本文的源代碼可從Gitee下載.
二、創(chuàng)建Module
該項目包含三個 Spring Boot模塊。

三、初始化數(shù)據(jù)庫
使用 data.sql腳本,創(chuàng)建 seata_order、seata_storage、seata_amount 三個庫。腳本內(nèi)容如下:
# 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); # 插入一條產(chǎn)品的庫存
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 模式必須創(chuàng)建的表,主要用于分支事務的回滾。
另外,考慮到測試方便,我們插入了一條 id = 1 的 account 記錄,和一條 id = 1 的 product 記錄。
四、 訂單服務
作為訂單服務。它主要提供 /order/create 接口,實現(xiàn)下單邏輯。
4.1 引入依賴
創(chuàng)建 [pom.xml] 文件,引入相關的依賴。內(nèi)容如下:
<?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>
<!-- 實現(xiàn)對 Spring MVC 的自動化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 實現(xiàn)對數(shù)據(jù)庫連接池的自動化配置 -->
<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>
<!-- 實現(xiàn)對 MyBatis 的自動化配置 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- 實現(xiàn)對 Seata 的自動化配置 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>
<!-- 實現(xiàn) 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 依賴,實現(xiàn)對 Seata 的自動配置。
② 引入 seata-http 依賴,實現(xiàn) Seata 對 HttpClient 的集成支持。
4.2 配置文件
創(chuàng)建 [application.yaml]配置文件,添加相關的配置項。內(nèi)容如下:
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
創(chuàng)建 [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 中,會調(diào)用 OrderService 進行下單。
4.4 OrderService
創(chuàng)建 [OrderService]接口,定義了創(chuàng)建訂單的方法。代碼如下:
/**
* @ClassName: OrderService
* @Description: 訂單 Service
* @author: 郭秀志 jbcode@126.com
* @date: 2020/6/19 16:00
* @Copyright:
*/
public interface OrderService {
/**
* 創(chuàng)建訂單
*
* @param userId 用戶編號
* @param productId 產(chǎn)品編號
* @param price 價格
* @return 訂單編號
* @throws Exception 創(chuàng)建訂單失敗,拋出異常
*/
Integer createOrder(Long userId, Long productId, Integer price) throws Exception;
}
4.5 OrderServiceImpl
創(chuàng)建 [OrderServiceImpl] 類,實現(xiàn)創(chuàng)建訂單的方法,全局事務的核心入口在這里,使用了@GlobalTransactional注解,其他子服務(扣減庫存、扣減余額)使用的都是本地事務注解@Transactional // 開啟事物。代碼如下:
/**
* @ClassName: OrderServiceImpl
* @Description: 核心入口方法通過httpclient調(diào)用其他服務。
* @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; // 購買數(shù)量,暫時設置為 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 {
// 參數(shù)拼接
JSONObject params = new JSONObject().fluentPut("productId", String.valueOf(productId))
.fluentPut("amount", String.valueOf(amount));
// 執(zhí)行調(diào)用
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 {
// 參數(shù)拼接
JSONObject params = new JSONObject().fluentPut("userId", String.valueOf(userId))
.fluentPut("price", String.valueOf(price));
// 執(zhí)行調(diào)用
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> 處,調(diào)用 #reduceStock(productId, amount) 方法,通過 Apache HttpClient 遠程 HTTP 調(diào)用商品服務,進行扣除庫存。
其中,DefaultHttpExecutor 是 Seata 封裝,在使用個 HttpClient 發(fā)起 HTTP 調(diào)用時,將 Seata 全局事務 XID 通過 Header 傳遞。不過有兩點要注意:
- 在使用 POST 請求時,DefaultHttpExecutor 暫時只支持
application/json請求參數(shù)格式。所以,如果想要application/x-www-form-urlencoded等格式,需要自己重新封裝~ - 針對返回結果的轉(zhuǎn)換,DefaultHttpExecutor 暫時沒有實現(xiàn)完成,代碼如下圖所示:實現(xiàn)代碼
另外,商品服務提供的 /product/reduce-stock 接口,通過返回 true 或 false 來表示扣除庫存是否成功。因此,我們在 false扣除失敗時,拋出 RuntimeException 異常,從而實現(xiàn)全局事務的回滾。
<3> 處,調(diào)用 #reduceBalance(userId, price) 方法,通過 Apache HttpClient 遠程 HTTP 調(diào)用賬戶服務,進行扣除余額。整體邏輯和 <2> 一致。
<4> 處,在全部調(diào)用成功后,調(diào)用 OrderDao 保存訂單。
4.6 OrderDao
創(chuàng)建 [OrderDao]接口,定義保存訂單的操作。代碼如下:
@Mapper
@Repository
public interface OrderDao {
/**
* 插入訂單記錄
*
* @param order 訂單
* @return 影響記錄數(shù)量
*/
@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;
/** 產(chǎn)品編號 **/
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模塊的結構。
五、測試
下面,我們將測試兩種情況:
- 分布式事務正常提交
- 分布式事務異?;貪L
5.1 步驟
- 啟動Nacos、Seata。
- Debug 執(zhí)行 OrderServiceApplication 啟動訂單服務。
- ProductServiceApplication 啟動商品服務。
- 執(zhí)行 AccountServiceApplication 啟動賬戶服務。
5.2 正常提交下單請求
使用 Postman 模擬調(diào)用 http://127.0.0.1:8081/order/create 創(chuàng)建訂單的接口,如下圖所示:

此時,在控制臺打印日志如下圖所示:
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(產(chǎn)品服務)控制臺信息:
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 異?;貪L下單請求
在 OrderServiceImpl 的 createOrder(...)方法打上斷點如下圖,方便我們看到 product 表的 stock 被減少:

現(xiàn)在的product 表的 stock是8 個。
使用 Postman 模擬調(diào)用 http://127.0.0.1:8081/order/create 創(chuàng)建訂單的接口,如下圖所示:

扣減了庫存,并回滾
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
庫存沒有減少,但是更新時間變化了

