Dubbo 分布式事务 Seata 入门

1. 概述

 

《Seata 极简入门》文章中,我们对 Seata 进行了简单的了解,并完成了 Seata 的部署。而本文,我们将 Dubbo 服务接入 Seata 来实现分布式事务。

Seata 是阿里开源的一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

注意,考虑到 Nacos 作为注册中心在国内越来越流行,本文将采用 Nacos 作为 Dubbo 的注册中心。

友情提示:如果对 Nacos 不了解的胖友,可以参考《Nacos 极简入门》文章。

2. AT 模式

示例代码对应仓库:

本小节,我们将使用 Seata 的 AT 模式,解决多个 Dubbo 服务下的分布式事务的问题。

友情提示:对 Seata 的 AT 模式不了解的胖友,可以阅读《Seata 文档 —— AT 模式》文档。

Seata 提供了 seata-dubbo 项目,对 Dubbo 进行集成。实现原理是:

如此,我们便实现了多个 Dubbo 应用的 Seata 全局事务的传播

我们以用户购买商品的业务逻辑,来作为具体示例,一共会有三个 Dubbo 服务,分别对应自己的数据库。整体如下图所示:整体图

下面,我们来新建 lab-53-seata-at-dubbo-demo 模块,包含三个 Dubbo 服务。最终结构如下图:

 é¡¹ç®ç»æ

2.1 初始化数据库

使用 data.sql 脚本,创建 seata_orderseata_storageseata_amount 三个库。脚本内容如下:

  1.  
    # Order
  2.  
    DROP DATABASE IF EXISTS seata_order;
  3.  
    CREATE DATABASE seata_order;
  4.  
     
  5.  
    CREATE TABLE seata_order.orders
  6.  
    (
  7.  
    id INT(11) NOT NULL AUTO_INCREMENT,
  8.  
    user_id INT(11) DEFAULT NULL,
  9.  
    product_id INT(11) DEFAULT NULL,
  10.  
    pay_amount DECIMAL(10, 0) DEFAULT NULL,
  11.  
    add_time DATETIME DEFAULT CURRENT_TIMESTAMP,
  12.  
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  13.  
    PRIMARY KEY (id)
  14.  
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
  15.  
     
  16.  
    CREATE TABLE seata_order.undo_log
  17.  
    (
  18.  
    id BIGINT(20) NOT NULL AUTO_INCREMENT,
  19.  
    branch_id BIGINT(20) NOT NULL,
  20.  
    xid VARCHAR(100) NOT NULL,
  21.  
    context VARCHAR(128) NOT NULL,
  22.  
    rollback_info LONGBLOB NOT NULL,
  23.  
    log_status INT(11) NOT NULL,
  24.  
    log_created DATETIME NOT NULL,
  25.  
    log_modified DATETIME NOT NULL,
  26.  
    PRIMARY KEY (id),
  27.  
    UNIQUE KEY ux_undo_log (xid, branch_id)
  28.  
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
  29.  
     
  30.  
    # Storage
  31.  
    DROP DATABASE IF EXISTS seata_storage;
  32.  
    CREATE DATABASE seata_storage;
  33.  
     
  34.  
    CREATE TABLE seata_storage.product
  35.  
    (
  36.  
    id INT(11) NOT NULL AUTO_INCREMENT,
  37.  
    stock INT(11) DEFAULT NULL,
  38.  
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  39.  
    PRIMARY KEY (id)
  40.  
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
  41.  
    INSERT INTO seata_storage.product (id, stock) VALUES (1, 10); # 插入一条产品的库存
  42.  
     
  43.  
    CREATE TABLE seata_storage.undo_log
  44.  
    (
  45.  
    id BIGINT(20) NOT NULL AUTO_INCREMENT,
  46.  
    branch_id BIGINT(20) NOT NULL,
  47.  
    xid VARCHAR(100) NOT NULL,
  48.  
    context VARCHAR(128) NOT NULL,
  49.  
    rollback_info LONGBLOB NOT NULL,
  50.  
    log_status INT(11) NOT NULL,
  51.  
    log_created DATETIME NOT NULL,
  52.  
    log_modified DATETIME NOT NULL,
  53.  
    PRIMARY KEY (id),
  54.  
    UNIQUE KEY ux_undo_log (xid, branch_id)
  55.  
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
  56.  
     
  57.  
    # Amount
  58.  
    DROP DATABASE IF EXISTS seata_amount;
  59.  
    CREATE DATABASE seata_amount;
  60.  
     
  61.  
    CREATE TABLE seata_amount.account
  62.  
    (
  63.  
    id INT(11) NOT NULL AUTO_INCREMENT,
  64.  
    balance DOUBLE DEFAULT NULL,
  65.  
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  66.  
    PRIMARY KEY (id)
  67.  
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
  68.  
     
  69.  
    CREATE TABLE seata_amount.undo_log
  70.  
    (
  71.  
    id BIGINT(20) NOT NULL AUTO_INCREMENT,
  72.  
    branch_id BIGINT(20) NOT NULL,
  73.  
    xid VARCHAR(100) NOT NULL,
  74.  
    context VARCHAR(128) NOT NULL,
  75.  
    rollback_info LONGBLOB NOT NULL,
  76.  
    log_status INT(11) NOT NULL,
  77.  
    log_created DATETIME NOT NULL,
  78.  
    log_modified DATETIME NOT NULL,
  79.  
    PRIMARY KEY (id),
  80.  
    UNIQUE KEY ux_undo_log (xid, branch_id)
  81.  
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
  82.  
    INSERT INTO seata_amount.account (id, balance) VALUES (1, 1);

其中,每个库中的 undo_log 表,是 Seata AT 模式必须创建的表,主要用于分支事务的回滚。

另外,考虑到测试方便,我们插入了一条 id = 1 的 account 记录,和一条 id = 1 的 product 记录。

2.2 订单服务

新建 lab-53-seata-at-dubbo-demo-order-service-api 和 lab-53-seata-at-dubbo-demo-order-service 项目,作为订单服务。项目如下图所示:项目结构

2.2.1 引入依赖

创建 pom.xml 文件,引入相关的依赖。内容如下:

  1.  
    <?xml version="1.0" encoding="UTF-8"?>
  2.  
    <project xmlns="http://maven.apache.org/POM/4.0.0"
  3.  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.  
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5.  
    <parent>
  6.  
    <groupId>org.springframework.boot</groupId>
  7.  
    <artifactId>spring-boot-starter-parent</artifactId>
  8.  
    <version>2.2.2.RELEASE</version>
  9.  
    <relativePath/> <!-- lookup parent from repository -->
  10.  
    </parent>
  11.  
    <modelVersion>4.0.0</modelVersion>
  12.  
     
  13.  
    <artifactId>lab-53-seata-at-dubbo-demo-order-service</artifactId>
  14.  
     
  15.  
    <dependencies>
  16.  
    <dependency>
  17.  
    <groupId>cn.iocoder.springboot.labs</groupId>
  18.  
    <artifactId>lab-53-seata-at-dubbo-demo-order-service-api</artifactId>
  19.  
    <version>1.0-SNAPSHOT</version>
  20.  
    </dependency>
  21.  
    <dependency>
  22.  
    <groupId>cn.iocoder.springboot.labs</groupId>
  23.  
    <artifactId>lab-53-seata-at-dubbo-demo-account-service-api</artifactId>
  24.  
    <version>1.0-SNAPSHOT</version>
  25.  
    </dependency>
  26.  
    <dependency>
  27.  
    <groupId>cn.iocoder.springboot.labs</groupId>
  28.  
    <artifactId>lab-53-seata-at-dubbo-demo-product-service-api</artifactId>
  29.  
    <version>1.0-SNAPSHOT</version>
  30.  
    </dependency>
  31.  
     
  32.  
    <!-- 实现对 Spring MVC 的自动化配置 -->
  33.  
    <dependency>
  34.  
    <groupId>org.springframework.boot</groupId>
  35.  
    <artifactId>spring-boot-starter-web</artifactId>
  36.  
    </dependency>
  37.  
     
  38.  
    <!-- 实现对数据库连接池的自动化配置 -->
  39.  
    <dependency>
  40.  
    <groupId>org.springframework.boot</groupId>
  41.  
    <artifactId>spring-boot-starter-jdbc</artifactId>
  42.  
    </dependency>
  43.  
    <dependency> <!-- 本示例,我们使用 MySQL -->
  44.  
    <groupId>mysql</groupId>
  45.  
    <artifactId>mysql-connector-java</artifactId>
  46.  
    <version>5.1.48</version>
  47.  
    </dependency>
  48.  
     
  49.  
    <!-- 实现对 MyBatis 的自动化配置 -->
  50.  
    <dependency>
  51.  
    <groupId>org.mybatis.spring.boot</groupId>
  52.  
    <artifactId>mybatis-spring-boot-starter</artifactId>
  53.  
    <version>2.1.2</version>
  54.  
    </dependency>
  55.  
     
  56.  
    <!-- 实现对 Seata 的自动化配置 -->
  57.  
    <dependency>
  58.  
    <groupId>io.seata</groupId>
  59.  
    <artifactId>seata-spring-boot-starter</artifactId>
  60.  
    <version>1.1.0</version>
  61.  
    </dependency>
  62.  
     
  63.  
    <!-- 引入 Dubbo 的依赖 -->
  64.  
    <dependency>
  65.  
    <groupId>org.apache.dubbo</groupId>
  66.  
    <artifactId>dubbo</artifactId>
  67.  
    <version>2.7.4.1</version>
  68.  
    </dependency>
  69.  
    <!-- 实现对 Dubbo 的自动化配置 -->
  70.  
    <dependency>
  71.  
    <groupId>org.apache.dubbo</groupId>
  72.  
    <artifactId>dubbo-spring-boot-starter</artifactId>
  73.  
    <version>2.7.4.1</version>
  74.  
    </dependency>
  75.  
    <!-- 实现 Dubbo 使用 Nacos 作为注册中心 -->
  76.  
    <dependency>
  77.  
    <groupId>com.alibaba</groupId>
  78.  
    <artifactId>dubbo-registry-nacos</artifactId>
  79.  
    <version>2.7.6</version>
  80.  
    </dependency>
  81.  
    </dependencies>
  82.  
     
  83.  
    </project>

① 引入 seata-spring-boot-starter 依赖,实现对 Seata 的自动配置。

② 引入 dubbodubbo-spring-boot-starterdubbo-registry-nacos 依赖,Dubbo 使用到的库。

友情提示:对 Dubbo 不是很了解的胖友,可以阅读《芋道 Spring Boot Dubbo 入门》文章。

2.2.2 配置文件

创建 application.yaml 配置文件,添加相关的配置项。内容如下:

  1.  
    server:
  2.  
    port: 8081 # 端口
  3.  
     
  4.  
    spring:
  5.  
    application:
  6.  
    name: order-service
  7.  
     
  8.  
    datasource:
  9.  
    url: jdbc:mysql://127.0.0.1:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=UTF-8
  10.  
    driver-class-name: com.mysql.jdbc.Driver
  11.  
    username: root
  12.  
    password:
  13.  
     
  14.  
    # dubbo 配置项,对应 DubboConfigurationProperties 配置类
  15.  
    dubbo:
  16.  
    # Dubbo 应用配置
  17.  
    application:
  18.  
    name: ${spring.application.name} # 应用名
  19.  
    # Dubbo 注册中心配
  20.  
    registry:
  21.  
    address: nacos://127.0.0.1:8848 # 注册中心地址。个鞥多注册中心,可见 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文档。
  22.  
    # Dubbo 服务提供者协议配置
  23.  
    protocol:
  24.  
    port: -1 # 协议端口。使用 -1 表示随机端口。
  25.  
    name: dubbo # 使用 `dubbo://` 协议。更多协议,可见 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文档
  26.  
    # 配置扫描 Dubbo 自定义的 @Service 注解,暴露成 Dubbo 服务提供者
  27.  
    scan:
  28.  
    base-packages: cn.iocoder.springboot.lab53.orderservice.service
  29.  
     
  30.  
    # Seata 配置项,对应 SeataProperties 类
  31.  
    seata:
  32.  
    application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
  33.  
    tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
  34.  
    # Seata 服务配置项,对应 ServiceProperties 类
  35.  
    service:
  36.  
    # 虚拟组和分组的映射
  37.  
    vgroup-mapping:
  38.  
    order-service-group: default
  39.  
    # Seata 注册中心配置项,对应 RegistryProperties 类
  40.  
    registry:
  41.  
    type: nacos # 注册中心类型,默认为 file
  42.  
    nacos:
  43.  
    cluster: default # 使用的 Seata 分组
  44.  
    namespace: # Nacos 命名空间
  45.  
    serverAddr: localhost # Nacos 服务地址

① spring.datasource 配置项,设置连接 seata_order 库。

② dubbo 配置项,设置 Dubbo 相关配置。

③ seata 配置项,设置 Seata 的配置项目,对应 SeataProperties 类。

  • application-id 配置项,对应 Seata 应用编号,默认为 ${spring.application.name}。实际上,可以不进行设置。
  • tx-service-group 配置项,Seata 事务组编号,用于 TC 集群名。

④ seata.service 配置项,设置 Seata 服务配置项,对应 ServiceProperties 类。它主要用于 Seata 在事务分组的特殊设计,可见《Seata 文档 —— 事务分组专题》。如果不能理解的胖友,可以见如下图:

 

简单来说,就是多了一层虚拟映射。这里,我们不设置 seata.service.grouplist 配置项,因为从注册中心加载 Seata TC Server 的地址。

友情提示:可能胖友对 seata.service.grouplist 配置项有点懵逼,继续往下看就会明白了。

④ seata.registry 配置项,设置 Seata 注册中心配置项,对应 RegistryProperties 类。

  • type 配置项,设置注册中心的类型,默认为 false。这里,我们设置为 nacos 来使用 Nacos 作为注册中心。
  • nacos 配置项,设置 Nacos 注册中心的配置项。

注意!!!我们需要将 Seata TC Server 注册到 Nacos 注册中心中。不会的胖友,《芋道 Seata 极简入门》文章的「3. 部署集群 TC Server」小节。


可能胖友不想从 Nacos 注册中心来读取 Seata TC Server 的地址,可以使用艿艿额外提供的 application-file.yaml 配置文件,覆盖到 application.yaml 配置文件中即可。内容如下:

  1.  
    server:
  2.  
    port: 8081 # 端口
  3.  
     
  4.  
    spring:
  5.  
    application:
  6.  
    name: order-service
  7.  
     
  8.  
    datasource:
  9.  
    url: jdbc:mysql://127.0.0.1:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=UTF-8
  10.  
    driver-class-name: com.mysql.jdbc.Driver
  11.  
    username: root
  12.  
    password:
  13.  
     
  14.  
    # dubbo 配置项,对应 DubboConfigurationProperties 配置类
  15.  
    dubbo:
  16.  
    # Dubbo 应用配置
  17.  
    application:
  18.  
    name: ${spring.application.name} # 应用名
  19.  
    # Dubbo 注册中心配
  20.  
    registry:
  21.  
    address: nacos://127.0.0.1:8848 # 注册中心地址。个鞥多注册中心,可见 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文档。
  22.  
    # Dubbo 服务提供者协议配置
  23.  
    protocol:
  24.  
    port: -1 # 协议端口。使用 -1 表示随机端口。
  25.  
    name: dubbo # 使用 `dubbo://` 协议。更多协议,可见 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文档
  26.  
    # 配置扫描 Dubbo 自定义的 @Service 注解,暴露成 Dubbo 服务提供者
  27.  
    scan:
  28.  
    base-packages: cn.iocoder.springboot.lab53.orderservice.service
  29.  
     
  30.  
    # Seata 配置项,对应 SeataProperties 类
  31.  
    seata:
  32.  
    application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
  33.  
    tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
  34.  
    # 服务配置项,对应 ServiceProperties 类
  35.  
    service:
  36.  
    # 虚拟组和分组的映射
  37.  
    vgroup-mapping:
  38.  
    order-service-group: default
  39.  
    # 分组和 Seata 服务的映射
  40.  
    grouplist:
  41.  
    default: 127.0.0.1:8091

差异点在于,删除了 seata.registry 配置项,增加 seata.service.grouplist 配置项来替代。原因如下图:

 

2.2.3 OrderController

创建 OrderController 类,提供 order/create 下单 HTTP API。代码如下:

  1.  
    @RestController
  2.  
    @RequestMapping("/order")
  3.  
    public class OrderController {
  4.  
     
  5.  
    private Logger logger = LoggerFactory.getLogger(OrderController.class);
  6.  
     
  7.  
    @Reference
  8.  
    private OrderService orderService;
  9.  
     
  10.  
    @PostMapping("/create")
  11.  
    public Integer createOrder(@RequestParam("userId") Long userId,
  12.  
    @RequestParam("productId") Long productId,
  13.  
    @RequestParam("price") Integer price) throws Exception {
  14.  
    logger.info("[createOrder] 收到下单请求,用户:{}, 商品:{}, 价格:{}", userId, productId, price);
  15.  
    return orderService.createOrder(userId, productId, price);
  16.  
    }
  17.  
     
  18.  
    }
  • 该 API 中,会调用 OrderService 进行下单。

友情提示:因为这个是示例项目,所以直接传入 price 参数,作为订单的金额,实际肯定不是这样的,哈哈哈~

2.2.4 OrderService

创建 OrderService 接口,定义了创建订单的方法。代码如下:

  1.  
    /**
  2.  
    * 订单 Service
  3.  
    */
  4.  
    public interface OrderService {
  5.  
     
  6.  
    /**
  7.  
    * 创建订单
  8.  
    *
  9.  
    * @param userId 用户编号
  10.  
    * @param productId 产品编号
  11.  
    * @param price 价格
  12.  
    * @return 订单编号
  13.  
    * @throws Exception 创建订单失败,抛出异常
  14.  
    */
  15.  
    Integer createOrder(Long userId, Long productId, Integer price) throws Exception;
  16.  
     
  17.  
    }

2.2.5 OrderServiceImpl

创建 OrderServiceImpl 类,实现创建订单的方法。代码如下:

 

  1.  
    @org.apache.dubbo.config.annotation.Service
  2.  
    public class OrderServiceImpl implements OrderService {
  3.  
     
  4.  
    private Logger logger = LoggerFactory.getLogger(getClass());
  5.  
     
  6.  
    @Autowired
  7.  
    private OrderDao orderDao;
  8.  
     
  9.  
    @Reference
  10.  
    private AccountService accountService;
  11.  
    @Reference
  12.  
    private ProductService productService;
  13.  
     
  14.  
    @Override
  15.  
    @GlobalTransactional // <1>
  16.  
    public Integer createOrder(Long userId, Long productId, Integer price) throws Exception {
  17.  
    Integer amount = 1; // 购买数量,暂时设置为 1。
  18.  
     
  19.  
    logger.info("[createOrder] 当前 XID: {}", RootContext.getXID());
  20.  
     
  21.  
    // <2> 扣减库存
  22.  
    productService.reduceStock(productId, amount);
  23.  
     
  24.  
    // <3> 扣减余额
  25.  
    accountService.reduceBalance(userId, price);
  26.  
     
  27.  
    // <4> 保存订单
  28.  
    OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);
  29.  
    orderDao.saveOrder(order);
  30.  
    logger.info("[createOrder] 保存订单: {}", order.getId());
  31.  
     
  32.  
    // 返回订单编号
  33.  
    return order.getId();
  34.  
    }
  35.  
     
  36.  
    }

<1> 处,在类上,添加 Seata @GlobalTransactional 注解,声明全局事务

<2> 和 <3> 处,在该方法中,调用 ProductService 扣除商品的库存,调用 AccountService 扣除账户的余额。

<3> 处,在全部调用成功后,调用 OrderDao 保存订单。

2.2.6 OrderDao

创建 OrderDao 接口,定义保存订单的操作。代码如下:

  1.  
    @Mapper
  2.  
    @Repository
  3.  
    public interface OrderDao {
  4.  
     
  5.  
    /**
  6.  
    * 插入订单记录
  7.  
    *
  8.  
    * @param order 订单
  9.  
    * @return 影响记录数量
  10.  
    */
  11.  
    @Insert("INSERT INTO orders (user_id, product_id, pay_amount) VALUES (#{userId}, #{productId}, #{payAmount})")
  12.  
    @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
  13.  
    int saveOrder(OrderDO order);
  14.  
     
  15.  
    }

 其中,OrderDO 实体类,对应 orders 表。代码如下:

  1.  
    /**
  2.  
    * 订单实体
  3.  
    */
  4.  
    public class OrderDO {
  5.  
     
  6.  
    /** 订单编号 **/
  7.  
    private Integer id;
  8.  
     
  9.  
    /** 用户编号 **/
  10.  
    private Long userId;
  11.  
     
  12.  
    /** 产品编号 **/
  13.  
    private Long productId;
  14.  
     
  15.  
    /** 支付金额 **/
  16.  
    private Integer payAmount;
  17.  
     
  18.  
    // ... 省略 setter/getter 方法
  19.  
     
  20.  
    }

2.2.7 OrderServiceApplication

创建 OrderServiceApplication 类,用于启动订单服务。代码如下:

  1.  
    @SpringBootApplication
  2.  
    public class OrderServiceApplication {
  3.  
     
  4.  
    public static void main(String[] args) {
  5.  
    SpringApplication.run(OrderServiceApplication.class, args);
  6.  
    }
  7.  
     
  8.  
    }

2.3 商品服务

新建 lab-53-seata-at-dubbo-demo-product-service-api 和 lab-53-seata-at-dubbo-demo-product-service 项目,作为商品服务。项目如下图所示:项目结构

2.3.1 引入依赖

创建 pom.xml 文件,引入相关的依赖。和「2.2.1 引入依赖」是一致的,就不重复“贴”出来了,胖友点击 pom.xml 文件查看。

2.3.2 配置文件

创建 application.yaml 配置文件,添加相关的配置项。和「2.2.2 配置文件」是一致的,就不重复“贴”出来了,胖友点击 application.yaml 文件查看。

2.3.3 ProductService

创建 ProductService 接口,定义了扣除库存的方法。代码如下:

  1.  
    *
  2.  
    * 商品 Service
  3.  
    */
  4.  
    public interface ProductService {
  5.  
     
  6.  
    /**
  7.  
    * 扣减库存
  8.  
    *
  9.  
    * @param productId 商品 ID
  10.  
    * @param amount 扣减数量
  11.  
    * @throws Exception 扣减失败时抛出异常
  12.  
    */
  13.  
    void reduceStock(Long productId, Integer amount) throws Exception;
  14.  
     
  15.  
    }

2.3.4 ProductServiceImpl

创建 ProductServiceImpl 类,实现扣减库存的方法。代码如下:

  1.  
    @org.apache.dubbo.config.annotation.Service
  2.  
    public class ProductServiceImpl implements ProductService {
  3.  
     
  4.  
    private Logger logger = LoggerFactory.getLogger(getClass());
  5.  
     
  6.  
    @Autowired
  7.  
    private ProductDao productDao;
  8.  
     
  9.  
    @Override
  10.  
    @Transactional // <1> 开启新事物
  11.  
    public void reduceStock(Long productId, Integer amount) throws Exception {
  12.  
    logger.info("[reduceStock] 当前 XID: {}", RootContext.getXID());
  13.  
     
  14.  
    // <2> 检查库存
  15.  
    checkStock(productId, amount);
  16.  
     
  17.  
    logger.info("[reduceStock] 开始扣减 {} 库存", productId);
  18.  
    // <3> 扣减库存
  19.  
    int updateCount = productDao.reduceStock(productId, amount);
  20.  
    // 扣除成功
  21.  
    if (updateCount == 0) {
  22.  
    logger.warn("[reduceStock] 扣除 {} 库存失败", productId);
  23.  
    throw new Exception("库存不足");
  24.  
    }
  25.  
    // 扣除失败
  26.  
    logger.info("[reduceStock] 扣除 {} 库存成功", productId);
  27.  
    }
  28.  
     
  29.  
    private void checkStock(Long productId, Integer requiredAmount) throws Exception {
  30.  
    logger.info("[checkStock] 检查 {} 库存", productId);
  31.  
    Integer stock = productDao.getStock(productId);
  32.  
    if (stock < requiredAmount) {
  33.  
    logger.warn("[checkStock] {} 库存不足,当前库存: {}", productId, stock);
  34.  
    throw new Exception("库存不足");
  35.  
    }
  36.  
    }
  37.  
     
  38.  
    }

<1> 处,在类上,添加了 Spring @Transactional 注解,声明本地事务。也就是说,此处会开启一个 seata_product 库的数据库事务。

  •  

<2> 处,检查库存是否足够,如果不够则抛出 Exception 异常。因为我们需要通过异常,回滚全局异常。

<3> 处,进行扣除库存,如果扣除失败则抛出 Exception 异常。

2.3.5 ProductDao

创建 ProductDao 接口,定义获取和扣除库存的操作。代码如下:

  1.  
    @Mapper
  2.  
    @Repository
  3.  
    public interface ProductDao {
  4.  
     
  5.  
    /**
  6.  
    * 获取库存
  7.  
    *
  8.  
    * @param productId 商品编号
  9.  
    * @return 库存
  10.  
    */
  11.  
    @Select("SELECT stock FROM product WHERE id = #{productId}")
  12.  
    Integer getStock(@Param("productId") Long productId);
  13.  
     
  14.  
    /**
  15.  
    * 扣减库存
  16.  
    *
  17.  
    * @param productId 商品编号
  18.  
    * @param amount 扣减数量
  19.  
    * @return 影响记录行数
  20.  
    */
  21.  
    @Update("UPDATE product SET stock = stock - #{amount} WHERE id = #{productId} AND stock >= #{amount}")
  22.  
    int reduceStock(@Param("productId") Long productId, @Param("amount") Integer amount);
  23.  
     
  24.  
    }

2.3.6 ProductServiceApplication

创建 ProductServiceApplication 类,用于启动商品服务。代码如下:

  1.  
    @SpringBootApplication
  2.  
    public class ProductServiceApplication {
  3.  
     
  4.  
    public static void main(String[] args) {
  5.  
    SpringApplication.run(ProductServiceApplication.class, args);
  6.  
    }
  7.  
     
  8.  
    }

2.4 账户服务

新建 lab-53-seata-at-dubbo-demo-account-service-api 和 lab-53-seata-at-dubbo-demo-account-service 项目,作为账户服务。项目如下图所示:项目结构

2.4.1 引入依赖

创建 pom.xml 文件,引入相关的依赖。和「2.2.1 引入依赖」是一致的,就不重复“贴”出来了,胖友点击 pom.xml 文件查看。

2.4.2 配置文件

创建 application.yaml 配置文件,添加相关的配置项。和「2.2.2 配置文件」是一致的,就不重复“贴”出来了,胖友点击 application.yaml 文件查看。

2.4.3 AccountService

创建 AccountService 类,定义扣除余额的方法。代码如下:

  1.  
    /**
  2.  
    * 账户 Service
  3.  
    */
  4.  
    public interface AccountService {
  5.  
     
  6.  
    /**
  7.  
    * 扣除余额
  8.  
    *
  9.  
    * @param userId 用户编号
  10.  
    * @param price 扣减金额
  11.  
    * @throws Exception 失败时抛出异常
  12.  
    */
  13.  
    void reduceBalance(Long userId, Integer price) throws Exception;
  14.  
     
  15.  
    }

2.4.3 AccountServiceImpl

创建 AccountServiceImpl 类,实现扣除余额的方法。代码如下:

  1.  
    @org.apache.dubbo.config.annotation.Service
  2.  
    public class AccountServiceImpl implements AccountService {
  3.  
     
  4.  
    private Logger logger = LoggerFactory.getLogger(getClass());
  5.  
     
  6.  
    @Autowired
  7.  
    private AccountDao accountDao;
  8.  
     
  9.  
    @Override
  10.  
    @Transactional // <1> 开启新事物
  11.  
    public void reduceBalance(Long userId, Integer price) throws Exception {
  12.  
    logger.info("[reduceBalance] 当前 XID: {}", RootContext.getXID());
  13.  
     
  14.  
    // <2> 检查余额
  15.  
    checkBalance(userId, price);
  16.  
     
  17.  
    logger.info("[reduceBalance] 开始扣减用户 {} 余额", userId);
  18.  
    // <3> 扣除余额
  19.  
    int updateCount = accountDao.reduceBalance(price);
  20.  
    // 扣除成功
  21.  
    if (updateCount == 0) {
  22.  
    logger.warn("[reduceBalance] 扣除用户 {} 余额失败", userId);
  23.  
    throw new Exception("余额不足");
  24.  
    }
  25.  
    logger.info("[reduceBalance] 扣除用户 {} 余额成功", userId);
  26.  
    }
  27.  
     
  28.  
    private void checkBalance(Long userId, Integer price) throws Exception {
  29.  
    logger.info("[checkBalance] 检查用户 {} 余额", userId);
  30.  
    Integer balance = accountDao.getBalance(userId);
  31.  
    if (balance < price) {
  32.  
    logger.warn("[checkBalance] 用户 {} 余额不足,当前余额:{}", userId, balance);
  33.  
    throw new Exception("余额不足");
  34.  
    }
  35.  
    }
  36.  
     
  37.  
    }

<1> 处,在类上,添加了 Spring @Transactional 注解,声明本地事务。也就是说,此处会开启一个 seata_account 库的数据库事务。

<2> 处,检查余额是否足够,如果不够则抛出 Exception 异常。因为我们需要通过异常,回滚全局异常。

<3> 处,进行扣除余额,如果扣除失败则抛出 Exception 异常。

2.4.5 AccountDao

创建 AccountDao 接口,定义获取和扣除库存的操作。代码如下:

  1.  
    @Mapper
  2.  
    @Repository
  3.  
    public interface AccountDao {
  4.  
     
  5.  
    /**
  6.  
    * 获取账户余额
  7.  
    *
  8.  
    * @param userId 用户 ID
  9.  
    * @return 账户余额
  10.  
    */
  11.  
    @Select("SELECT balance FROM account WHERE id = #{userId}")
  12.  
    Integer getBalance(@Param("userId") Long userId);
  13.  
     
  14.  
    /**
  15.  
    * 扣减余额
  16.  
    *
  17.  
    * @param price 需要扣减的数目
  18.  
    * @return 影响记录行数
  19.  
    */
  20.  
    @Update("UPDATE account SET balance = balance - #{price} WHERE id = 1 AND balance >= ${price}")
  21.  
    int reduceBalance(@Param("price") Integer price);
  22.  
     
  23.  
    }

2.4.6 AccountServiceApplication

创建 AccountServiceApplication 类,用于启动账户服务。代码如下:

  1.  
    @SpringBootApplication
  2.  
    public class AccountServiceApplication {
  3.  
     
  4.  
    public static void main(String[] args) {
  5.  
    SpringApplication.run(AccountServiceApplication.class, args);
  6.  
    }
  7.  
     
  8.  
    }

2.5 简单测试

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

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

执行 ProductServiceApplication 启动商品服务。相关的日志,胖友自己瞅瞅。
执行 AccountServiceApplication 启动账户服务。相关的日志,胖友自己瞅瞅。

Debug 执行 OrderServiceApplication 启动订单服务。此时,我们可以看到 Seata 相关日志如下:

友情提示:日志的顺序,艿艿做了简单的整理,为了更容易阅读。

  1.  
    # ... 上面还有 Seata 相关 Bean 初始化的日志,忘记加进来了,嘿嘿~
  2.  
     
  3.  
    # 给数据源增加 Seata 的数据源代理
  4.  
    2020-04-05 21:31:37.818 INFO 64204 --- [ main] s.s.a.d.SeataDataSourceBeanPostProcessor : Auto proxy of [dataSource]
  5.  
    2020-04-05 21:31:37.820 INFO 64204 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
  6.  
    2020-04-05 21:31:38.016 INFO 64204 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
  7.  
    # 加载 Druid 提供的 SQL 解析器
  8.  
    2020-04-05 21:31:38.059 INFO 64204 --- [ main] i.s.common.loader.EnhancedServiceLoader : load DbTypeParser[druid] extension by class[io.seata.sqlparser.druid.DruidDelegatingDbTypeParser]
  9.  
    # 加载 Seata 使用 Nacos 注册中心的拓展,从而加载 Seata TC Server 的地址
  10.  
    2020-04-05 21:31:38.071 INFO 64204 --- [ main] i.s.common.loader.EnhancedServiceLoader : load RegistryProvider[Nacos] extension by class[io.seata.discovery.registry.nacos.NacosRegistryProvider]
  11.  
    # 连接到某个 Seata TC Server 服务器
  12.  
    2020-04-05 21:31:38.315 INFO 64204 --- [ main] i.s.c.r.netty.NettyClientChannelManager : will connect to 10.37.129.2:28091
  13.  
    2020-04-05 21:31:38.318 INFO 64204 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:10.37.129.2:28091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/seata_order', applicationId='order-service', transactionServiceGroup='order-service-group'} >
  14.  
    # 加载 Seata 序列化器
  15.  
    2020-04-05 21:31:38.441 INFO 64204 --- [lector_RMROLE_1] i.s.common.loader.EnhancedServiceLoader : load Serializer[SEATA] extension by class[io.seata.serializer.seata.SeataSerializer]
  16.  
    # 连接到另一个 Seata TC Server 服务器
  17.  
    2020-04-05 21:31:38.474 INFO 64204 --- [ main] i.s.c.r.netty.NettyClientChannelManager : will connect to 10.37.129.2:18091
  18.  
    2020-04-05 21:31:38.474 INFO 64204 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:10.37.129.2:18091,msg:< RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/seata_order', applicationId='order-service', transactionServiceGroup='order-service-group'} >
  19.  
    # 注册 Seata Resource Manager 到某个 Seata TC Server 成功
  20.  
    2020-04-05 21:31:38.315 INFO 64204 --- [ main] io.seata.core.rpc.netty.RmRpcClient : RM will register :jdbc:mysql://127.0.0.1:3306/seata_order
  21.  
    2020-04-05 21:31:38.463 INFO 64204 --- [ main] io.seata.core.rpc.netty.RmRpcClient : register RM success. server version:1.1.0,channel:[id: 0x5a7a3adb, L:/10.37.129.2:61910 - R:/10.37.129.2:28091]
  22.  
    2020-04-05 21:31:38.474 INFO 64204 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 64 ms, version:1.1.0,role:RMROLE,channel:[id: 0x5a7a3adb, L:/10.37.129.2:61910 - R:/10.37.129.2:28091]
  23.  
    # 注册 Seata Resource Manager 到另一个 Seata TC Server 成功
  24.  
    2020-04-05 21:31:38.474 INFO 64204 --- [ main] io.seata.core.rpc.netty.RmRpcClient : RM will register :jdbc:mysql://127.0.0.1:3306/seata_order
  25.  
    2020-04-05 21:31:38.478 INFO 64204 --- [ main] io.seata.core.rpc.netty.RmRpcClient : register RM success. server version:1.1.0,channel:[id: 0x84e2337c, L:/10.37.129.2:61911 - R:/10.37.129.2:18091]
  26.  
    2020-04-05 21:31:38.478 INFO 64204 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 3 ms, version:1.1.0,role:RMROLE,channel:[id: 0x84e2337c, L:/10.37.129.2:61911 - R:/10.37.129.2:18091]
  27.  
    # 因为 OrderServiceImpl 添加了 `@GlobalTransactional` 注解,所以创建其代理,用于全局事务。
  28.  
    2020-04-05 21:31:39.107 INFO 64204 --- [ main] i.s.s.a.GlobalTransactionScanner : Bean[cn.iocoder.springboot.lab53.orderservice.service.OrderServiceImpl] with name [orderServiceImpl] would use interceptor [io.seata.spring.annotation.GlobalTransactionalInterceptor]

2.5.1 正常流程

① 先查询下目前数据库的数据情况。如下图所示:

                               

② 使用 Postman 模拟调用 http://127.0.0.1:8081/order/create 创建订单的接口,如下图所示:Postman

此时,在控制台打印日志如下图所示:

  • 订单服务:执行日志
  • 商品服务:执行日志
  • 账户服务:执行日志

再查询下目前数据库的数据情况。如下图所示:数据库 - 结果

2.5.2 异常流程

① 先查询下目前数据库的数据情况。如下图所示:数据库 - 结果

② 在 OrderServiceImpl 的 #createOrder(...) 方法上,打上断点如下图,方便我们看到 product 表的 balance 被减少:断点

友情提示:这里忘记截图了,稍后 IDEA 停留在该断点时,胖友可以去查询 product 表,会发现 balance 已经减少。

③ 使用 Postman 模拟调用 http://127.0.0.1:8081/order/create 创建订单的接口,如下图所示:Postman

此时,在控制台打印日志如下图所示:

  • 订单服务:执行日志
  • 商品服务:

       

  • 账户服务:执行日志

再查询下目前数据库的数据情况。如下图所示:数据库 - 结果

 

            </div><div data-report-view="{&quot;mod&quot;:&quot;1585297308_001&quot;,&quot;spm&quot;:&quot;1001.2101.3001.6548&quot;,&quot;dest&quot;:&quot;https://blog.csdn.net/weixin_42073629/article/details/106749277&quot;,&quot;extend1&quot;:&quot;pc&quot;,&quot;ab&quot;:&quot;new&quot;}"><div></div></div>
    </div>

</article>
posted @ 2022-06-09 22:40  小陈子博客  阅读(860)  评论(0编辑  收藏  举报