Springcloud基础知识(18)- Spring Cloud Alibaba Seata (四) | Nacos+Seata+Openfeign 分布式事务实例(库存服务)


我们以电商系统为例,来演示下业务系统如何整合 Seata。

在电商系统中,用户下单购买一件商品,简化为 3 个服务提供支持:

    Order(订单服务):创建和修改订单。
    Storage(库存服务):对指定的商品扣除仓库库存。
    Account(账户服务) :从用户帐户中扣除商品金额。

当用户从这个电商网站购买了一件商品后,其服务调用步骤如下:

    (1) 调用 Order 服务,创建一条订单数据,订单状态为 “未完成”;
    (2) 调用 Storage 服务,扣减商品库存;
    (3) 调用 Account 服务,从用户账户中扣除商品金额;
    (4) 调用 Order 服务,将订单状态修改为 “已完成”。  


本文使用 “Springcloud基础知识(11)- Spring Cloud Alibaba Nacos (一) | Nacos 简介、服务注册中心” 里的 Nacos 2.1.0 作为 Seata 的注册和配置中心,设置 Nacos 运行在 8848 端口上。

使用 “Springcloud基础知识(15)- Spring Cloud Alibaba Seata (一) | Seata 简介、事务模式、Seata Server” 里的 Seata Server 1.4.2,设置 Seata Server 运行在 8092 端口上。  

在 “Springcloud基础知识(17)- Spring Cloud Alibaba Seata (三) | 配置 db 存储模式、整合 Nacos” 里 SpringcloudDemo05 项目基础上,创建 SeataStorage 子模块。


1. 创建数据库

    在 MariaDB (MySQL) 中,创建一个名为 seata_storage 的数据库实例,并在该数据库内执行以下 SQL。

 1         DROP TABLE IF EXISTS `tbl_storages`;
 2         CREATE TABLE `tbl_storages` (
 3             `id` bigint NOT NULL AUTO_INCREMENT,
 4             `product_id` bigint DEFAULT NULL COMMENT 'product id',
 5             `total` int DEFAULT NULL COMMENT 'total inventory',
 6             `used` int DEFAULT NULL COMMENT 'used inventory',
 7             `residue` int DEFAULT NULL COMMENT 'remaining inventory',
 8             PRIMARY KEY (`id`)
 9         ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
10 
11         INSERT INTO `tbl_storages` VALUES ('1', '1', '100', '0', '100');
12 
13         DROP TABLE IF EXISTS `undo_log`;
14         CREATE TABLE `undo_log` (
15             `branch_id` bigint NOT NULL COMMENT 'branch transaction id',
16             `xid` varchar(128) NOT NULL COMMENT 'global transaction id',
17             `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
18             `rollback_info` longblob NOT NULL COMMENT 'rollback info',
19             `log_status` int NOT NULL COMMENT '0:normal status,1:defense status',
20             `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
21             `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
22             UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
23         ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';


    执行 SQL 后,数据库 `tbl_storages` 表显示如下:

id product_id total used residue
1 1 100 0 100

 


2. 创建 Maven 模块

    选择左上的项目列表中的 SpringcloudDemo05,点击鼠标右键,选择 New -> Module 进入 New Module 页面:

        Maven -> Project SDK: 1.8 -> Check "Create from archtype" -> select "org.apache.maven.archtypes:maven-archtype-quickstart" -> Next

            Name: SeataStorage
            GroupId: com.example
            ArtifactId: SeataStorage

        -> Finish


3. 修改 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
  5                                 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  6         <parent>
  7             <artifactId>SpringcloudDemo05</artifactId>
  8             <groupId>com.example</groupId>
  9             <version>1.0-SNAPSHOT</version>
 10         </parent>
 11         <modelVersion>4.0.0</modelVersion>
 12 
 13         <artifactId>SeataStorage</artifactId>
 14 
 15         <name>SeataStorage</name>
 16         <!-- FIXME change it to the project's website -->
 17         <url>http://www.example.com</url>
 18 
 19         <properties>
 20             <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 21             <maven.compiler.source>1.8</maven.compiler.source>
 22             <maven.compiler.target>1.8</maven.compiler.target>
 23             <maven.install.skip>true</maven.install.skip>
 24         </properties>
 25 
 26         <dependencies>
 27             <dependency>
 28                 <groupId>junit</groupId>
 29                 <artifactId>junit</artifactId>
 30                 <scope>test</scope>
 31             </dependency>
 32 
 33             <dependency>
 34                 <groupId>org.springframework.boot</groupId>
 35                 <artifactId>spring-boot-starter-web</artifactId>
 36             </dependency>        
 37             <dependency>
 38                 <groupId>org.springframework.boot</groupId>
 39                 <artifactId>spring-boot-starter-test</artifactId>
 40                 <scope>test</scope>
 41             </dependency>
 42 
 43             <!-- JDBC -->
 44             <dependency>
 45                 <groupId>org.springframework.boot</groupId>
 46                 <artifactId>spring-boot-starter-data-jdbc</artifactId>
 47             </dependency>
 48             <!-- Druid -->
 49             <dependency>
 50                 <groupId>com.alibaba</groupId>
 51                 <artifactId>druid</artifactId>
 52                 <version>1.2.8</version>
 53             </dependency>
 54 
 55             <!-- MariaDB -->
 56             <dependency>
 57                 <groupId>org.mariadb.jdbc</groupId>
 58                 <artifactId>mariadb-java-client</artifactId>
 59                 <version>${mariadb.version}</version>
 60             </dependency>
 61             <!-- MyBatis -->
 62             <dependency>
 63                 <groupId>org.mybatis.spring.boot</groupId>
 64                 <artifactId>mybatis-spring-boot-starter</artifactId>
 65                 <version>${mybatis.version}</version>
 66             </dependency>                
 67             <dependency>
 68                 <groupId>org.projectlombok</groupId>
 69                 <artifactId>lombok</artifactId>
 70                 <version>${lombok.version}</version>
 71             </dependency>
 72 
 73             <!-- nacos -->
 74             <dependency>
 75                 <groupId>com.alibaba.cloud</groupId>
 76                 <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
 77             </dependency>
 78             <dependency>
 79                 <groupId>com.alibaba.cloud</groupId>
 80                 <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
 81             </dependency>
 82 
 83             <!-- seata -->
 84             <dependency>
 85                 <groupId>com.alibaba.cloud</groupId>
 86                 <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
 87                 <!-- Spring cloud 2021.1 自动导入的 seata 版本是 1.3.0 -->
 88                 <exclusions>
 89                     <exclusion>
 90                         <groupId>io.seata</groupId>
 91                         <artifactId>seata-spring-boot-starter</artifactId>
 92                     </exclusion>
 93                 </exclusions>
 94             </dependency>
 95             <dependency>
 96                 <groupId>io.seata</groupId>
 97                 <artifactId>seata-spring-boot-starter</artifactId>
 98                 <version>1.4.2</version>
 99             </dependency>
100 
101             <!-- OpenFeign -->
102             <!--
103             <dependency>
104                 <groupId>org.springframework.cloud</groupId>
105                 <artifactId>spring-cloud-starter-openfeign</artifactId>
106             </dependency>
107             <dependency>
108                 <groupId>org.springframework.cloud</groupId>
109                 <artifactId>spring-cloud-loadbalancer</artifactId>
110             </dependency> -->
111 
112         </dependencies>
113 
114     </project>


    注:这里我们用 seata 1.4.2 版本替换自动导入的 seata 1.3.0 版本,是因为下文需要用到 seata 1.4.2 的导入单个 dataId 配置的功能。


4. 配置文件

    1) 访问 Nacos 页面修改 seataClient.properties

        浏览器访问 http://localhost:8848/nacos/, 输入登录名和密码(默认 nacos/nacos),点击提交按钮,跳转到 Nacos Server 控制台页面。

        在 Nacos Server 控制台的 “配置管理” 下的 “配置列表” 中,创建或修改如下配置。

1             Data ID: seataClient.properties
2             Group:   SEATA_GROUP
3             配置格式: Properties
4             配置内容:
5 
6                 service.vgroupMapping.default_tx_group=default
7                 service.vgroupMapping.service-storage-group=default
8                 service.default.grouplist=127.0.0.1:8092


        注:可以把这两条内容直接加入到 seataServer.properties,无需新创建 seataClient.properties。这里分开放置 server 和 client 的配置,可以避免混淆两者的配置。

    2) 创建 src/main/resources/application.yml 文件

 1         server:
 2             port: 5001  # 端口号
 3 
 4         spring:
 5             application:
 6                 name: seata-storage-5001  # 服务名
 7             datasource: # 数据源配置
 8                 driver-class-name: org.mariadb.jdbc.Driver
 9                 name: seata_storage
10                 url: jdbc:mysql://127.0.0.1:3306/seata_storage?rewriteBatchedStatements=true
11                 username: nacos
12                 password: nacos
13             cloud:
14                 nacos:
15                     discovery:
16                         server-addr: 127.0.0.1:8848
17                         namespace:  # 留空表示使用 public
18                         group: SEATA_GROUP
19                         username: nacos
20                         password: nacos
21                     config:
22                         server-addr: ${spring.cloud.nacos.discovery.server-addr}
23                         context-path: /nacos
24                         namespace:   # 留空表示使用 public
25                         username: ${spring.cloud.nacos.discovery.username}
26                         password: ${spring.cloud.nacos.discovery.password}
27 
28         mybatis:
29             mapper-locations: classpath:mapper/*.xml
30 
31         seata:
32             #enabled: true
33             application-id: ${spring.application.name}
34             tx-service-group: service-storage-group
35             registry:
36                 type: nacos
37                 nacos:
38                     server-addr: ${spring.cloud.nacos.discovery.server-addr}
39                     application: seata-server
40                     group: ${spring.cloud.nacos.discovery.group}
41                     namespace: ${spring.cloud.nacos.discovery.namespace}
42                     username: ${spring.cloud.nacos.discovery.username}
43                     password: ${spring.cloud.nacos.discovery.password}
44             config:
45                 type: nacos
46                 nacos:
47                     server-addr:  ${spring.cloud.nacos.discovery.server-addr}
48                     group: ${spring.cloud.nacos.discovery.group}
49                     namespace: ${spring.cloud.nacos.discovery.namespace}
50                     username: ${spring.cloud.nacos.discovery.username}
51                     password: ${spring.cloud.nacos.discovery.password}
52                     dataId: seataClient.properties


5. 数据库配置

    1) 配置 Druid

        创建 src/main/java/com/example/config/DruidDataSourceConfig.java 文件

 1         package com.example.config;
 2 
 3         import javax.sql.DataSource;
 4         import java.sql.SQLException;
 5 
 6         import com.alibaba.druid.pool.DruidDataSource;
 7         import org.springframework.boot.context.properties.ConfigurationProperties;
 8         import org.springframework.context.annotation.Bean;
 9         import org.springframework.context.annotation.Configuration;
10         import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
11 
12         @Configuration
13         public class DruidDataSourceConfig implements WebMvcConfigurer {
14 
15             @ConfigurationProperties("spring.datasource")
16             @Bean
17             public DataSource dataSource() throws SQLException {
18                 DruidDataSource druidDataSource = new DruidDataSource();
19                 return druidDataSource;
20             }
21         }


    2) 实体类

        创建 src/main/java/com/example/entity/Storage.java 文件

 1         package com.example.entity;
 2 
 3         import lombok.Data;
 4         import lombok.NoArgsConstructor;
 5         import lombok.experimental.Accessors;
 6         import java.io.Serializable;
 7 
 8         @NoArgsConstructor // 无参构造函数
 9         @Data // 提供类的 get、set、equals、hashCode、canEqual、toString 方法
10         @Accessors(chain = true)
11         public class Storage implements Serializable {
12             private Long id;
13             private Long productId;
14             private Integer total;
15             private Integer used;
16             private Integer residue;
17         }


    3) Mybatis Mapper

        (1) 创建 src/main/java/com/example/mapper/StorageMapper.java 文件

 1             package com.example.mapper;
 2 
 3             import com.example.entity.Storage;
 4             import org.apache.ibatis.annotations.Mapper;
 5 
 6             @Mapper
 7             public interface StorageMapper {
 8 
 9                 Storage selectByProductId(Long productId);
10 
11                 int decrease(Storage storage);
12             }


        (2) 创建 src/main/resources/mapper/StorageMapper.xml 文件

 1             <?xml version="1.0" encoding="UTF-8"?>
 2             <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 3                                     "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 4             <mapper namespace="com.example.mapper.StorageMapper">
 5                 <resultMap id="BaseResultMap" type="com.example.entity.Storage">
 6                     <id column="id" jdbcType="BIGINT" property="id"/>
 7                     <result column="product_id" jdbcType="BIGINT" property="productId"/>
 8                     <result column="total" jdbcType="INTEGER" property="total"/>
 9                     <result column="used" jdbcType="INTEGER" property="used"/>
10                     <result column="residue" jdbcType="INTEGER" property="residue"/>
11                 </resultMap>
12                 <sql id="Base_Column_List">
13                     id, product_id, total, used, residue
14                 </sql>
15                 <update id="decrease" parameterType="com.example.entity.Storage">
16                     UPDATE tbl_storages
17                     <set>
18                         <if test="total != null">
19                             total = #{total,jdbcType=INTEGER},
20                         </if>
21                         <if test="used != null">
22                             used = #{used,jdbcType=INTEGER},
23                         </if>
24                         <if test="residue != null">
25                             residue = #{residue,jdbcType=INTEGER},
26                         </if>
27                     </set>
28                     WHERE product_id = #{productId,jdbcType=BIGINT}
29                 </update>
30                 <select id="selectByProductId" parameterType="java.lang.Long" resultMap="BaseResultMap">
31                     SELECT
32                     <include refid="Base_Column_List"/>
33                     FROM tbl_storages
34                     WHERE product_id = #{productId,jdbcType=BIGINT}
35                 </select>
36             </mapper>


6. 业务操作

    1) 创建 src/main/java/com/example/service/StorageService.java 文件

1         package com.example.service;
2 
3         public interface StorageService {
4 
5             int decrease(Long productId, Integer count);
6 
7         }


    2) 创建 src/main/java/com/example/service/StorageServiceImpl.java 文件

 1         package com.example.service;
 2 
 3         import org.springframework.beans.factory.annotation.Autowired;
 4 
 5         import com.example.entity.Storage;
 6         import com.example.mapper.StorageMapper;
 7         import org.springframework.stereotype.Service;
 8 
 9         @Service
10         public class StorageServiceImpl implements StorageService {
11             @Autowired
12             private StorageMapper storageMapper;
13 
14             @Override
15             public int decrease(Long productId, Integer count) {
16 
17                 Storage storage = storageMapper.selectByProductId(productId);
18                 System.out.println("StorageServiceImpl -> decrease(): storage = " + storage);
19 
20                 if (storage != null && storage.getResidue().intValue() >= count.intValue()) {
21 
22                     Storage storage2 = new Storage();
23                     storage2.setProductId(productId);
24                     storage.setUsed(storage.getUsed() + count);
25                     storage.setResidue(storage.getTotal().intValue() - storage.getUsed());
26                     int ret = storageMapper.decrease(storage);
27                     System.out.println("StorageServiceImpl -> decrease(): ret = " + ret);
28                     return ret;
29                 } else {
30 
31                     System.out.println("StorageServiceImpl -> decrease(): Insufficient Balance");
32                     throw new RuntimeException("StorageServiceImpl - Insufficient Balance");
33                 }
34             }
35         }


    3) 创建 src/main/java/com/example/controller/StorageController.java 文件

 1         package com.example.controller;
 2 
 3         import org.springframework.beans.factory.annotation.Autowired;       
 4         import org.springframework.beans.factory.annotation.Value;
 5         import org.springframework.web.bind.annotation.PostMapping;
 6         import org.springframework.web.bind.annotation.RequestParam;
 7         import org.springframework.web.bind.annotation.RestController;
 8         import com.example.service.StorageService;
 9 
10         @RestController
11         public class StorageController {
12             @Autowired
13             private StorageService storageService;
14 
15             @Value("${server.port}")
16             private String serverPort;
17 
18             @PostMapping(value = "/storage/decrease")
19             public int decrease(@RequestParam("productId") Long productId,            
20                                 @RequestParam("count") Integer count) {
21 
22                 return storageService.decrease(productId, count);
23 
24             }
25         }


    4) 修改 src/main/java/com/example/App.java 文件

 1         package com.example;
 2 
 3         import org.springframework.boot.SpringApplication;
 4         import org.springframework.boot.autoconfigure.SpringBootApplication;
 5         import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 6         import org.springframework.cloud.openfeign.EnableFeignClients;
 7 
 8         @EnableDiscoveryClient
 9         @EnableFeignClients
10         @SpringBootApplication(scanBasePackages = "com.example")
11         public class App {
12             public static void main(String[] args) {
13                 SpringApplication.run(App.class, args);
14             }
15         }

 

 

7. 打包运行

    菜单 Run -> Edit Configurations (或工具条上选择) —> 进入 Run/Debug Configurations 页面 -> Click "+" add new configuration -> Select "Maven":

        Working directory: SeataStorage 所在路径
        Command line: clean package

    -> Apply / OK

    Click Run "SeataStorage [clean, package]" ,jar 包生成在目录 target/ 里

        SeataStorage-1.0-SNAPSHOT.jar
        SeataStorage-1.0-SNAPSHOT.jar.original

    打开 cmd 命令行窗口,进入 SeataStorage 模块目录,运行如下命令:

        ...\SpringcloudDemo05\SeataStorage>java -jar target\SeataStorage-1.0-SNAPSHOT.jar

    显示如下:

 1        ...
 2 
 3         INFO 29796 --- [           main] com.example.App                          : Started App in 3.527 seconds (JVM running for 3.906)
 4         INFO 29796 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.0.2:8092
 5         INFO 29796 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.0.2:8092
 6         INFO 29796 --- [eoutChecker_2_1] i.s.core.rpc.netty.NettyPoolableFactory  : NettyPool create channel to transactionRole:RMROLE,address:192.168.0.2:8092,msg:< RegisterRMRequest{resourceIds='null', applicationId='seata-storage-5001', transactionServiceGroup='service-storage-group'} >
 7         INFO 29796 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory  : NettyPool create channel to transactionRole:TMROLE,address:192.168.0.2:8092,msg:< RegisterTMRequest{applicationId='seata-storage-5001', transactionServiceGroup='service-storage-group'} >
 8         INFO 29796 --- [eoutChecker_2_1] i.s.c.rpc.netty.RmNettyRemotingClient    : register RM success. client version:1.4.2, server version:1.4.2,channel:[id: 0x71db9d47, L:/192.168.0.2:49789 - R:/192.168.0.2:8092]
 9         INFO 29796 --- [eoutChecker_1_1] i.s.c.rpc.netty.TmNettyRemotingClient    : register TM success. client version:1.4.2, server version:1.4.2,channel:[id: 0xe1c5c21b, L:/192.168.0.2:49788 - R:/192.168.0.2:8092]
10         INFO 29796 --- [eoutChecker_2_1] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 130 ms, version:1.4.2,role:RMROLE,channel:[id: 0x71db9d47, L:/192.168.0.2:49789 - R:/192.168.0.2:8092]
11         INFO 29796 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 130 ms, version:1.4.2,role:TMROLE,channel:[id: 0xe1c5c21b, L:/192.168.0.2:49788 - R:/192.168.0.2:8092]


        注:使用 spring-cloud-starter-alibaba-seata 或 seata-spring-boot-starter 的 seata 客户端默认是开启状态 (可以设置 seata.enabled=false 来关闭)。

            seata 客户端里包含了一个全局事务扫描器 (GlobalTransactionScanner),seata 客户端运行后(30 秒左右)GlobalTransactionScanner 会调用初始化功能,使用 netty 连接 Seata 服务端。

            从 log 可以看出 SeataStorage 成功连接到了 Seata Server (192.168.0.2:8092),192.168.0.2 是本地主机的内网地址。


posted @ 2022-08-01 21:50  垄山小站  阅读(175)  评论(0编辑  收藏  举报