@[TOC]
一、前言
至此,微服务系列正式开启分布式事务篇;
捎带一提,seata官方给的案例是真的******
,版本之间的差异并未说明,据悉官方案例属于政治任务!在开启案例之前,博主和网友们踩过一些坑,具体见文章:
- can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;
- Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
- Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版
- 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)
本文基于AT模式 +搭建SpringCloud 集成 Seata + Nacos 实现分布式事务的案例;
版本信息如下:
<properties>
<spring-boot.version>2.4.2</spring-boot.version>
<spring-cloud.version>2020.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<mysql.version>8.0.22</mysql.version>
</properties>
二、Seata简介
Seata 是一款开源的分布式事务解决方案,全称:Simple extensiable autonomous transaction architecture;意思是:简单的、可扩展的、自治的事务架构。Seata致力于提供高性能和简单易用的分布式事务服务;Seata 为用户提供了 AT、TCC、SAGA 和 XA 四种分布式事务模式;
1> AT模式:
- 提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB 和 MariaDB;
2> TCC 模式:
- 支持 TCC 模式并可与 AT 混用,灵活度更高;
3> SAGA 模式:
- 为长事务提供有效的解决方案,提供编排式与注解式(开发中);
4> XA 模式:
- 支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle、TiDB和MariaDB;
三个角色
1> TC (Transaction Coordinator) - 事务协调者
- 维护全局和分支事务的状态,驱动全局事务提交或回滚。
2> TM (Transaction Manager) - 事务管理器
- 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
3> RM (Resource Manager) - 资源管理器
- 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
案例中三个角色的交互
三、SpringCloud 集成Seata(注册和配置均采用Nacos)
本文基于AT模式 +搭建SpringCloud 集成 Seata + Nacos 实现分布式事务的案例;
整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
用例为用户购买商品的业务逻辑,整个业务逻辑由3个微服务提供支持,其中:
- 仓储服务(stock-service):对给定的商品扣除仓储数量。
- 订单服务(order-service):根据采购需求创建订单。
- 帐户服务(account-service):从用户帐户中扣除余额。
此外,trade-center为交易中心,是处理用户请求的入口;
0、业务架构图
1、MySQL数据库信息
必须要使用具有InnoDB引擎的MySQL;也就是说数据库的引擎要支持事务,因为AT模式底层是依赖数据库事务实现的分布式事务。
在案例中,仓储服务(stock-service)、订单服务(order-service)、帐户服务(account-service) 这三个服务对应三个数据库,为了方便测试,我们只创建一个数据库并配置3个数据源。
0)一键执行所有SQL
1> 案例中seata-client相关的所有业务库、业务表、undo_log表创建SQL;
2> seata-server保存数据的表;
#Account
DROP SCHEMA IF EXISTS seata_account;
CREATE SCHEMA seata_account;
USE seata_account;
CREATE TABLE `account_tbl`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(255) DEFAULT NULL,
`money` INT(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
INSERT INTO account_tbl (id, user_id, money)
VALUES (1, '1001', 10000);
INSERT INTO account_tbl (id, user_id, money)
VALUES (2, '1002', 10000);
CREATE TABLE `undo_log`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
#Order
DROP SCHEMA IF EXISTS seata_order;
CREATE SCHEMA seata_order;
USE seata_order;
CREATE TABLE `order_tbl`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(255) DEFAULT NULL,
`commodity_code` VARCHAR(255) DEFAULT NULL,
`count` INT(11) DEFAULT '0',
`money` INT(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE `undo_log`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
#Stock
DROP SCHEMA IF EXISTS seata_stock;
CREATE SCHEMA seata_stock;
USE seata_stock;
CREATE TABLE `stock_tbl`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`commodity_code` VARCHAR(255) DEFAULT NULL,
`count` INT(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
INSERT INTO stock_tbl (id, commodity_code, count)
VALUES (1, '2001', 1000);
CREATE TABLE `undo_log`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- the table to store GlobalSession data
DROP SCHEMA IF EXISTS seata_server;
CREATE SCHEMA seata_server;
USE seata_server;
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(128),
`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;
1)undo_log 事务回滚日志表
SEATA AT 模式需要 undo_log
表,用于事务回滚使用。所以上面三个服务每个服务都要有一个undo_log
表。表结构如下:
建表SQL:
CREATE TABLE `undo_log`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
undo_log表的结构是从哪里找的?为什么它是这个?看了一些文章并没有说这个,本文简要说明一下;
undo_log表结构从哪里找?
1> 在GitHub中找到seata的源码,选择响应版本的代码分支:
注意源码最上层目录结构下有一个script
文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。
2> 进入目录/script/client/at/db
,找到mysql.sql文件,其就是我们需要的创建undo_log表结构的SQL:
2)仓储服务(stock-service)业务表
1> 表结构:
2> 建表SQL:
DROP SCHEMA IF EXISTS seata_stock;
CREATE SCHEMA seata_stock;
USE seata_stock;
CREATE TABLE `stock_tbl`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`commodity_code` VARCHAR(255) DEFAULT NULL,
`count` INT(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
INSERT INTO stock_tbl (id, commodity_code, count)
VALUES (1, '2001', 1000);
3)订单服务(order-service)业务表
1> 表结构:
2> 建表SQL:
DROP SCHEMA IF EXISTS seata_order;
CREATE SCHEMA seata_order;
USE seata_order;
CREATE TABLE `order_tbl`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(255) DEFAULT NULL,
`commodity_code` VARCHAR(255) DEFAULT NULL,
`count` INT(11) DEFAULT '0',
`money` INT(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
4)账户服务(account)业务表
1> 表结构:
2> 建表SQL:
DROP SCHEMA IF EXISTS seata_account;
CREATE SCHEMA seata_account;
USE seata_account;
CREATE TABLE `account_tbl`
(
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(255) DEFAULT NULL,
`money` INT(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
INSERT INTO account_tbl (id, user_id, money)
VALUES (1, '1001', 10000);
INSERT INTO account_tbl (id, user_id, money)
VALUES (2, '1002', 10000);
5)seata-server表结构
当seata-server配置信息中 store配置的是db时,
需要使用到三张表:global_table
(记录全局事务)、branch_table
(记录分支事务)、lock_table
(记录全局锁);
当然数据库表和表名是可以改变的,只需要在store配置中对应上即可。
1> global_table
1> 表结构:
2> 建表SQL:
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;
2> branch_table
1> 表结构:
2> 建表SQL:
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;
3> lock_table
1> 表结构:
2> 建表SQL:
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`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;
seata-server相关表结构从哪里找?
1> 在GitHub中找到seata的源码,选择响应版本的代码分支:
注意源码最上层目录结构下有一个script
文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。
2> 进入目录/script/server/db
,找到mysql.sql文件,其就是我们需要的创建seata-server相关的表结构的SQL:
数据库表结构处理完之后,看一下seata-server需要如何下载、配置、启动?
2、seata-server
1)seata-server配置
将下载下来的seata-server-1.3.0.tar.gz
压缩包解压,解压后的文件目录为:seata-server-1.3.0
;
# 进入seata-server主目录
cd seata-server-1.3.0
# 进入seata-server配置目录
cd conf
修改registry.conf
和file.conf
配置文件,内容如下:
1> registry.conf
seata-server的配置中心和注册中心均采用nacos的方式,
设置type为nacos、设置serverAddr为nacos节点地址,serverAddr不能带‘http://’前缀;
特别注意:registry.nacos.cluster(注册到的nacos集群名称)要和事务分组名称一致,即:下面config.txt文件中service.vgroupMapping.saint-trade-tx-group
对应的值seata-server-sh
;
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "seata-server-sh"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
}
}
为什么要用service.vgroupMapping.saint-trade-tx-group
对应的值作为seata-server注册到nacos注册中心的集群名?
- ==因为采用nacos作为注册中心时,seata-client是通过服务名(
seata-server
) + 集群名(seata-server-sh
)+ 分组(SEATA_GROUP
)去nacos注册中心中找到所有的seata-server实例地址;== 如果不设置相应的集群名,则在从Nacos获取服务实例列表时会报错:
i.s.c.r.netty.NettyClientChannelManager : no available service 'seata-server-sh' found, please make sure registry config correct
这里涉及到Nacos的源码,感兴趣的可以看博主的另一篇文章:图文详述Nacos服务发现源码分析。
2> seata-server相关配置上传到Nacos配置中心
方法一:
(1) 在GitHub中找到seata的源码,选择响应版本的代码分支:
注意源码最上层目录结构下有一个script
文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。
(2)进入/script/config-center
目录,其中config.txt
是上传到配置中心的配置所在的文件,nacos
目录中是将nacos上传到Nacos配置中心的脚本(nacos-config.sh
、nacos-config.py
):
(3)将上面的config.txt文件复制到seata-server的根目录,将nacos-config.sh复制到seat-server的bin目录下:
注意:config.txt文件必须要在nacos-config.sh的上一级目录(../
)
- 因为在
nacos-config.sh
脚本中,是通过cat $(dirname "$PWD")
的方式去查找config.txt所在是目录;而cat $(dirname "$PWD")
返回的正是nacos-config.sh
脚本所在目录的上级目录;
(4)修改config.txt文件:
重点关注以下配置:
- 事务分组名称的配置、事务分组名称对应的Seata Server服务实例列表;
注意,高版本的vgroupMapping后面的如: saint-trade-tx-group 不能定义为 saint_trade_tx_group - Seata Server数据存放的模式,如果是模式db,配置db信息;
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.saint-trade-tx-group=seata-server-sh
service.seata-server-sh.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
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
store.db.user=root
store.db.password=123456
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
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
(5)执行nacos-config.sh
脚本将配置上传到nacos配置中心:
方法二:通过dataId配置
- 从seata的
v1.4.2
版本开始,已支持从一个Nacos dataId中获取所有配置信息,只需要额外添加一个dataId配置项。 - 首先在nacos新建配置,此处dataId为seataServer.properties,配置内容参考
https://github.com/seata/seata/tree/develop/script/config-center
的config.txt并按需修改(见方法一的修改)保存; 在client参考如下配置进行修改:
seata: config: type: nacos nacos: server-addr: 127.0.0.1:8848 group : "SEATA_GROUP" namespace: "" dataId: "seataServer.properties" username: "nacos" password: "nacos"
2)启动seata-server
进入seata-server-1.3.0/bin
目录,然后运行seata-server.sh
shell脚本;
cd ../bin
sh seata-server.sh
seata-server.sh脚本中的参数
具体参考官方文档:https://github.com/seata/seata/tree/develop/script/config-center;
v1.5.0开始支持的 Nacos相关启动脚本参数介绍:
3、seata-client
一般Spring Cloud集成seata大致会分为5步:
1> 第一步:
添加Spring Cloud Alibaba 依赖管理工具和 Seata 依赖;
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
另外,spring-cloud-starter-alibaba-seata
依赖中seata相关的只依赖了spring-cloud-alibaba-seata
,所以在项目中添加spring-cloud-starter-alibaba-seata
和spring-cloud-alibaba-seata
是一样的;
2> 第二步:
- 在application.yml配置文件中设置Seata相关配置,(不再需要file.conf文件,registry.conf也不需要)
3> 第三步:
- 注入数据源;Seata 通过代理数据源的方式实现分支事务;MyBatis 和 JPA 都需要注入
io.seata.rm.datasource.DataSourceProxy
, 不同的是,MyBatis 还需要额外注入org.apache.ibatis.session.SqlSessionFactory
;
4> 第四步:
- 在业务相关的数据库中添加 undo_log 表,用于保存需要回滚的数据;
5> 第五步:
- 在业务的发起方的方法上使用
@GlobalTransactional
开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务;
上面提到整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
下面从这四个Module以具体的代码来看,这五步是如何体现在代码中的;
0)最上层父项目spring-cloud-center的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.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modules>
<module>trade-center</module>
<module>stock-service</module>
<module>order-service</module>
<module>account-service</module>
</modules>
<groupId>com.saint</groupId>
<artifactId>transaction-seata</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>transaction-seata</name>
<description>transaction-seata</description>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.4.2</spring-boot.version>
<spring-cloud.version>2020.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<druid.version>1.2.8</druid.version>
<mysql.version>8.0.22</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.10</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合spring cloud alibaba-->
<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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
关于Spring-cloud和SpringBoot的版本对应关系,参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系。
1)account-service
account-service整体代码结构目录如下:
整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;application.yml配置文件;
1> 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>transaction-seata</artifactId>
<groupId>com.saint</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<version>0.0.1-SNAPSHOT</version>
<groupId>com.saint</groupId>
<artifactId>account-service</artifactId>
<name>account-service</name>
<dependencies>
<!--protostuff序列化依赖-->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
</project>
2> DataSourceConfig
package com.saint.account.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
/**
* 数据源配置
*
* @author Saint
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
*
* @param druidDataSource The DruidDataSource
* @return The default datasource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
3> AccountController
package com.saint.account.controller;
import com.saint.account.service.AccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
/**
* @author Saint
*/
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class AccountController {
private final AccountService accountService;
@RequestMapping("/debit")
public Boolean debit(String userId, BigDecimal money) {
accountService.debit(userId, money);
return true;
}
}
4> Account
package com.saint.account.entity;
import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;
/**
* @author Saint
*/
@Entity
@Table(name = "account_tbl")
@DynamicUpdate
@DynamicInsert
@Data
public class Account {
@Id
private Long id;
private String userId;
private BigDecimal money;
}
5> AccountDAO
package com.saint.account.repository;
import com.saint.account.entity.Account;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author Saint
*/
public interface AccountDAO extends JpaRepository<Account, Long> {
Account findByUserId(String userId);
}
6> AccountService
package com.saint.account.service;
import com.saint.account.entity.Account;
import com.saint.account.repository.AccountDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
* @author Saint
*/
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class AccountService {
private final AccountDAO accountDAO;
private static final String ERROR_USER_ID = "1002";
@Transactional(rollbackFor = Exception.class)
public void debit(String userId, BigDecimal num) {
Account account = accountDAO.findByUserId(userId);
account.setMoney(account.getMoney().subtract(num));
accountDAO.save(account);
if (ERROR_USER_ID.equals(userId)) {
throw new RuntimeException("account branch exception");
}
}
}
7> AccountApplication
package com.saint.account;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* @author Saint
*/
@SpringBootApplication
@EnableFeignClients
@EnableJpaRepositories
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
8> application.yml
server:
port: 9031
spring:
application:
name: account-service
cloud:
nacos:
# 这里配置的是当前服务所要注册到的Nacos地址
discovery:
server-addr: 127.0.0.1:8848
group: TRADE_GROUP
datasource:
url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
seata:
# 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
# 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
tx-service-group: saint-trade-tx-group
# 采用nacos作为配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
# 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
registry:
type: nacos
nacos:
# seata-server的应用名称
application: seata-server
# seata-server注册到的nacos服务地址
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
2)order-service
order-service整体代码结构目录如下:
整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个FeignClient、一个Dao、一个Service、一个启动类;application.yml配置文件;
1> 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>transaction-seata</artifactId>
<groupId>com.saint</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<version>0.0.1-SNAPSHOT</version>
<groupId>com.saint</groupId>
<artifactId>order-service</artifactId>
<name>order-service</name>
<dependencies>
<!--protostuff序列化依赖-->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
</project>
2> DataSourceConfig
JPA的数据源配置和account-service中的一样;
3> OrderController
package com.saint.order.controller;
import com.saint.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Saint
*/
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderController {
private final OrderService orderService;
@GetMapping("/create")
public Boolean create(String userId, String commodityCode, Integer count) {
orderService.create(userId, commodityCode, count);
return true;
}
}
4> Order
package com.saint.order.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
import java.math.BigDecimal;
/**
* @author Saint
*/
@Entity
@Table(name = "order_tbl")
@DynamicUpdate
@DynamicInsert
@NoArgsConstructor
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private String userId;
@Column(name = "commodity_code")
private String commodityCode;
@Column(name = "money")
private BigDecimal money;
@Column(name = "count")
private Integer count;
}
5> AccountFeignClient
AccountFeignClient用于通过OpenFeign调用account-service;
package com.saint.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
/**
* @author Saint
*/
@FeignClient(name = "account-service")
public interface AccountFeignClient {
@GetMapping("/debit")
Boolean debit(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money);
}
6> OrderDAO
package com.saint.order.repository;
import com.saint.order.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author Saint
*/
public interface OrderDAO extends JpaRepository<Order, Long> {
}
7> OrderService
package com.saint.order.service;
import com.saint.order.entity.Order;
import com.saint.order.feign.AccountFeignClient;
import com.saint.order.repository.OrderDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
* @author Saint
*/
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderService {
private final AccountFeignClient accountFeignClient;
private final OrderDAO orderDAO;
@Transactional
public void create(String userId, String commodityCode, Integer count) {
BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
Order order = new Order();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setCount(count);
order.setMoney(orderMoney);
orderDAO.save(order);
accountFeignClient.debit(userId, orderMoney);
}
}
8> OrderApplication
package com.saint.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* @author Saint
*/
@SpringBootApplication
@EnableFeignClients
@EnableJpaRepositories
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
9> application.yml
server:
port: 9021
spring:
application:
name: order-service
cloud:
nacos:
# 这里配置的是当前服务所要注册到的Nacos地址
discovery:
server-addr: 127.0.0.1:8848
group: TRADE_GROUP
datasource:
url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
seata:
# 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
# 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
tx-service-group: saint-trade-tx-group
# 采用nacos作为配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
# 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
registry:
type: nacos
nacos:
# seata-server的应用名称
application: seata-server
# seata-server注册到的nacos服务地址
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
3)stock-service
stck-service整体代码结构目录如下:
整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;application.yml配置文件;
1> 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>transaction-seata</artifactId>
<groupId>com.saint</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<version>0.0.1-SNAPSHOT</version>
<groupId>com.saint</groupId>
<artifactId>stock-service</artifactId>
<name>stock-service</name>
<dependencies>
<!--protostuff序列化依赖-->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
</project>
2> DataSourceConfig
JPA的数据源配置和account-service中的一样;
3> StockController
package com.saint.stock.controller;
import com.saint.stock.service.StockService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Saint
*/
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockController {
private final StockService stockService;
@GetMapping(path = "/deduct")
public Boolean deduct(String commodityCode, Integer count) {
stockService.deduct(commodityCode, count);
return true;
}
}
4> Stock
package com.saint.stock.entity;
import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* @author Saint
*/
@Entity
@Table(name = "stock_tbl")
@DynamicUpdate
@DynamicInsert
@Data
public class Stock {
@Id
private Long id;
private String commodityCode;
private Integer count;
}
5> StockDAO
package com.saint.stock.repository;
import com.saint.stock.entity.Stock;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @author Saint
*/
public interface StockDAO extends JpaRepository<Stock, String> {
Stock findByCommodityCode(String commodityCode);
}
6> StockService
package com.saint.stock.service;
import com.saint.stock.entity.Stock;
import com.saint.stock.repository.StockDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author Saint
*/
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockService {
private final StockDAO stockDAO;
@Transactional
public void deduct(String commodityCode, int count) {
Stock stock = stockDAO.findByCommodityCode(commodityCode);
stock.setCount(stock.getCount() - count);
stockDAO.save(stock);
}
}
7> StockApplication
package com.saint.stock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* @author Saint
*/
@SpringBootApplication
@EnableJpaRepositories
public class StockApplication {
public static void main(String[] args) {
SpringApplication.run(StockApplication.class, args);
}
}
8> application.yml
server:
port: 9011
spring:
application:
name: stock-service
cloud:
nacos:
# 这里配置的是当前服务所要注册到的Nacos地址
discovery:
server-addr: 127.0.0.1:8848
group: TRADE_GROUP
datasource:
url: jdbc:mysql://127.0.0.1:3306/seata_stock?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
seata:
# 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
# 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
tx-service-group: saint-trade-tx-group
# 采用nacos作为配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
# 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
registry:
type: nacos
nacos:
# seata-server的应用名称
application: seata-server
# seata-server注册到的nacos服务地址
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
4)trade-center
trade-center整体代码结构目录如下:
整体包括:pom.xml、一个Controller、一个entity、两个FeignClient、一个Service、一个启动类;application.yml配置文件;
1> 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>transaction-seata</artifactId>
<groupId>com.saint</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>trade-center</artifactId>
<version>0.0.1-SNAPSHOT</version>
<groupId>com.saint</groupId>
<name>trade-center</name>
<dependencies>
<!--protostuff序列化依赖-->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!--openFeign-->
<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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
2> TradeController
package com.saint.trade.controller;
import com.saint.trade.service.TradeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Saint
*/
@RestController
public class TradeController {
@Autowired
private TradeService businessService;
/**
* 购买下单,模拟全局事务提交
*
* @return
*/
@RequestMapping("/purchase/commit")
public Boolean purchaseCommit() {
businessService.purchase("1001", "2001", 1);
return true;
}
/**
* 购买下单,模拟全局事务回滚
*
* @return
*/
@RequestMapping("/purchase/rollback")
public Boolean purchaseRollback() {
try {
businessService.purchase("1002", "2001", 1);
// businessService.failToPurchase("1001", "2001", 1);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
3> OrderFeignClient
trade-center通过OpenFeign调用order-service;
package com.saint.trade.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author Saint
*/
@FeignClient(name = "order-service")
public interface OrderFeignClient {
@GetMapping("/create")
void create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode,
@RequestParam("count") Integer count);
}
4> StockFeignClient
trade-center通过OpenFeign调用stock-service;
package com.saint.trade.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author Saint
*/
@FeignClient(name = "stock-service")
public interface StockFeignClient {
@GetMapping("/deduct")
void deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);
}
5> TradeService
TradeService中通过@GlobalTransactional
开启分布式事务;
package com.saint.trade.service;
import com.saint.trade.feign.OrderFeignClient;
import com.saint.trade.feign.StockFeignClient;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @author Saint
*/
@Service
@RequiredArgsConstructor
public class TradeService {
private final StockFeignClient stockFeignClient;
private final OrderFeignClient orderFeignClient;
/**
* 减库存,下订单
*
* @param userId
* @param commodityCode
* @param orderCount
*/
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
stockFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
}
/**
* 减库存,下订单(有异常)
*
* @param userId
* @param commodityCode
* @param orderCount
*/
@GlobalTransactional
public void failToPurchase(String userId, String commodityCode, int orderCount) {
stockFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
throw new RuntimeException("Error!");
}
}
6> TradeApplication
package com.saint.trade;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author Saint
*/
@SpringBootApplication
@EnableFeignClients
public class TradeApplication {
public static void main(String[] args) {
SpringApplication.run(TradeApplication.class, args);
}
}
7> application.yml
server:
port: 9001
spring:
application:
name: trade-center
cloud:
nacos:
# 这里配置的是当前服务所要注册到的Nacos地址
discovery:
server-addr: 127.0.0.1:8848
group: TRADE_GROUP
# cloud:
# alibaba:
# seata:
# tx-service-group: saint-trade-tx-group
seata:
# 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
# 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
tx-service-group: saint-trade-tx-group
# 采用nacos作为配置中心
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
# 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
registry:
type: nacos
nacos:
# seata-server的应用名称
application: seata-server
# seata-server注册到的nacos服务地址
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
4、AT模式分布式事务效果演示
分别启动trade-center、stock-service、order-service、account-service;都启动成功之后,Nacos服务信息如下:
四个服务的分组名称均为我们通过spring.cloud.nacos.discovery.group
属性配置的;
1)请求正常
分布式事务成功,模拟正常下单、扣库存
请求访问:http://127.0.0.1:9001/purchase/commit
2)请求异常
分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚
请求访问:http://127.0.0.1:9001/purchase/rollback;
四、总结和后续
当前文章讲述了Spring Cloud + Nacos + Seata + OpenFeign + JPA实现分布式事务的案例。
如果你运行不起来,私信我。
官方案例文档:https://github.com/Saint9768/seata-samples/tree/master/springcloud-nacos-seata;明人不说暗话,里面全是坑,基本是搭不起来的;
下一篇文章正式开启Seata源码分析,期间会穿插聊一下Seata的TCC模式实现案例;