本文在 “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/tech/pnotes/278306.html