我们以电商系统为例,来演示下业务系统如何整合 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 是本地主机的内网地址。
原创文章,作者:bd101bd101,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/278284.html