Springcloud基础知识(19)- Spring Cloud Alibaba Seata (五) | Nacos+Seata+Openfeign 分布式事务实例(账户服务)
本文在 “Springcloud基础知识(18)- Spring Cloud Alibaba Seata (四) | Nacos+Seata+Openfeign 分布式事务实例(库存服务)” 里 SpringcloudDemo05 项目基础上,创建 SeataAccount 子模块。
1. 创建数据库
在 MariaDB (MySQL) 中,创建一个名为 seata_account 的数据库实例,并在该数据库内执行以下 SQL。
1 DROP TABLE IF EXISTS `tbl_accounts`; 2 CREATE TABLE `tbl_accounts` ( 3 `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', 4 `user_id` bigint DEFAULT NULL COMMENT 'user id', 5 `total` decimal(10,2) DEFAULT NULL COMMENT 'total amount', 6 `used` decimal(10,2) DEFAULT NULL COMMENT 'used amount', 7 `residue` decimal(10,2) DEFAULT '0' COMMENT 'remaining amount', 8 PRIMARY KEY (`id`) 9 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 10 11 INSERT INTO `tbl_accounts` VALUES ('1', '1', '1000.00', '0.00', '1000.00'); 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_accounts` 表显示如下:
id | user_id | total | used | residue |
1 | 1 | 1000.00 | 0.00 | 1000.00 |
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: SeataAccount
GroupId: com.example
ArtifactId: SeataAccount
-> 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>SeataAccount</artifactId> 14 15 <name>SeataAccount</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.vgroupMapping.service-account-group=default 9 service.default.grouplist=127.0.0.1:8092
注:可以把这两条内容直接加入到 seataServer.properties,无需新创建 seataClient.properties。这里分开放置 server 和 client 的配置,可以避免混淆两者的配置。
2) 创建 src/main/resources/application.yml 文件
1 server: 2 port: 6001 # 端口号 3 4 spring: 5 application: 6 name: seata-account-6001 # 服务名 7 datasource: # 数据源配置 8 driver-class-name: org.mariadb.jdbc.Driver 9 name: seata_account 10 url: jdbc:mysql://127.0.0.1:3306/seata_account?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: ${spring.cloud.nacos.discovery.namespace} 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-account-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/Account.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 Account implements Serializable { 12 private Long id; 13 private Long userId; 14 private Integer total; 15 private Integer used; 16 private Integer residue; 17 }
3) Mybatis Mapper
(1) 创建 src/main/java/com/example/mapper/AccountMapper.java 文件
1 package com.example.mapper; 2 3 import org.apache.ibatis.annotations.Mapper; 4 import com.example.entity.Account; 5 6 @Mapper 7 public interface AccountMapper { 8 9 Account selectByUserId(Long userId); 10 11 int decrease(Long userId, Double money); 12 }
(2) 创建 src/main/resources/mapper/AccountMapper.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.AccountMapper"> 5 <resultMap id="BaseResultMap" type="com.example.entity.Account"> 6 <id column="id" jdbcType="BIGINT" property="id"/> 7 <result column="user_id" jdbcType="BIGINT" property="userId"/> 8 <result column="total" jdbcType="DECIMAL" property="total"/> 9 <result column="used" jdbcType="DECIMAL" property="used"/> 10 <result column="residue" jdbcType="DECIMAL" property="residue"/> 11 </resultMap> 12 <sql id="Base_Column_List"> 13 id, user_id, total, used, residue 14 </sql> 15 <select id="selectByUserId" resultType="com.example.entity.Account"> 16 SELECT 17 <include refid="Base_Column_List"/> 18 FROM tbl_accounts 19 WHERE user_id = #{userId,jdbcType=BIGINT} 20 </select> 21 <update id="decrease"> 22 UPDATE tbl_accounts 23 SET residue = residue - #{money}, 24 used = used + #{money} 25 WHERE user_id = #{userId}; 26 </update> 27 </mapper>
6. 业务操作
1) 创建 src/main/java/com/example/service/AccountService.java 文件
1 package com.example.service; 2 3 public interface AccountService { 4 5 int decrease(Long userId, Double money); 6 7 }
2) 创建 src/main/java/com/example/service/AccountServiceImpl.java 文件
1 package com.example.service; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 5 import com.example.entity.Account; 6 import com.example.mapper.AccountMapper; 7 import org.springframework.stereotype.Service; 8 9 @Service 10 public class AccountServiceImpl implements AccountService { 11 @Autowired 12 private AccountMapper accountMapper; 13 14 @Override 15 public int decrease(Long userId, Double money) { 16 17 Account account = accountMapper.selectByUserId(userId); 18 if (account != null && account.getResidue().intValue() >= money.intValue()) { 19 20 int ret = accountMapper.decrease(userId, money); 21 System.out.println("AccountServiceImpl -> decrease(): ret = " + ret); 22 return ret; 23 24 } else { 25 26 System.out.println("AccountServiceImpl -> decrease(): Insufficient Balance"); 27 throw new RuntimeException("AccountServiceImpl - Insufficient Balance"); 28 29 } 30 } 31 32 }
3) 创建 src/main/java/com/example/controller/AccountController.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.AccountService; 9 10 @RestController 11 public class AccountController { 12 @Autowired 13 private AccountService accountService; 14 15 @Value("${server.port}") 16 private String serverPort; 17 18 @PostMapping(value = "/account/decrease") 19 public int decrease(@RequestParam("userId") Long userId, 20 @RequestParam("money") Double money) { 21 22 return accountService.decrease(userId, money); 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: SeataAccount 所在路径
Command line: clean package
-> Apply / OK
Click Run "SeataAccount [clean, package]" ,jar 包生成在目录 target/ 里
SeataAccount-1.0-SNAPSHOT.jar
SeataAccount-1.0-SNAPSHOT.jar.original
打开 cmd 命令行窗口,进入 SeataAccount 模块目录,运行如下命令:
...\SpringcloudDemo05\SeataAccount>java -jar target\SeataAccount-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-account-6001', transactionServiceGroup='service-account-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-account-6001', transactionServiceGroup='service-account-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 可以看出 SeataAccount 成功连接到了 Seata Server (192.168.0.2:8092),192.168.0.2 是本地主机的内网地址。