前言
由于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
业务系统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集群来实现生产环境上的分布式事务解决方案!敬请期待!!!