至此,微服务系列正式开启分布式事务篇;
捎带一提,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报错原因/解决方案最全汇总版
本文基于AT模式 + File配置/注册搭建SpringCloud 和 Seata的集成案例;
版本信息如下:
<properties>
<spring-boot.version>2.4.2spring-boot.version>
<spring-cloud.version>2020.0.1spring-cloud.version>
<spring-cloud-alibaba.version>2021.1spring-cloud-alibaba.version>
<mysql.version>8.0.22mysql.version>
properties>
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;
官方文档:https://seata.io/zh-cn/docs/overview/what-is-seata.html
1> TC (Transaction Coordinator) - 事务协调者
2> TM (Transaction Manager) - 事务管理器
3> RM (Resource Manager) - 资源管理器
本文基于AT模式 + File配置/注册搭建SpringCloud 和 Seata的集成案例;
整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
用例为用户购买商品的业务逻辑,整个业务逻辑由3个微服务提供支持,其中:
此外,trade-center为交易中心,是处理用户请求的入口;
必须要使用具有InnoDB引擎的MySQL;也就是说数据库的引擎要支持事务,因为AT模式底层是依赖数据库事务实现的分布式事务。
在案例中,仓储服务(stock-service)、订单服务(order-service)、帐户服务(account-service) 这三个服务对应三个数据库,为了方便测试,我们只创建一个数据库并配置3个数据源。
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;
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表的结构是从哪里找的?为什么它是这个?**看了一些文章并没有说这个,本文简要说明一下;
1> 在GitHub中找到seata的源码,选择响应版本的代码分支:
注意源码最上层目录结构下有一个script
文件夹,其中记录了所有我们可能需要的SQL、配置…。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。
2> 进入目录/script/client/at/db
,找到mysql.sql文件,其就是我们需要的创建undo_log表结构的SQL:
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);
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;
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);
当seata-server配置信息中 store配置的是db时,
需要使用到三张表:global_table
(记录全局事务)、branch_table
(记录分支事务)、lock_table
(记录全局锁);
当然数据库表和表名是可以改变的,只需要在store配置中对应上即可。
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;
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;
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;
1> 在GitHub中找到seata的源码,选择响应版本的代码分支:
注意源码最上层目录结构下有一个script
文件夹,其中记录了所有我们可能需要的SQL、配置…。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。
2> 进入目录/script/server/db
,找到mysql.sql文件,其就是我们需要的创建seata-server相关的表结构的SQL:
数据库表结构处理完之后,看一下seata-server需要如何下载、配置、启动?
seata-server下载地址:https://seata.io/zh-cn/blog/download.html,其中binary
选项为seata-server可执行程序,source
为相应版本源码。
将下载下来的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
配置文件,内容如下:
seata-server的配置中心和注册中心均采用file的方式:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
seata-server的配置中心采用file时,具体配置如下:
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata_server"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = false
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThreadPrefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
transaction {
undo.data.validation = true
undo.log.serialization = "jackson"
}
## metrics configuration, only used in server side
metrics {
enabled = false
registryType = "compact"
# multi exporters use comma divided
exporterList = "prometheus"
exporterPrometheusPort = 9898
}
进入seata-server-1.3.0/bin
目录,然后运行seata-server.sh
shell脚本;
cd ../bin
sh seata-server.sh
官方文档介绍:https://github.com/seata/seata/tree/develop/script/config-center;
采用File配置方式时不需要关注,当使用其他配置中心时再关注即可(留坑集成Nacos时处理)。
一般Spring Cloud集成seata大致会分为5步:
1> 第一步:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2021.1version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
另外,spring-cloud-starter-alibaba-seata
依赖中seata相关的只依赖了spring-cloud-alibaba-seata
,所以在项目中添加spring-cloud-starter-alibaba-seata
和spring-cloud-alibaba-seata
是一样的;
2> 第二步:
registry.conf
用于指定 TC 的注册中心和配置文件,默认都是 file; 如果使用其他的注册中心,要求 Seata-Server 也注册到该配置中心上;file.conf
用于指定TC的相关属性;如果使用注册中心也可以将配置添加到配置中心;3> 第三步:
io.seata.rm.datasource.DataSourceProxy
, 不同的是,MyBatis 还需要额外注入 org.apache.ibatis.session.SqlSessionFactory
;4> 第四步:
5> 第五步:
@GlobalTransactional
开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务;上面提到整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
下面从这四个Module以具体的代码来看,这五步是如何体现在代码中的;
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.2version>
<relativePath/>
parent>
<modules>
<module>trade-centermodule>
<module>stock-servicemodule>
<module>order-servicemodule>
<module>account-servicemodule>
modules>
<groupId>com.saintgroupId>
<artifactId>transaction-seataartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>transaction-seataname>
<description>transaction-seatadescription>
<packaging>pompackaging>
<properties>
<java.version>1.8java.version>
<spring-boot.version>2.4.2spring-boot.version>
<spring-cloud.version>2020.0.1spring-cloud.version>
<spring-cloud-alibaba.version>2021.1spring-cloud-alibaba.version>
<druid.version>1.2.8druid.version>
<mysql.version>8.0.22mysql.version>
properties>
<dependencies>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.12.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>2.0.10version>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>${spring-boot.version}version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
关于Spring-cloud和SpringBoot的版本对应关系,参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系。
整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;
<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-seataartifactId>
<groupId>com.saintgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<version>0.0.1-SNAPSHOTversion>
<groupId>com.saintgroupId>
<artifactId>account-serviceartifactId>
<name>account-servicename>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
dependencies>
project>
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);
}
}
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;
}
}
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;
}
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);
}
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");
}
}
}
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);
}
}
server:
port: 9031
spring:
application:
name: stock-service
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
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroupMapping.saint-trade-tx-group = "seata-server-sh"
#only support when registry.type=file, please don't set multiple addresses
seata-server-sh.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1"
namespace = ""
username = ""
password = ""
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
nacos {
serverAddr = "127.0.0.1"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
file {
name = "file.conf"
}
}
整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个FeignClient、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;
<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-seataartifactId>
<groupId>com.saintgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<version>0.0.1-SNAPSHOTversion>
<groupId>com.saintgroupId>
<artifactId>order-serviceartifactId>
<name>order-servicename>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
dependencies>
project>
JPA的数据源配置和account-service中的一样;
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;
}
}
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;
}
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", url = "127.0.0.1:9031")
public interface AccountFeignClient {
@GetMapping("/debit")
Boolean debit(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money);
}
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> {
}
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);
}
}
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);
}
}
server:
port: 9021
spring:
application:
name: stock-service
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
file.conf 和 registry.conf与account-service的一样;
整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;
<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-seataartifactId>
<groupId>com.saintgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<version>0.0.1-SNAPSHOTversion>
<groupId>com.saintgroupId>
<artifactId>stock-serviceartifactId>
<name>stock-servicename>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
dependencies>
project>
JPA的数据源配置和account-service中的一样;
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;
}
}
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;
}
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);
}
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);
}
}
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);
}
}
server:
port: 9011
spring:
application:
name: stock-service
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
file.conf 和 registry.conf和account-service一样;
整体包括:pom.xml、一个Controller、一个entity、两个FeignClient、一个Service、一个启动类;resources目录下三个配置文件:application.yml、file.conf、registry.conf;
<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-seataartifactId>
<groupId>com.saintgroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>trade-centerartifactId>
<version>0.0.1-SNAPSHOTversion>
<groupId>com.saintgroupId>
<name>trade-centername>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
project>
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;
}
}
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", url = "127.0.0.1:9021")
public interface OrderFeignClient {
@GetMapping("/create")
void create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode,
@RequestParam("count") Integer count);
}
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", url = "127.0.0.1:9011")
public interface StockFeignClient {
@GetMapping("/deduct")
void deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);
}
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!");
}
}
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);
}
}
server:
port: 9001
spring:
application:
name: trade-center
seata:
# 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
# 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
tx-service-group: saint-trade-tx-group
file.conf 和 registry.conf 与 account-service的一样;
分别启动trade-center、stock-service、order-service、account-service;
分布式事务成功,模拟正常下单、扣库存
请求访问:http://127.0.0.1:9001/purchase/commit
分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚
请求访问:http://127.0.0.1:9001/purchase/rollback;
当前文章讲述了Spring Cloud + JPA + OpenFeign + Seata实现分布式事务的案例。
下一篇文章为:Spring Cloud 整合Seata + Nacos。