Spring Cloud Alibaba(6.3 Seata——纯 Spring Boot AT 模式 + HttpClient 远程调用)

一、前言

上篇文章介绍用户购买商品的多数据源分布式事务 Spring Boot 单体应用,本文拆成分多个 Spring Boot 应用,通过 Apache HttpClient 来实现 HTTP 远程调用每个 Spring Boot 应用提供的 Restful API 接口。整体如下图所示:

整体架构图

早期的微服务架构,会采用 Nginx 对后端的服务进行负载均衡,而服务提供者使用 HttpClient 进行远程 HTTP 调用。如下调用链路:

Nginx + Spring Boot

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_orderseata_storageseata_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 = 1account 记录,和一条 id = 1product 记录。

四、 订单服务

作为订单服务。它主要提供 /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 文档 —— 事务分组专题》。如果不能理解,可以见如下图:

image

简单来说,就是多了一层虚拟映射。这里,我们直接设置 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 接口,通过返回 truefalse 来表示扣除库存是否成功。因此,我们在 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模块的结构。

五、测试

下面,我们将测试两种情况:

  1. 分布式事务正常提交
  2. 分布式事务异常回滚

5.1 步骤

  1. 启动Nacos、Seata。
  2. Debug 执行 OrderServiceApplication 启动订单服务。
  3. ProductServiceApplication 启动商品服务。
  4. 执行 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 被减少:

image

现在的product 表的 stock8 个。
使用 Postman 模拟调用 http://127.0.0.1:8081/order/create 创建订单的接口,如下图所示:

image

扣减了库存,并回滚

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

库存没有减少,但是更新时间变化了

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355