本文在 “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