springboot2.1.8+springcloud-Greenwich.SR3+seata-server1.3.0+nacos-server1.3.0 实现分布式事务

前言

由于seata是目前主流的分布式事务解决方案,所以下面来讲述下如何基于seata实现分布式事务控制。

前期准备

本文基于seata的 AT模式实现分布式版本控制,AT的好处是配置开发简单、对业务无侵入,是seata主推的分布式事务解决方案。

Nacos 依赖 Java 环境来运行。前期先安装好java运行环境。这里安装的版本是JDK8。

相关版本

springboot版本:2.1.8
springcloud版本:Greenwich.SR3
spring-cloud-alibaba版本:2.1.0.RELEASE
seata-server版本:1.3.0 下载地址:https://seata.io/zh-cn/blog/download.html
nacos-server版本:1.3.0 下载地址:https://github.com/alibaba/nacos/releases
pom.xml中seata jar包的版本为

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.3.0</version>
        </dependency>

依赖环境安装

nacos相关

1.nacos1.3.0版本需要依赖msyql数据库运行,首先安装mysql5.6
2.创建nacos数据库
3.将nacos的mysql脚本导入,脚本文件为nacos-mysql.sql,路径在nacos的conf文件夹下
4.windows下运行nacos/bin下的startup.cmd命令开始启动

seata相关

seata1.0后就不自带数据库脚本了,需要我们自己去github上下载。相关脚本和配置下载方法如下:

client

https://github.com/seata/seata/tree/develop/script/client

存放用于客户端的配置和SQL

  • at: AT模式下的 undo_log 建表语句
  • conf: 客户端的配置文件
  • saga: SAGA 模式下所需表的建表语句
  • spring: SpringBoot 应用支持的配置文件

server

https://github.com/seata/seata/tree/develop/script/server

存放server侧所需SQL和部署脚本

  • db: server 侧的保存模式为 db 时所需表的建表语句
  • docker-compose: server 侧通过 docker-compose 部署的脚本
  • helm: server 侧通过 Helm 部署的脚本
  • kubernetes: server 侧通过 Kubernetes 部署的脚本

config-center

https://github.com/seata/seata/tree/develop/script/config-center

用于存放各种配置中心的初始化脚本,执行时都会读取 config.txt配置文件,并写入配置中心

  • nacos: 用于向 Nacos 中添加配置
  • zk: 用于向 Zookeeper 中添加配置,脚本依赖 Zookeeper 的相关脚本,需要手动下载;ZooKeeper相关的配置可以写在 zk-params.txt 中,也可以在执行的时候输入
  • apollo: 向 Apollo 中添加配置,Apollo 的地址端口等可以写在 apollo-params.txt,也可以在执行的时候输入
  • etcd3: 用于向 Etcd3 中添加配置
  • consul: 用于向 consul 中添加配置

导入seata-server端的数据库脚本

mysql中新建数据库名为seata,将下载好的脚本文件导入的seata数据库中即可。脚本sql如下:

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

新建三个业务数据库,分别为seat-account,seat-order,seat-storage

将以下三张表分别导入这三个库中

seat-account库中导入如下脚本

CREATE TABLE `account` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
  `total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
  `used` decimal(10,0) DEFAULT NULL COMMENT '已用余额',
  `residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `seat-account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');

seat-order导入如下脚本:

-- noinspection SqlDialectInspectionForFile

-- noinspection SqlNoDataSourceInspectionForFile

CREATE TABLE `order` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
  `count` int(11) DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;

seat-storage库导入如下脚本:

CREATE TABLE `storage` (
                         `id` bigint(11) NOT NULL AUTO_INCREMENT,
                         `product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
                         `total` int(11) DEFAULT NULL COMMENT '总库存',
                         `used` int(11) DEFAULT NULL COMMENT '已用库存',
                         `residue` int(11) DEFAULT NULL COMMENT '剩余库存',
                         PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `seat-storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');

下载业务端的undo-log脚本,并导入业务数据库中

由于我们这里使用的是mysql数据库,需要导入mysql 的undo-log.sql脚本文件

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

将上面undo-log.sql脚本分别导入seat-account,seat-order,seat-storage三个库中。

客户端的配置文件下载及导入

从https://github.com/seata/seata/tree/develop/script/client这个地址中下载客户端的conf配置文件,包括
file.conf和registry.conf

新建三个业务系统,分别为:

  • spring-cloud-alibaba-nacos-discovery-client
  • spring-cloud-alibaba-nacos-discovery-server
  • spring-cloud-alibaba-nacos-discovery-server2

如下图所示:

业务系统1
111
业务系统2
在这里插入图片描述
业务系统3
在这里插入图片描述
将file.conf和registry.conf文件分别导入上面三个业务系统工程的resources目录下,由于我们采用nacos作为注册中心,将所有三个业务系统中的registry.conf文件中的registry下的type改成nacos,以及config下的type改成nacos,如下图

在这里插入图片描述
在这里插入图片描述
注意:nacos的username和password默认都是nacos。

将各业务系统resource目录下的file.conf文件中的service下的vgroupMapping.xxx="default"这个配置的xxx的名字要和后面将要操作的服务端seata的config.txt配置导入nacos时的名字要一致,如下图:

在这里插入图片描述

修改每个业务系统的application.yml配置文件

spring-cloud-alibaba-nacos-discovery-client的application.yml配置

server:
  port: 9011

spring:
  profiles:
    active: dev
  application:
    name: cloud-discovery-client
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.0.104:3306/seat-account
    username: nacos
    password: nacos

logging:
  level:
    io:
      seata: debug
mybatis:
  mapperLocations: classpath:mapper/*.xml


##暴露actuator的所有端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
  ##health endpoint是否必须显示全部细节。默认情况下, /actuator/health 是公开的,并且不显示细节
  endpoint:
    health:
      show-details: ALWAYS
  health:
    elasticsearch:
      enabled: false

info:
  version: 1.0.0

spring-cloud-alibaba-nacos-discovery-server的application.yml配置

server:
  port: 9012

spring:
  profiles:
    active: dev
  application:
    name: cloud-discovery-server
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
#      config:
#        server-addr: 127.0.0.1:8848
#        file-extension: yaml
#        prefix: cloud-discovery-server  #prefix 默认为 spring.application.name 的值, Nacos中的dataId 的格式是${prefix}-${spring.profiles.active}.${file-extension}
#        group: GROUP1
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.0.104:3306/seat-order
    username: nacos
    password: nacos
feign:
  hystrix:
    enabled: false
logging:
  level:
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml


##暴露actuator的所有端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
  ##health endpoint是否必须显示全部细节。默认情况下, /actuator/health 是公开的,并且不显示细节
  endpoint:
    health:
      show-details: ALWAYS
  health:
    elasticsearch:
      enabled: false

info:
  version: 1.0.0

ker-ver: 1.2

spring-cloud-alibaba-nacos-discovery-server2的application.yml配置

server:
  port: 9013

spring:
  profiles:
    active: dev
  application:
    name: cloud-discovery-server2
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    #      config:
    #        server-addr: 127.0.0.1:8848
    #        file-extension: yaml
    #        prefix: cloud-discovery-server  #prefix 默认为 spring.application.name 的值, Nacos中的dataId 的格式是${prefix}-${spring.profiles.active}.${file-extension}
    #        group: GROUP1
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.0.104:3306/seat-storage
    username: nacos
    password: nacos
logging:
  level:
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml


##暴露actuator的所有端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
  ##health endpoint是否必须显示全部细节。默认情况下, /actuator/health 是公开的,并且不显示细节
  endpoint:
    health:
      show-details: ALWAYS
  health:
    elasticsearch:
      enabled: false

info:
  version: 1.0.0

ker-ver: 1.2

修改pom.xml文件

三个系统的pom.xml中的配置内容是一样的,区别是项目的名字不一样,依赖都是一样的,配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.8.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.kernel</groupId>
	<artifactId>spring-cloud-alibaba-nacos-discovery-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-cloud-alibaba-nacos-discovery-server</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
		<spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version>
		<mysql-connector-java.version>5.1.37</mysql-connector-java.version>
		<mybatis-spring-boot-starter.version>2.0.0</mybatis-spring-boot-starter.version>
		<druid-spring-boot-starter.version>1.1.10</druid-spring-boot-starter.version>
		<lombok.version>1.18.8</lombok.version>
		<seata.version>1.3.0</seata.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
		</dependency>

		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
		</dependency>

		<!--feign-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>

		<!--seata-->
		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
			<exclusions>
				<exclusion>
					<artifactId>seata-all</artifactId>
					<groupId>io.seata</groupId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>io.seata</groupId>
			<artifactId>seata-all</artifactId>
			<version>${seata.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql-connector-java.version}</version>
		</dependency>

		<!--mybatis-->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>${mybatis-spring-boot-starter.version}</version>
		</dependency>

		<!--druid-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>${druid-spring-boot-starter.version}</version>
		</dependency>

		<!--lombok-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
		</dependency>

	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>

			<dependency>
				<groupId>com.alibaba.cloud</groupId>
				<artifactId>spring-cloud-alibaba-dependencies</artifactId>
				<version>${spring-cloud-alibaba.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

修改pom完毕后,刷新maven工程,导入完毕。

seata配置文件导入nacos配置中心

config.txt内容如下,需要修改的地方注释出来了

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default #my_test_tx_group这个名字要与项目工程下registry.conf里的名字一致
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=db                     #这里要修改成db,用数据库来保存
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver   #mysql8.0的话这里的名字要改成com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.0.104:3306/seata?useUnicode=true #修改地址
store.db.user=nacos   #修改用户名
store.db.password=nacos  #修改密码
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

将之前下载好的config.txt配置文件放置于seata的根目录下,如下图所示:
在这里插入图片描述

通过地址:https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh,将nacos-config.sh文件下载下来后,在nacos-config.sh文件所在目录打开git bash here 执行nacos-config.sh
输入命令 :sh nacos-config.sh -h 127.0.0.1
在这里插入图片描述
出现这些则表示成功

登录nacos就能看见添加的配置信息
在这里插入图片描述

修改服务端seata配置文件

打开seata/conf下的file.conf

将mode修改为mode=“db” 和自己的数据库信息
在这里插入图片描述
在这里插入图片描述
启动seata-server
成功后nacos服务列表会显示服务信息

在这里插入图片描述

编写业务代码

编写account业务代码(spring-cloud-alibaba-nacos-discovery-client)

在这里插入图片描述
上图中圈起来的是新增业务代码,其中config/DataSourceProxyConfig和config/MyBatisConfig为三个业务系统都使用的通用代码,如下

DataSourceProxyConfig类代码:

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * Create by lenovo
 * Date 2020/8/26 20:33
 * Description
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }
}

MyBatisConfig类代码:

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

/**
 * Create by lenovo
 * Date 2020/8/26 20:35
 * Description
 */
@Configuration
@MapperScan({"com.kernel.scanc.dao"})  #这里要注意每个系统的包路径不一样,要改
public class MyBatisConfig {
}

启动类SpringCloudAlibabaNacosClientApplication

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SpringCloudAlibabaNacosClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudAlibabaNacosClientApplication.class, args);
    }

}

AccountController类

import com.kernel.scanc.domain.CommonResult;
import com.kernel.scanc.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@RequestMapping("/account")
public class AccountController {

    @Autowired
    private AccountService accountService;

    /**
     * 扣减账户余额
     */
    @RequestMapping("/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        accountService.decrease(userId, money);
        return new CommonResult("扣减账户余额成功!",200);
    }
}

AccountDao类

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;

@Repository
public interface AccountDao {

    /**
     * 扣减账户余额
     */
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

Account类

import lombok.Data;

import java.math.BigDecimal;

@Data
public class Account {

    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 总额度
     */
    private BigDecimal total;

    /**
     * 已用额度
     */
    private BigDecimal used;

    /**
     * 剩余额度
     */
    private BigDecimal residue;
}

CommonResult类

public class CommonResult<T> {
    private T data;
    private String message;
    private Integer code;

    public CommonResult() {
    }

    public CommonResult(T data, String message, Integer code) {
        this.data = data;
        this.message = message;
        this.code = code;
    }

    public CommonResult(String message, Integer code) {
        this(null, message, code);
    }

    public CommonResult(T data) {
        this(data, "操作成功", 200);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

AccountService类

import java.math.BigDecimal;

public interface AccountService {

    /**
     * 扣减账户余额
     * @param userId 用户id
     * @param money 金额
     */
    void decrease(Long userId, BigDecimal money);
}

AccountServiceImpl类

import com.kernel.scanc.dao.AccountDao;
import com.kernel.scanc.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * Create by lenovo
 * Date 2020/8/26 20:38
 * Description
 */
@Service
public class AccountServiceImpl implements AccountService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);
    @Autowired
    private AccountDao accountDao;

    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, BigDecimal money) {
        LOGGER.info("------->account-service中扣减账户余额开始");
        //模拟超时异常,全局事务回滚
//        try {
//            Thread.sleep(30*1000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        accountDao.decrease(userId,money);
        LOGGER.info("------->account-service中扣减账户余额结束");
    }
}

AccountMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.kernel.scanc.dao.AccountDao">
    <resultMap id="BaseResultMap" type="com.kernel.scanc.domain.Account">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="DECIMAL"/>
        <result column="used" property="used" jdbcType="DECIMAL"/>
        <result column="residue" property="residue" jdbcType="DECIMAL"/>
    </resultMap>
    <update id="decrease">
        UPDATE account
        SET residue = residue - #{money},
            used    = used + #{money}
        WHERE user_id = #{userId};
    </update>
</mapper>

bootstrap.yml

spring:
  profiles:
    active: dev
  application:
    name: cloud-discovery-client
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        prefix: cloud-discovery-client  #prefix 默认为 spring.application.name 的值, Nacos中的dataId 的格式是${prefix}-${spring.profiles.active}.${file-extension}
        group: GROUP1

编写order业务系统代码(spring-cloud-alibaba-nacos-discovery-server)

在这里插入图片描述

启动类SpringCloudAlibabaNacosDiscoveryServerApplication

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SpringCloudAlibabaNacosDiscoveryServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudAlibabaNacosDiscoveryServerApplication.class, args);
    }

}

OrderController类

import com.kernel.scands.domain.CommonResult;
import com.kernel.scands.domain.Order;
import com.kernel.scands.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 创建订单
     */
    @GetMapping("/create")
    public CommonResult create(Order order) {
        orderService.create(order);
        return new CommonResult("订单创建成功!", 200);
    }
}

OrderDao类

import com.kernel.scands.domain.Order;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;


@Repository
public interface OrderDao {

    /**
     * 创建订单
     */
    void create(Order order);

    /**
     * 修改订单金额
     */
    void update(@Param("userId") Long userId, @Param("status") Integer status);
}

CommonResult类

public class CommonResult<T> {
    private T data;
    private String message;
    private Integer code;

    public CommonResult() {
    }

    public CommonResult(T data, String message, Integer code) {
        this.data = data;
        this.message = message;
        this.code = code;
    }

    public CommonResult(String message, Integer code) {
        this(null, message, code);
    }

    public CommonResult(T data) {
        this(data, "操作成功", 200);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

Order类

import lombok.Data;

import java.math.BigDecimal;

@Data
public class Order {

    private Long id;

    private Long userId;

    private Long productId;

    private Integer count;

    private BigDecimal money;

    /**
     * 订单状态:0:创建中;1:已完结
     */
    private Integer status;

}

AccountService类

import com.kernel.scands.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "cloud-discovery-client")
public interface AccountService {

    /**
     * 扣减账户余额
     */
    @RequestMapping("/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

StorageService类

import com.kernel.scands.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "cloud-discovery-server2")
public interface StorageService {

    /**
     * 扣减库存
     */
    @GetMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

OrderService类

import com.kernel.scands.domain.Order;

public interface OrderService {

    /**
     * 创建订单
     */
    void create(Order order);
}

OrderServiceImpl类

import com.kernel.scands.dao.OrderDao;
import com.kernel.scands.domain.Order;
import com.kernel.scands.service.AccountService;
import com.kernel.scands.service.OrderService;
import com.kernel.scands.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

/**
 * Create by lenovo
 * Date 2020/8/26 21:00
 * Description
 */
@Service
public class OrderServiceImpl implements OrderService {

    private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);

    //服务提供者 项目名称 spring.application.name
//    public static final String CLOUD_DISCOVERY_CLIENT = "cloud-discovery-client";
//    public static final String CLOUD_DISCOVERY_SERVER2 = "cloud-discovery-server2";

    /**
     * 在spring cloud commons中提供了大量的与服务治理相关的抽象接口,包括DiscoveryClient、LoadBalancerClient等。
     * 从LoadBalancerClient接口的命名中,是一个负载均衡客户端的抽象定义
     */
//    @Autowired
//    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private OrderDao orderDao;
    @Autowired
    private StorageService storageService;
    @Autowired
    private AccountService accountService;


    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     */
    @Override
    @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
    public void create(Order order) {
        LOGGER.info("------->下单开始");
        //本应用创建订单
        orderDao.create(order);

        //远程调用库存服务扣减库存
        LOGGER.info("------->order-service中扣减库存开始");
//        ServiceInstance serviceInstance = loadBalancerClient.choose(CLOUD_DISCOVERY_SERVER2);
//        System.out.println("ServiceInstance :" + serviceInstance);
//        String url = serviceInstance.getUri() + "/storage/decrease?productId=" + order.getProductId() + "&count=" + order.getCount();
//        RestTemplate restTemplate = new RestTemplate();
//        String result = restTemplate.getForObject(url, String.class);    //spring cloud的服务消费方式主要为:RestTemplate,WebClient,OpenFeign

        storageService.decrease(order.getProductId(),order.getCount());
        LOGGER.info("------->order-service中扣减库存结束");

        //远程调用账户服务扣减余额
//        LOGGER.info("------->order-service中扣减余额开始");
//        ServiceInstance serviceInstance2 = loadBalancerClient.choose(CLOUD_DISCOVERY_CLIENT);
//        System.out.println("ServiceInstance2 :" + serviceInstance2);
//        String url2 = serviceInstance2.getUri() + "/account/decrease?userId=" + order.getUserId() + "&money=" + order.getMoney();
//        RestTemplate restTemplate2 = new RestTemplate();
//        String result2 = restTemplate2.getForObject(url2, String.class);    //spring cloud的服务消费方式主要为:RestTemplate,WebClient,OpenFeign

        accountService.decrease(order.getUserId(),order.getMoney());
        LOGGER.info("------->order-service中扣减余额结束");

        //修改订单状态为已完成
        LOGGER.info("------->order-service中修改订单状态开始");
        orderDao.update(order.getUserId(),0);
        LOGGER.info("------->order-service中修改订单状态结束");

        LOGGER.info("------->下单结束");
    }
}

OrderMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.kernel.scands.dao.OrderDao">
    <resultMap id="BaseResultMap" type="com.kernel.scands.domain.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>
    <insert id="create">
        INSERT INTO `order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)
        VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0);
    </insert>

    <update id="update">
        UPDATE `order`
        SET status = 1
        WHERE user_id = #{userId}
          AND status = #{status};
    </update>
</mapper>

bootstrap.yml

spring:
  profiles:
    active: dev
  application:
    name: cloud-discovery-server
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        prefix: cloud-discovery-server  #prefix 默认为 spring.application.name 的值, Nacos中的dataId 的格式是${prefix}-${spring.profiles.active}.${file-extension}
        group: GROUP1

编写storage业务系统代码(spring-cloud-alibaba-nacos-discovery-server2)

在这里插入图片描述
启动类(SpringCloudAlibabaNacosDiscoveryServer2Application)

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SpringCloudAlibabaNacosDiscoveryServer2Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudAlibabaNacosDiscoveryServer2Application.class, args);
    }

}

StorageController类

import com.kernel.scands2.domain.CommonResult;
import com.kernel.scands2.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Create by lenovo
 * Date 2020/8/26 21:48
 * Description
 */
@RestController
@RequestMapping("/storage")
public class StorageController {

    @Autowired
    private StorageService storageService;

    /**
     * 扣减库存
     */
    @RequestMapping("/decrease")
    public CommonResult decrease(Long productId, Integer count) {
        storageService.decrease(productId, count);
        return new CommonResult("扣减库存成功!",200);
    }
}

StorageDao类

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface StorageDao {

    /**
     * 扣减库存
     */
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

StorageService类

public interface StorageService {
    /**
     * 扣减库存
     */
    void decrease(Long productId, Integer count);
}

StorageServiceImpl类

import com.kernel.scands2.dao.StorageDao;
import com.kernel.scands2.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class StorageServiceImpl implements StorageService {

    private static final Logger LOGGER = LoggerFactory.getLogger(com.kernel.scands2.service.impl.StorageServiceImpl.class);

    @Autowired
    private StorageDao storageDao;

    /**
     * 扣减库存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        LOGGER.info("------->storage-service中扣减库存开始");
        storageDao.decrease(productId,count);
        LOGGER.info("------->storage-service中扣减库存结束");
    }
}

StorageMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.kernel.scands2.dao.StorageDao">
    <resultMap id="BaseResultMap" type="com.kernel.scands2.domain.Storage">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="INTEGER"/>
        <result column="used" property="used" jdbcType="INTEGER"/>
        <result column="residue" property="residue" jdbcType="INTEGER"/>
    </resultMap>
    <update id="decrease">
        UPDATE storage
        SET used    = used + #{count},
            residue = residue - #{count}
        WHERE product_id = #{productId}
    </update>
</mapper>

bootstrap.yml

spring:
  profiles:
    active: dev
  application:
    name: cloud-discovery-server2
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        prefix: cloud-discovery-server2  #prefix 默认为 spring.application.name 的值, Nacos中的dataId 的格式是${prefix}-${spring.profiles.active}.${file-extension}
        group: GROUP2

最后分别启动三个微服务系统后,打开浏览器,访问地址测试:http://localhost:9012/order/create?id=1&userId=1&productId=1&count=2&money=10,默认各个系统是可以正常完成业务并数据入库的,
如果在其中一个系统的业务层中人为制造一个异常,再次运行,就会出现全局回滚的现象了。

至此,一个简单的基于单nacos和seata实例的分布式事务解决方案就实现了,下次我们来看一下如果基于nacos集群和seata集群来实现生产环境上的分布式事务解决方案!敬请期待!!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值