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 是本地主机的内网地址。


posted @ 2022-08-01 22:05  垄山小站  阅读(146)  评论(0编辑  收藏  举报