Dubbo 分布式事务 Seata 入门


1. 概述

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

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

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

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

lab-53-seata-at-dubbo-demo-order-service
  • 商品服务:lab-53-seata-at-dubbo-demo-product-service
  • 账户服务:lab-53-seata-at-dubbo-demo-account-service
  • 本小节,我们将使用 Seata 的 AT 模式,解决多个 Dubbo 服务下的分布式事务的问题。

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

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

    • 服务消费者,使用 Seata 封装的 ApacheDubboTransactionPropagationFilter 过滤器,在发起 Dubbo 远程调用时,将 Seata 全局事务 XID 通过隐式参数传递。
    • 服务提供者,使用 Seata 封装的 ApacheDubboTransactionPropagationFilter 过滤器,在收到 Dubbo 远程调用时,从隐式参数中解析出 Seata 全局事务 XID。

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

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

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

     é?1?????

    data.sql 脚本,创建 seata_order、seata_storage、seata_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 记录。

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

    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.bootgroupId>
    7.   <artifactId>spring-boot-starter-parentartifactId>
    8.   <version>2.2.2.RELEASEversion>
    9.   <relativePath/>
    10.   parent>
    11.   <modelVersion>4.0.0modelVersion>
    12.    
    13.   <artifactId>lab-53-seata-at-dubbo-demo-order-serviceartifactId>
    14.    
    15.   <dependencies>
    16.   <dependency>
    17.   <groupId>cn.iocoder.springboot.labsgroupId>
    18.   <artifactId>lab-53-seata-at-dubbo-demo-order-service-apiartifactId>
    19.   <version>1.0-SNAPSHOTversion>
    20.   dependency>
    21.   <dependency>
    22.   <groupId>cn.iocoder.springboot.labsgroupId>
    23.   <artifactId>lab-53-seata-at-dubbo-demo-account-service-apiartifactId>
    24.   <version>1.0-SNAPSHOTversion>
    25.   dependency>
    26.   <dependency>
    27.   <groupId>cn.iocoder.springboot.labsgroupId>
    28.   <artifactId>lab-53-seata-at-dubbo-demo-product-service-apiartifactId>
    29.   <version>1.0-SNAPSHOTversion>
    30.   dependency>
    31.    
    32.  
    33.   <dependency>
    34.   <groupId>org.springframework.bootgroupId>
    35.   <artifactId>spring-boot-starter-webartifactId>
    36.   dependency>
    37.    
    38.  
    39.   <dependency>
    40.   <groupId>org.springframework.bootgroupId>
    41.   <artifactId>spring-boot-starter-jdbcartifactId>
    42.   dependency>
    43.   <dependency>
    44.   <groupId>mysqlgroupId>
    45.   <artifactId>mysql-connector-javaartifactId>
    46.   <version>5.1.48version>
    47.   dependency>
    48.    
    49.  
    50.   <dependency>
    51.   <groupId>org.mybatis.spring.bootgroupId>
    52.   <artifactId>mybatis-spring-boot-starterartifactId>
    53.   <version>2.1.2version>
    54.   dependency>
    55.    
    56.  
    57.   <dependency>
    58.   <groupId>io.seatagroupId>
    59.   <artifactId>seata-spring-boot-starterartifactId>
    60.   <version>1.1.0version>
    61.   dependency>
    62.    
    63.  
    64.   <dependency>
    65.   <groupId>org.apache.dubbogroupId>
    66.   <artifactId>dubboartifactId>
    67.   <version>2.7.4.1version>
    68.   dependency>
    69.  
    70.   <dependency>
    71.   <groupId>org.apache.dubbogroupId>
    72.   <artifactId>dubbo-spring-boot-starterartifactId>
    73.   <version>2.7.4.1version>
    74.   dependency>
    75.  
    76.   <dependency>
    77.   <groupId>com.alibabagroupId>
    78.   <artifactId>dubbo-registry-nacosartifactId>
    79.   <version>2.7.6version>
    80.   dependency>
    81.   dependencies>
    82.    
    83.   project>

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

    ② 引入 dubbo、dubbo-spring-boot-starter、dubbo-registry-nacos 依赖,Dubbo 使用到的库。

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

    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 配置项来替代。原因如下图:

     

    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 参数,作为订单的金额,实际肯定不是这样的,哈哈哈~

    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.   }

    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 保存订单。

    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.   }

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

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

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

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

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

    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.   }

    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 异常。

    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.   }

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

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

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

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

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

    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.   }

    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 异常。

    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.   }

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

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

    http://127.0.0.1:8081/order/create 创建订单的接口,如下图所示:Postman

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

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

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

    http://127.0.0.1:8081/order/create 创建订单的接口,如下图所示:Postman

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

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

           

    • 账户服务:执行日志

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