Springcloud基础知识(20)- Spring Cloud Alibaba Seata (六) | Nacos+Seata+Openfeign 分布式事务实例(订单服务、集成测试)


本文在 “Springcloud基础知识(19)- Spring Cloud Alibaba Seata (五) | Nacos+Seata+Openfeign 分布式事务实例(账户服务)” 里 SpringcloudDemo05 项目基础上,创建 SeataOrder 子模块,协同 SeataStorage 和 SeataAccount 子模块,进行集成测试。

1. 创建数据库

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

 1         # tbl_orders(订单表)
 2         DROP TABLE IF EXISTS `tbl_orders`;
 3         CREATE TABLE `tbl_orders` (
 4             `id` bigint NOT NULL AUTO_INCREMENT,
 5             `user_id` bigint DEFAULT NULL COMMENT 'user id',
 6             `product_id` bigint DEFAULT NULL COMMENT 'product id',
 7             `count` int DEFAULT NULL COMMENT 'product count',
 8             `money` decimal(10,2) DEFAULT NULL COMMENT 'amount',
 9             `status` int DEFAULT NULL COMMENT 'order status:0:pending;1:finish',
10             PRIMARY KEY (`id`)
11         ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
12 
13         # undo_log(回滚日志表)
14         DROP TABLE IF EXISTS `undo_log`;
15         CREATE TABLE `undo_log` (
16             `branch_id` bigint NOT NULL COMMENT 'branch transaction id',
17             `xid` varchar(128) NOT NULL COMMENT 'global transaction id',
18             `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
19             `rollback_info` longblob NOT NULL COMMENT 'rollback info',
20             `log_status` int NOT NULL COMMENT '0:normal status,1:defense status',
21             `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
22             `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
23             UNIQUE KEY `ux_undo_log` (`branch_id`,`xid`)
24         ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

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: SeataOrder
            GroupId: com.example
            ArtifactId: SeataOrder

        -> 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>SeataOrder</artifactId>
 14 
 15         <name>SeataOrder</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                 <exclusions>
 88                     <exclusion>
 89                         <groupId>io.seata</groupId>
 90                         <artifactId>seata-spring-boot-starter</artifactId>
 91                     </exclusion>
 92                 </exclusions>
 93             </dependency>
 94             <dependency>
 95                 <groupId>io.seata</groupId>
 96                 <artifactId>seata-spring-boot-starter</artifactId>
 97                 <version>1.4.2</version>
 98             </dependency>
 99 
100             <!-- OpenFeign -->
101             <dependency>
102                 <groupId>org.springframework.cloud</groupId>
103                 <artifactId>spring-cloud-starter-openfeign</artifactId>
104             </dependency>
105             <dependency>
106                 <groupId>org.springframework.cloud</groupId>
107                 <artifactId>spring-cloud-loadbalancer</artifactId>
108             </dependency>
109 
110         </dependencies>
111 
112     </project>

 

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.vgroupMapping.service-account-group=default
 9                 service.vgroupMapping.service-order-group=default
10                 service.default.grouplist=127.0.0.1:8092

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

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

 1         server:
 2             port: 7001  # 端口号
 3 
 4         spring:
 5             application:
 6                 name: seata-order-7001  # 服务名
 7             datasource: # 数据源配置
 8                 driver-class-name: org.mariadb.jdbc.Driver
 9                 name: seata_order
10                 url: jdbc:mysql://127.0.0.1:3306/seata_order?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-order-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:   # 留空表示使用 public
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:
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) 实体类

        (1) 创建 src/main/java/com/example/entity/Order.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 Order implements Serializable {
12                 private Long id;
13                 private Long userId;
14                 private Long productId;
15                 private Integer count;
16                 private Double money;
17                 private Integer status;
18             }

        (2) 创建 src/main/java/com/example/entity/CommonResult.java 文件

 1             package com.example.entity;
 2 
 3             import lombok.Data;
 4             import lombok.experimental.Accessors;
 5             import java.io.Serializable;
 6 
 7             @Data // 提供类的 get、set、equals、hashCode、canEqual、toString 方法
 8             @Accessors(chain = true)
 9             public class CommonResult implements Serializable {
10                 private Integer code;
11                 private String description;
12 
13                 public CommonResult(Integer code, String description) {
14                     this.code = code;
15                     this.description = description;
16                 }
17 
18             }

    3) Mybatis Mapper

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

 1             package com.example.mapper;
 2 
 3             import com.example.entity.Order;
 4             import org.apache.ibatis.annotations.Mapper;
 5             import org.apache.ibatis.annotations.Param;
 6 
 7             @Mapper
 8             public interface OrderMapper {
 9 
10                 int insert(Order order);
11                 int insertSelective(Order order);
12 
13                 Order selectByPrimaryKey(Long id);
14 
15                 int updateByPrimaryKey(Order order);
16                 int updateByPrimaryKeySelective(Order order);
17                 void update(@Param("userId") Long userId, @Param("status") Integer status);
18 
19                 int deleteByPrimaryKey(Long id);
20             }

        (2) 创建 src/main/resources/mapper/OrderMapper.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.OrderMapper">
  5             <resultMap id="BaseResultMap" type="com.example.entity.Order">
  6                 <id column="id" jdbcType="BIGINT" property="id"/>
  7                 <result column="user_id" jdbcType="BIGINT" property="userId"/>
  8                 <result column="product_id" jdbcType="BIGINT" property="productId"/>
  9                 <result column="count" jdbcType="INTEGER" property="count"/>
 10                 <result column="money" jdbcType="DECIMAL" property="money"/>
 11                 <result column="status" jdbcType="INTEGER" property="status"/>
 12             </resultMap>
 13             <sql id="Base_Column_List">
 14                 id, user_id, product_id, count, money, status
 15             </sql>
 16             <insert id="insert" parameterType="com.example.entity.Order">
 17                 INSERT INTO tbl_orders (id, user_id, product_id, count, money, status)
 18                 VALUES (#{id,jdbcType=BIGINT}, #{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},
 19                 #{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER})
 20             </insert>
 21             <update id="update">
 22                 UPDATE tbl_orders
 23                 SET status = 1
 24                 WHERE user_id = #{userId}
 25                 AND status = #{status};
 26             </update>
 27 
 28             <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
 29                 SELECT
 30                 <include refid="Base_Column_List"/>
 31                 FROM tbl_orders
 32                 WHERE id = #{id,jdbcType=BIGINT}
 33             </select>
 34             <insert id="insertSelective" parameterType="com.example.entity.Order">
 35                 INSERT INTO tbl_orders
 36                 <trim prefix="(" suffix=")" suffixOverrides=",">
 37                     <if test="id != null">
 38                         id,
 39                     </if>
 40                     <if test="userId != null">
 41                         user_id,
 42                     </if>
 43                     <if test="productId != null">
 44                         product_id,
 45                     </if>
 46                     <if test="count != null">
 47                         count,
 48                     </if>
 49                     <if test="money != null">
 50                         money,
 51                     </if>
 52                     <if test="status != null">
 53                         status,
 54                     </if>
 55                 </trim>
 56                 <trim prefix="values (" suffix=")" suffixOverrides=",">
 57                     <if test="id != null">
 58                         #{id,jdbcType=BIGINT},
 59                     </if>
 60                     <if test="userId != null">
 61                         #{userId,jdbcType=BIGINT},
 62                     </if>
 63                     <if test="productId != null">
 64                         #{productId,jdbcType=BIGINT},
 65                     </if>
 66                     <if test="count != null">
 67                         #{count,jdbcType=INTEGER},
 68                     </if>
 69                     <if test="money != null">
 70                         #{money,jdbcType=DECIMAL},
 71                     </if>
 72                     <if test="status != null">
 73                         #{status,jdbcType=INTEGER},
 74                     </if>
 75                 </trim>
 76             </insert>
 77             <update id="updateByPrimaryKeySelective" parameterType="com.example.entity.Order">
 78                 UPDATE tbl_orders
 79                 <set>
 80                     <if test="userId != null">
 81                         user_id = #{userId,jdbcType=BIGINT},
 82                     </if>
 83                     <if test="productId != null">
 84                         product_id = #{productId,jdbcType=BIGINT},
 85                     </if>
 86                     <if test="count != null">
 87                         count = #{count,jdbcType=INTEGER},
 88                     </if>
 89                     <if test="money != null">
 90                         money = #{money,jdbcType=DECIMAL},
 91                     </if>
 92                     <if test="status != null">
 93                         status = #{status,jdbcType=INTEGER},
 94                     </if>
 95                 </set>
 96                 WHERE id = #{id,jdbcType=BIGINT}
 97             </update>
 98             <update id="updateByPrimaryKey" parameterType="com.example.entity.Order">
 99                 UPDATE tbl_orders
100                 SET user_id    = #{userId,jdbcType=BIGINT},
101                 product_id = #{productId,jdbcType=BIGINT},
102                 count      = #{count,jdbcType=INTEGER},
103                 money      = #{money,jdbcType=DECIMAL},
104                 status     = #{status,jdbcType=INTEGER}
105                 WHERE id = #{id,jdbcType=BIGINT}
106             </update>
107             <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
108                 DELETE FROM tbl_orders
109                 WHERE id = #{id,jdbcType=BIGINT}
110             </delete>
111 
112         </mapper>

6. 业务操作

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

1         package com.example.service;
2 
3         import com.example.entity.Order;
4 
5         public interface OrderService {
6 
7             int insert(Order order);
8 
9         }

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

 1         package com.example.service;
 2 
 3         import org.springframework.cloud.openfeign.FeignClient;
 4         import org.springframework.web.bind.annotation.PostMapping;
 5         import org.springframework.web.bind.annotation.RequestParam;
 6 
 7         @FeignClient(value = "seata-storage-5001")
 8         public interface StorageFeignService {
 9 
10             @PostMapping(value = "/storage/decrease")
11             int decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
12 
13         }

    3) 创建 src/main/java/com/example/service/AccountFeignService.java 文件

 1         package com.example.service;
 2 
 3         import org.springframework.cloud.openfeign.FeignClient;
 4         import org.springframework.web.bind.annotation.PostMapping;
 5         import org.springframework.web.bind.annotation.RequestParam;
 6 
 7         @FeignClient(value = "seata-account-6001")
 8         public interface AccountFeignService {
 9 
10             @PostMapping(value = "/account/decrease")
11             int decrease(@RequestParam("userId") Long userId, @RequestParam("money") Double money);
12 
13         }    

    4) 创建 src/main/java/com/example/service/OrderServiceImpl.java 文件

 1         package com.example.service;
 2 
 3         import org.springframework.beans.factory.annotation.Autowired;
 4         import org.springframework.stereotype.Service;
 5         import io.seata.spring.annotation.GlobalTransactional;
 6 
 7         import com.example.entity.Order;
 8         import com.example.mapper.OrderMapper;
 9 
10         @Service
11         public class OrderServiceImpl implements OrderService {
12             @Autowired
13             private OrderMapper orderMapper;
14             @Autowired
15             private StorageFeignService storageFeignService;
16             @Autowired
17             private AccountFeignService accountFeignService;
18 
19             @Override
20             @GlobalTransactional(rollbackFor = Exception.class)
21             public int insert(Order order) {
22 
23                 order.setUserId(new Long(1));
24                 order.setStatus(0);
25 
26                 int ret = orderMapper.insert(order);
27                 System.out.println("OrderServiceImpl -> orderMapper.insert(): ret = " + ret);
28 
29                 ret = storageFeignService.decrease(order.getProductId(), order.getCount());
30                 System.out.println("OrderServiceImpl -> storageService.decrease(): ret = " + ret);
31 
32                 ret = accountFeignService.decrease(order.getUserId(), order.getMoney());
33                 System.out.println("OrderServiceImpl -> accountFeignService.decrease(): ret = " + ret);
34 
35                 orderMapper.update(order.getUserId(), 0);
36 
37                 return ret;
38 
39             }
40 
41         }

        在分布式微服务架构中,可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。

        当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。

        @GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:

            (1) 在类中某个方法使用时,全局事务的范围就是该方法以及它所涉及的所有服务。
            (2) 在类上使用时,全局事务的范围就是这个类中的所有方法以及这些方法涉及的服务。

    5) 创建 src/main/java/com/example/controller/OrderController.java 文件

 1         package com.example.controller;
 2 
 3         import org.springframework.beans.factory.annotation.Autowired;
 4         import org.springframework.web.bind.annotation.GetMapping;
 5         import org.springframework.web.bind.annotation.PathVariable;
 6         import org.springframework.web.bind.annotation.RestController;
 7 
 8         import com.example.entity.Order;
 9         import com.example.entity.CommonResult;
10         import com.example.service.OrderService;
11 
12         @RestController
13         public class OrderController {
14             @Autowired
15             private OrderService orderService;
16 
17             @GetMapping("/order/create/{pid}/{count}/{money}")
18             public CommonResult create(@PathVariable("pid") Integer pid,
19                                     @PathVariable("count") Integer count,
20                                     @PathVariable("money") Double money) {
21 
22                 Order order = new Order();
23                 order.setProductId(Integer.valueOf(pid).longValue());
24                 order.setCount(count);
25                 order.setMoney(money);
26 
27                 System.out.println("OrderController -> create(): order = " + order);
28                 return orderService.insert(order);
29             }
30 
31         }

    6) 修改 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: SeataOrder 所在路径
        Command line: clean package

    -> Apply / OK

    Click Run “SeataOrder [clean, package]” ,jar 包生成在目录 target/ 里

        SeataOrder-1.0-SNAPSHOT.jar
        SeataOrder-1.0-SNAPSHOT.jar.original

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

        …/SpringcloudDemo05/SeataOrder>java -jar target/SeataOrder-1.0-SNAPSHOT.jar

    显示如下:

 1         ...
 2 
 3         INFO 15716 --- [           main] com.example.App                          : Started App in 3.62 seconds (JVM running for 4.009)
 4         INFO 15716 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.0.2:8092
 5         INFO 15716 --- [eoutChecker_2_1] i.s.c.r.netty.NettyClientChannelManager  : will connect to 192.168.0.2:8092
 6         INFO 15716 --- [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-order-7001', transactionServiceGroup='service-order-group'} >
 7         INFO 15716 --- [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-order-7001', transactionServiceGroup='service-order-group'} >
 8         INFO 15716 --- [eoutChecker_1_1] i.s.c.rpc.netty.TmNettyRemotingClient    : register TM success. client version:1.4.2, server version:1.4.2,channel:[id: 0x2dd221d2, L:/192.168.0.2:51581 - R:/192.168.0.2:8092]
 9         INFO 15716 --- [eoutChecker_2_1] i.s.c.rpc.netty.RmNettyRemotingClient    : register RM success. client version:1.4.2, server version:1.4.2,channel:[id: 0xc12cf699, L:/192.168.0.2:51582 - R:/192.168.0.2:8092]
10         INFO 15716 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 38 ms, version:1.4.2,role:TMROLE,channel:[id: 0x2dd221d2, L:/192.168.0.2:51581 - R:/192.168.0.2:8092]
11         INFO 15716 --- [eoutChecker_2_1] i.s.core.rpc.netty.NettyPoolableFactory  : register success, cost 38 ms, version:1.4.2,role:RMROLE,channel:[id: 0xc12cf699, L:/192.168.0.2:51582 - R:/192.168.0.2:8092]

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

8. 集成测试

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

        java -jar target/SeataStorage-1.0-SNAPSHOT.jar

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

        java -jar target/SeataAccount-1.0-SNAPSHOT.jar

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

        java -jar target/SeataOrder-1.0-SNAPSHOT.jar

    等待以上程序都连接到 Seata Server 后,开始测试。

    1) 正常访问

        浏览器访问 http://localhost:7001/order/create/1/1/1,页面返回结果如下:

            {“code”:200,”description”:”Create order successfully”}

        查看 seata_storage 数据库 tbl_storages 表,显示结果如下:

            id  product_id  total   used    residue
            1       1             100     1         99

        查看 seata_account 数据库 tbl_accounts 表,显示结果如下:

            id  user_id      total     used    residue
            1       1          1000.0    1.00     999.00

        查看 seata_order 数据库 tbl_orders 表,显示结果如下:

            id  user_id  product_id   count  money   status
            1      1             1              1        1.00        1

    2) 异常访问

        浏览器访问 http://localhost:7001/order/create/1/1/1000,该操作的意思是减掉 1 个库存和减掉 1000.00 余额,页面显示结果如下:

            Whitelabel Error Page
            This application has no explicit mapping for /error, so you are seeing this as a fallback.

            Mon Aug 01 18:01:47 CST 2022
            There was an unexpected error (type=Internal Server Error, status=500).
             
        页面显示的 500 异常,定位到 SeataOrder (seata-order-7001) 程序 OrderServiceImpl.insert() 方法,代码如下:

 1             @Override
 2             @GlobalTransactional(rollbackFor = Exception.class)
 3             public int insert(Order order) {
 4 
 5                 order.setUserId(new Long(1));
 6                 order.setStatus(0);
 7 
 8                 int ret = orderMapper.insert(order);
 9                 System.out.println("OrderServiceImpl -> orderMapper.insert(): ret = " + ret);
10 
11                 ret = storageFeignService.decrease(order.getProductId(), order.getCount());
12                 System.out.println("OrderServiceImpl -> storageService.decrease(): ret = " + ret);
13 
14                 ret = accountFeignService.decrease(order.getUserId(), order.getMoney());
15                 System.out.println("OrderServiceImpl -> accountFeignService.decrease(): ret = " + ret);
16 
17                 orderMapper.update(order.getUserId(), 0);
18 
19                 return ret;
20 
21             }

        异常发生在 accountFeignService.decrease() 方法,这个方法是通过 Openfeign 运程调用到 SeataAccount (seata-account-6001) 程序的 AccountServiceImpl.decrease() 方法。异常原因是 seata_account 数据库 tbl_accounts 表的 residue 值 999.00 小于 1000.00,就是余额不足。
        
        异常抛出前,orderMapper.insert() 和 storageFeignService.decrease() 已经完成了数据库插入和更新操作。事务的特点:所有操作必须全部完成,中间有操作出错,所有已完成的操作,需要恢复原样(或称事务回滚,Rollback)。

        @GlobalTransactional 注解的作用,就是在这种情形下触发 SeataOrder 和 SeataStorage 的事务回滚 (Rollback)。

    3) 异常访问的控制台信息

       SeataStorage (seata-storage-5001) 控制台输出如下(回滚日志):

1             ...
2 
3             2022-08-01 18:01:47.767  INFO 16120 --- [h_RMROLE_1_2_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=192.168.0.2:8092:2035917448298704911,branchId=2035917448298704915,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seata_storage,applicationData=null
4             2022-08-01 18:01:47.768  INFO 16120 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.0.2:8092:2035917448298704911 2035917448298704915 jdbc:mysql://127.0.0.1:3306/seata_storage
5             2022-08-01 18:01:47.804  INFO 16120 --- [h_RMROLE_1_2_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 192.168.0.2:8092:2035917448298704911 branch 2035917448298704915, undo_log deleted with GlobalFinished
6             2022-08-01 18:01:47.804  INFO 16120 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked    

        SeataAccount (seata-account-6001) 控制台输出如下:

1             ...
2 
3             2022-08-01 17:58:20.810  INFO 49720 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=192.168.0.2:8092:2035917448298704897,branchId=2035917448298704904,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seata_account,applicationData=null
4             2022-08-01 17:58:20.812  INFO 49720 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.0.2:8092:2035917448298704897 2035917448298704904 jdbc:mysql://127.0.0.1:3306/seata_account null
5             2022-08-01 17:58:20.814  INFO 49720 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
6             AccountServiceImpl -> decrease(): Insufficient Balance

        SeataOrder (seata-order-7001) 控制台输出如下(回滚日志):

1             ...
2 
3             18:01:47.813  INFO 15600 --- [h_RMROLE_1_3_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=192.168.0.2:8092:2035917448298704911,branchId=2035917448298704913,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seata_order,applicationData=null
4             2022-08-01 18:01:47.813  INFO 15600 --- [h_RMROLE_1_3_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.0.2:8092:2035917448298704911 2035917448298704913 jdbc:mysql://127.0.0.1:3306/seata_order
5             2022-08-01 18:01:47.837  INFO 15600 --- [h_RMROLE_1_3_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 192.168.0.2:8092:2035917448298704911 branch 2035917448298704913, undo_log deleted with GlobalFinished
6             2022-08-01 18:01:47.838  INFO 15600 --- [h_RMROLE_1_3_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
7             2022-08-01 18:01:47.848  INFO 15600 --- [nio-7001-exec-4] i.seata.tm.api.DefaultGlobalTransaction  : Suspending current transaction, xid = 192.168.0.2:8092:2035917448298704911
8             2022-08-01 18:01:47.848  INFO 15600 --- [nio-7001-exec-4] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.0.2:8092:2035917448298704911] rollback status: Rollbacked

         Seata Server 控制台输出如下:

 1             ...
 2 
 3             18:01:47.697  INFO --- [verHandlerThread_1_34_500] i.seata.server.coordinator.AbstractCore  : Register branch successfully, xid = 192.168.0.2:8092:2035917448298704911, branchId = 2035917448298704913, resourceId = jdbc:mysql://127.0.0.1:3306/seata_order ,lockKeys = tbl_orders:2
 4             18:01:47.714  INFO --- [     batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler         : SeataMergeMessage xid=192.168.0.2:8092:2035917448298704911,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seata_storage,lockKey=tbl_storages:1
 5             ,clientIp:192.168.0.2,vgroup:service-storage-group
 6             18:01:47.720  INFO --- [verHandlerThread_1_35_500] i.seata.server.coordinator.AbstractCore  : Register branch successfully, xid = 192.168.0.2:8092:2035917448298704911, branchId = 2035917448298704915, resourceId = jdbc:mysql://127.0.0.1:3306/seata_storage ,lockKeys = tbl_storages:1
 7             18:01:47.759  INFO --- [     batchLoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler         : SeataMergeMessage xid=192.168.0.2:8092:2035917448298704911,extraData=null
 8             ,clientIp:192.168.0.2,vgroup:service-order-group
 9             18:01:47.811  INFO --- [verHandlerThread_1_36_500] io.seata.server.coordinator.DefaultCore  : Rollback branch transaction successfully, xid = 192.168.0.2:8092:2035917448298704911 branchId = 2035917448298704915
10             18:01:47.842  INFO --- [verHandlerThread_1_36_500] io.seata.server.coordinator.DefaultCore  : Rollback branch transaction successfully, xid = 192.168.0.2:8092:2035917448298704911 branchId = 2035917448298704913
11             18:01:47.846  INFO --- [verHandlerThread_1_36_500] io.seata.server.coordinator.DefaultCore  : Rollback global transaction successfully, xid = 192.168.0.2:8092:2035917448298704911.

    4) 异常访问的数据库结果显示 (回滚成功)

        查看 seata_storage 数据库 tbl_storages 表,显示结果如下:

            id  product_id  total   used    residue
            1       1            100      1         99

        查看 seata_account 数据库 tbl_accounts 表,显示结果如下:

            id  user_id      total     used    residue
            1       1          1000.0    1.00     999.00

        查看 seata_order 数据库 tbl_orders 表,显示结果如下:

            id  user_id  product_id   count    money   status
            1      1             1              1          1.00       1

 

原创文章,作者:,如若转载,请注明出处:https://blog.ytso.com/278306.html

(0)
上一篇 2022年8月1日
下一篇 2022年8月1日

相关推荐

发表回复

登录后才能评论