1. Seata简介
2. 下载Seata服务器
3. 示例(在项目中集成Seata)
处理分布式(微服务)架构的事务问题。
例(用户在某电商系统下单购买了一件商品,电商系统会执行下4步)
1. 调用订单服务创建订单数据;
2. 调用库存服务扣减库存;
3. 调用账户服务扣减账户金额;
4. 最后调用订单服务修改订单状态;
在分布式微服务架构中,几乎所有业务操作都需要多个服务协作才能完成。对于单个服务的数据一致性可以交由其自身数据库事务来保证,但对于整个分布式微服务架构,各服务自身的事务特性无法保证全局数据的正确性和一致性(要么全部成功,要么全部失败),这需要通过分布式事务框架(如:Seata)来解决。
/*
分布式系统(distributed system)由多台计算机和通信的软件组件通过计算机网络连接(本地网络或广域网)组成。分布式系统是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。分布式系统可以应用在不同的平台上如:Pc、工作站、局域网和广域网上等。
分布式的优点
1. 可靠性(容错)
一台服务器的系统崩溃并不影响到其余的服务器。
2. 可扩展性
可以根据需要增加更多的机器。
3. 资源共享
共享数据(银行,预订系统)。
4. 灵活性
很容易安装,实施和调试新的服务。
5. 更快的速度
可以有多台计算机的计算能力,使得它比其他系统有更快的处理速度。
6. 开放系统
本地或者远程都可以访问到该服务。
7. 更高的性能
相较于集中式计算机网络集群可以提供更高的性能(及更好的性价比)。
分布式的缺点
1. 故障排除
故障排除和诊断问题。
2. 软件
更少的软件支持。
3. 网络
网络基础设施的问题,包括:传输问题,高负载,信息丢失等。
4. 安全性
开放系统的特性让分布式计算系统存在着数据的安全性和共享的风险等问题。
*/
- Seata简介
Seata(分布式事务框架)
由阿里巴巴和蚂蚁金服共同开发的开源分布式事务解决方案。
发展历程
2014年发布TXC(Taobao Transaction Constructor),为内部应用提供分布式事务服务。
2016年TXC改造为GTS(Global Transaction Service),作为阿里云的云上分布式事务解决方案,为外部客户提供服务。
2019年(基于TXC和GTS)创建了开源项目Fescar(Fast & EaSy Commit And Rollback, FESCAR)分布式事务解决方案。Fescar被重命名为了Seata(simple extensiable autonomous transaction architecture)。
分布式事务相关概念
1. 事务:由一组操作构成的可靠、独立的工作单元,事务具备ACID特性(即:原子性、一致性、隔离性、持久性)。
2. 本地事务:本地事务由本地资源管理器(通常指数据库管理系统DBMS,如:MySQL、Oracle等)管理,严格支持ACID特性。本地事务不具备分布式事务的处理能力,隔离的最小单位受限于资源管理器,即本地事务只能对自己数据库的操作进行控制,对于其他数据库的操作则无能为力。
3. 全局事务/分布式事务:协调其管辖的各个分支事务达成一致(要么一起成功提交,要么一起失败回滚)。
4. 分支事务:受全局事务管辖和协调的本地事务。
Seata对分布式事务的协调和控制,是通过XID和3个相互协作的核心组件(TC、TM、RM)实现的。
TC以Seata服务器形式独立部署,TM、RM则以SeataClient形式集成在微服务中运行。
1. XID(全局事务的唯一标识)
可以在服务的调用链路中传递,绑定到服务的事务上下文中。
2. TC(Transaction Coordinator事务协调器)
事务的协调者(即Seata服务器),负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
3. TM(Transaction Manager事务管理器)
事务的发起者,负责定义全局事务的范围,并根据TC维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
4. RM(Resource Manager资源管理器)
资源的管理者(各服务使用的数据库),负责管理分支事务上的资源,向TC注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。
工作流程:
1. TM向TC申请开启一个全局事务,全局事务创建成功后,TC会针对这个全局事务生成一个全局唯一的 XID并返回给TM;
2. XID通过服务的调用链传递到其他服务;
3. RM向TC注册一个分支事务,并将其纳入XID对应全局事务的管辖;
4. TM根据TC收集的各个分支事务的执行结果,向TC发起全局事务提交或回滚决议;
5. TC调度XID下管辖的所有分支事务完成提交或回滚操作。
Seata提供了4种事务模式(快速有效地对分布式事务进行控制):AT(使用最多,无业务入侵,使开发员更专注于业务逻辑开发)、TCC、SAGA、XA 。
AT模式的前提
项目必须是使用JDBC访问(支持本地ACID事务特性的)关系型数据库(如:MySQL、Oracle)的JAVA应用。
此外,还需要针对业务中涉及的各个数据库表,分别创建一个UNDO_LOG(回滚日志)表(不同数据库创建UNDO_LOG表时会略有不同)。例(MySQL下创建UNDO_LOG表):
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;
AT模式的工作机制(两个阶段)
假设某数据库中存在一张名为webset的表:
1. id bigint(20) 主键
2. name varchar(255)
3. url varchar(255)
在某次分支事务中,需要在webset表中执行以下操作
update webset set url = 'www.baidu.com' where name = '百度';
一阶段:
1. 获取SQL的基本信息:Seata拦截并解析业务SQL,得到SQL的操作类型(UPDATE)、表名(webset)、判断条件(where name = '百度')等相关信息。
2. 查询前镜像:根据得到的业务 SQL 信息,生成“前镜像查询语句”。
select id,name,url from webset where name='百度';
执行“前镜像查询语句”,得到即将执行操作的数据,并将其保存为“前镜像数据(beforeImage)”。
3. 执行业务SQL(update webset set url = 'www.baidu.com' where name = '百度';)。
4. 查询后镜像:根据“前镜像数据”的主键(id : 1),生成“后镜像查询语句”。
select id,name,url from webset where id= 1;
执行“后镜像查询语句”,得到执行业务操作后的数据,并将其保存为“后镜像数据(afterImage)”。
5. 插入回滚日志:将前后镜像数据和业务SQL的信息组成一条回滚日志记录,插入到UNDO_LOG表中。
6. 注册分支事务,生成行锁:在这次业务操作的本地事务提交前,RM会向TC注册分支事务,并针对主键id为1的记录生成行锁。
以上所有操作均在同一个数据库事务内完成,可以保证一阶段的操作的原子性。
7. 本地事务提交:将业务数据的更新和前面生成的UNDO_LOG一并提交。
8. 上报执行结果:将本地事务提交的结果上报给TC。
二阶段:提交
当所有的RM都将自己分支事务的提交结果上报给TC后,TM根据TC收集的各个分支事务的执行结果,来决定向TC发起全局事务的提交或回滚。
若所有分支事务都执行成功,TM向TC发起全局事务的提交,并批量删除各个RM保存的UNDO_LOG记录和行锁;否则全局事务回滚。
二阶段:回滚
若全局事务中的任何一个分支事务失败,则TM向TC发起全局事务的回滚,并开启一个本地事务,执行如下操作。
1. 查找UNDO_LOG记录:通过XID和分支事务 ID(Branch ID) 查找所有的UNDO_LOG记录。
2. 数据校验:将UNDO_LOG中的后镜像数据(afterImage)与当前数据进行比较,如果有不同,则说明数据被当前全局事务之外的动作所修改,需要人工对这些数据进行处理。
3. 生成回滚语句:根据UNDO_LOG中的前镜像(beforeImage)和业务 SQL 的相关信息生成回滚语句:update webset set url= 'www.baidu.com' where id = 1;
4. 还原数据:执行回滚语句,并将前镜像数据、后镜像数据以及行锁删除。
5. 提交事务:提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给TC。
- 下载Seata服务器
从github下载Seata服务器(压缩包+SourceCode)
Seata服务器的目录说明:
1. bin目录(存放:可执行命令)
2. conf目录(存放:配置文件)
3. lib目录(存放:依赖Jar包)
4. logs目录(存放:日志)
- Seata配置:配置中心(存放配置文件,客户端可以根据需要获取指定的配置文件)
支持:
file (本地文件,包含 conf、properties、yml等配置文件)、nacos、consul、apollo、etcd、zookeeper等多种配置中心。
Seata整合Nacos配置中心
1. SeataServer(Seata服务器)配置
将SeataServer的config目录下的registry.conf文件中的config.type配置方式改为Nacos,并对Nacos配置中心的相关信息进行配置:
config {
#配置方式
type = "nacos"
nacos {
#nacos服务器地址
serverAddr = "127.0.0.1:1111"
#配置中心的命名空间
namespace = ""
#配置中心所在的分组
group = "SEATA_GROUP"
#Nacos配置中心的用户名
username = "nacos"
#Nacos配置中心的密码
password = "nacos"
}
}
2. SeataClient(即微服务架构中的服务)配置
1. 在pom.xml文件添加依赖
io.seata
seata-spring-boot-starter
最新版
com.alibaba.nacos
nacos-client
1.2.0及以上版本
如果是SpringCloud项目中,通常只需要添加
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2. 在application.yml配置文件对Nacos配置中心进行配置
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:1111 # Nacos 配置中心的地址
group : "SEATA_GROUP" #分组
namespace: ""
username: "nacos" #Nacos 配置中心的用于名
password: "nacos" #Nacos 配置中心的密码
3. 上传配置到Nacos配置中心
1. 根据需要修改SeataServer源码/script/config-center目录中的config.txt。
2. 跳转到SeataServer源码/script/config-center/nacos目录(确保 Nacos 服务器已启动)
Linux:直接终端执行如下命令:
Windows:右键鼠标选择Git Bush Here,并在弹出的 Git 命令窗口中执行如下命令:
sh nacos-config.sh -h 127.0.0.1 -p 1111 -g SEATA_GROUP -u nacos -w nacos命令。
说明:
-h:Nacos 的 host,默认取值为 localhost
-p:端口号,默认取值为 8848
-g:Nacos 配置的分组,默认取值为 SEATA_GROUP
-u:Nacos 用户名
-w:Nacos 密码
4. 验证Nacos配置中心
启动NacosServer,登陆Nacos控制台(http://localhost:1111/nacos/)查看配置列表。
- Seata配置:服务注册中心(存放:服务与服务地址的映射关系)
支持:
file (直连)、eureka、consul、nacos、etcd、zookeeper、sofa、redis等多种服务注册中心。
Seata整合Nacos注册中心
1. SeataServer配置
将SeataServer的config目录下的registry.conf文件的registry.type注册方式改为Nacos,并对Nacos注册中心的相关信息进行配置:
registry {
#注册方式
type = "nacos"
nacos {
application = "seata-server"
# 修改 nacos 注册中心的地址
serverAddr = "127.0.0.1:1111"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
2. SeataClient配置
1. 在pom.xml文件添加依赖(同上)
2. 在application.yml配置文件中对Nacos注册中心进行配置
seata:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:1111 # Nacos 注册中心的地址
group : "SEATA_GROUP" #分组
namespace: ""
username: "nacos" #Nacos 注册中心的用户名
password: "nacos" # Nacos 注册中心的密码
3. 验证Nacos注册中心
启动NacosServer、SeataServer,登录Nacos控制台查看服务列表。
- Seata事务分组
Seata通过事务分组查找获取TC服务,流程如下:
1. 在应用中配置事务分组。
2. 应用通过配置中心去查找配置:service.vgroupMapping.{事务分组} 获取TC集群名。
3. 获得集群名称后,应用通过一定的前后缀 + 集群名称去构造服务名。
4. 得到服务名后,去注册中心去拉取服务列表,获得后端真实的 TC 服务列表。
1. SeataServer配置
在SeataServer的config目录下的registry.conf文件的nacos中添加cluster = "com.sst.cx"设置TC集群名。
2. SeataClient配置
在application.yml中配置
spring:
alibaba:
seata:
tx-service-group: service-order-group #事务分组名,默认值为:服务名-fescar-service-group
seata:
registry:
type: nacos #从 Nacos 获取 TC 服务
nacos:
server-addr: 127.0.0.1:1111
config:
type: nacos #使用 Nacos 作为配置中心
nacos:
server-addr: 127.0.0.1:1111
3. 上传配置到Nacos
修改SeataServer源码/script/config-center目录中的config.txt(事务分组名service-order-group、TC集群名com.sst.cx):
service.vgroupMapping.service-order-group=com.sst.cx
4. 获取事务分组
1. 依次启动Nacos集群、SeataServer、SeataClient。
2. SeataClient在启动时,会根据application.yml中的spring.cloud.alibaba.seata.tx-service-group获取事务分组名:service-order-group。
5. 获取TC集群名
使用事务分组名“service-order-group”拼接成“service.vgroupMapping.service-order-group”,并从 Nacos配置中心获取该配置的取值(TC集群名):com.sst.cx。
6. 查找TC服务
根据TC集群名、Nacos注册中心的地址(server-addr)以及命名空间(namespace),在Nacos注册中心找到真实的TC服务列表。
- 启动SeataServer(使用db模式)配置:服务注册中心、配置中心、事务分组
SeataServer的3种存储模式(store.mode):
1. file(文件存储模式)默认
单机模式,全局事务的会话信息在内存中读写,并持久化本地文件 root.data,性能较高。
2. db(数据库存储模式)
高可用模式,全局事务会话信息通过数据库共享,性能较低。
需要建数据库表。
3. redis(缓存处处模式)
Seata Server 1.3 及以上版本支持该模式,性能较高,但存在事务信息丢失风险。
需要配置 redis 持久化配置。
1. 建表
在db模式下,需要针对全局事务的会话信息创建以下3张数据库表(在seata数据库下)。
1. 全局事务表,对应的表为:global_table
2. 分支事务表,对应的表为:branch_table
3. 全局锁表,对应的表为:lock_table
-- 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(256),
`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;
2. 配置SeataServer
将SeataServer的conf目录下的registry.conf文件的registry.type服务注册方式和config.type配置方式改为Nacos。
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:1111"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:1111"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
3. 将Seata配置上传到Nacos
修改SeataServer源码/script/config-center目录下的config.txt,并上传到Nacos。
#将 Seata Server 的存储模式修改为 db
store.mode=db
# 数据库驱动
store.db.driverClassName=com.mysql.cj.jdbc.Driver
# 数据库 url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false&characterEncoding=UTF-8&useUnicode=true&serverTimezone=UTC
# 数据库的用户名
store.db.user=root
# 数据库的密码
store.db.password=12345678
# 自定义事务分组
service.vgroupMapping.service-order-group=default
service.vgroupMapping.service-storage-group=default
service.vgroupMapping.service-account-group=default
4. 启动Seata Server
Windows:双击SeataServer的bin目录下的seata-server.bat启动脚本。
Linux:sh seata-server.sh
- 示例(在业务中集成Seata)
电商系统中用户下单购买一件商品,涉及到3个微服务(分别使用各自的数据库)
1. Order(订单服务):创建和修改订单。
2. Storage(库存服务):对指定的商品扣除仓库库存。
3. Account(账户服务) :从用户帐户中扣除商品金额。
服务调用步骤如下:
1. 调用Order服务,创建一条订单数据,订单状态为“未完成”;
2. 调用Storage服务,扣减商品库存;
3. 调用Account服务,从用户账户中扣除商品金额;
4. 调用Order服务,将订单状态修改为“已完成”。
===》1. 创建订单(Order)服务
sql
create database seata_order character set utf8 collate utf8_general_ci;
use seata_order;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint DEFAULT NULL COMMENT '用户id',
`product_id` bigint DEFAULT NULL COMMENT '产品id',
`count` int DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
`status` int DEFAULT NULL COMMENT '订单状态:0:未完成;1:已完结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) 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 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 DEFAULT CHARSET=utf8 ;
创建spring-cloud-alibaba-seata-order-8005子项目
1. 修改pom.xml文件
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-loadbalancer
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
com.sst.cx
spring-cloud-alibaba-api
${project.version}
junit
junit
mysql
mysql-connector-java
ch.qos.logback
logback-core
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
org.springframework.boot
spring-boot-starter-actuator
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.mybatis.generator
mybatis-generator-maven-plugin
1.4.0
src/main/resources/mybatis-generator/generatorConfig.xml
true
true
mysql
mysql-connector-java
8.0.19
org.mybatis.generator
mybatis-generator-core
1.4.0
org.springframework.boot
spring-boot-maven-plugin
2. 创建bootstrap.yml配置文件(类路径resources目录下)
spring:
cloud:
## Nacos认证信息
nacos:
config:
username: nacos
password: nacos
context-path: /nacos
server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. 创建application.yml配置文件(类路径resources目录下)
spring:
application:
name: spring-cloud-alibaba-seata-order-8005 #服务名
#数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动
name: defaultDataSource
url: jdbc:mysql://localhost:3306/seata_order?serverTimezone=UTC #数据库连接地址
username: root #数据库的用户名
password: 12345678 #数据库密码
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:1111 #nacos 服务器地址
namespace: public #nacos 命名空间
username:
password:
sentinel:
transport:
dashboard: 127.0.0.1:8080 #Sentinel 控制台地址
port: 8719
alibaba:
seata:
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-order-group
server:
port: 8005 #端口
seata:
application-id: ${spring.application.name}
#自定义服务群组,该值必须与 Nacos 配置中的 service.vgroupMapping.{my-service-group}=default 中的 {my-service-group}相同
tx-service-group: service-order-group
service:
grouplist:
#Seata 服务器地址
seata-server: 127.0.0.1:8091
# Seata 的注册方式为 nacos
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:1111
# Seata 的配置中心为 nacos
config:
type: nacos
nacos:
server-addr: 127.0.0.1:1111
feign:
sentinel:
enabled: true #开启 OpenFeign 功能
management:
endpoints:
web:
exposure:
include: "*"
#### MyBatis 配置 ######
mybatis:
# 指定 mapper.xml 的位置
mapper-locations: classpath:mybatis/mapper/*.xml
#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
type-aliases-package: com.sst.cx.domain
configuration:
#默认开启驼峰命名法,可以不用设置该属性
map-underscore-to-camel-case: true
4. 创建Order.java
package com.sst.cx.domain;
import java.math.BigDecimal;
public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public BigDecimal getMoney() {
return money;
}
public void setMoney(BigDecimal money) {
this.money = money;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
5. 创建OrderMapper.java
package com.sst.cx.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.sst.cx.domain.Order;
@Mapper
public interface OrderMapper {
int deleteByPrimaryKey(Long id);
int insert(Order record);
int create(Order order);
int insertSelective(Order record);
// 修改订单状态,从零改为1
void update(@Param("userId") Long userId, @Param("status") Integer status);
Order selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(Order record);
int updateByPrimaryKey(Order record);
}
6. 创建OrderMapper.xml(resouces/mybatis/mapper 目录下)
id
, user_id, product_id, count, money, status
delete
from t_order
where id = #{id,jdbcType=BIGINT}
insert into t_order (id, user_id, product_id,
count, money, status)
values (#{id,jdbcType=BIGINT}, #{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},
#{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER})
insert into t_order
id,
user_id,
product_id,
count,
money,
status,
#{id,jdbcType=BIGINT},
#{userId,jdbcType=BIGINT},
#{productId,jdbcType=BIGINT},
#{count,jdbcType=INTEGER},
#{money,jdbcType=DECIMAL},
#{status,jdbcType=INTEGER},
insert into t_order (user_id, product_id,
count, money, status)
values (#{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},
#{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER})
update t_order
user_id = #{userId,jdbcType=BIGINT},
product_id = #{productId,jdbcType=BIGINT},
count = #{count,jdbcType=INTEGER},
money = #{money,jdbcType=DECIMAL},
status = #{status,jdbcType=INTEGER},
where id = #{id,jdbcType=BIGINT}
update t_order
set user_id = #{userId,jdbcType=BIGINT},
product_id = #{productId,jdbcType=BIGINT},
count = #{count,jdbcType=INTEGER},
money = #{money,jdbcType=DECIMAL},
status = #{status,jdbcType=INTEGER}
where id = #{id,jdbcType=BIGINT}
update t_order
set status = 1
where user_id = #{userId}
and status = #{status};
7. 创建OrderService.java
package com.sst.cx.service;
import com.sst.cx.domain.*;
public interface OrderService {
// 创建订单数据
CommonResult create(Order order);
}
8. 创建StorageService.java
package com.sst.cx.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.sst.cx.domain.CommonResult;
@FeignClient(value = "spring-cloud-alibaba-seata-storage-8006")
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
9. 创建AccountService.java
package com.sst.cx.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
import com.sst.cx.domain.CommonResult;
@FeignClient(value = "spring-cloud-alibaba-seata-account-8007")
public interface AccountService {
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
10. 创建OrderServiceImpl.java
package com.sst.cx.service.impl;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import com.sst.cx.mapper.OrderMapper;
import com.sst.cx.domain.*;
import com.sst.cx.service.*;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改订单状态
*/
@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public CommonResult create(Order order) {
log.info("----->开始新建订单");
// 1 新建订单
order.setUserId(new Long(1));
order.setStatus(0);
orderMapper.create(order);
// 2 扣减库存
log.info("----->订单服务开始调用库存服务,开始扣减库存");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->订单微服务开始调用库存,扣减库存结束");
// 3 扣减账户
log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->订单微服务开始调用账户,账户扣减商品金额结束");
// 4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderMapper.update(order.getUserId(), 0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了------->");
return new CommonResult(200, "订单创建成功");
}
}
11. 创建OrderController.java
package com.sst.cx.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import com.sst.cx.service.OrderService;
import com.sst.cx.domain.*;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/order/create/{productId}/{count}/{money}")
public CommonResult create(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count
, @PathVariable("money") BigDecimal money) {
Order order = new Order();
order.setProductId(Integer.valueOf(productId).longValue());
order.setCount(count);
order.setMoney(money);
return orderService.create(order);
}
}
12. 给主启动类添加@EnableDiscoveryClient、@EnableFeignClients注解
package com.sst.cx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "com.sst.cx")
public class SpringCloudAlibabaSeataOrder8005Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaSeataOrder8005Application.class, args);
}
}
===》2. 搭建库存(Storage)服务
sql
create database seata_storage character set utf8 collate utf8_general_ci;
use seata_storage;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint DEFAULT NULL COMMENT '产品id',
`total` int DEFAULT NULL COMMENT '总库存',
`used` int DEFAULT NULL COMMENT '已用库存',
`residue` int DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `t_storage` VALUES ('1', '1', '100', '0', '100');
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) 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 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 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
创建spring-cloud-alibaba-seata-storage-8006子项目
1. 修改pom.xml文件
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-loadbalancer
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
com.sst.cx
spring-cloud-alibaba-api
${project.version}
junit
junit
mysql
mysql-connector-java
ch.qos.logback
logback-core
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
org.springframework.boot
spring-boot-starter-actuator
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.mybatis.generator
mybatis-generator-maven-plugin
1.4.0
src/main/resources/mybatis-generator/generatorConfig.xml
true
true
mysql
mysql-connector-java
8.0.19
org.mybatis.generator
mybatis-generator-core
1.4.0
org.springframework.boot
spring-boot-maven-plugin
2. 创建bootstrap.yml配置文件(类路径resources目录下)
spring:
cloud:
## Nacos认证信息
nacos:
config:
username: nacos
password: nacos
context-path: /nacos
server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. 创建application.yml配置文件(类路径resources目录下)
spring:
application:
name: spring-cloud-alibaba-seata-storage-8006
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
name: defaultDataSource
url: jdbc:mysql://localhost:3306/seata_storage?serverTimezone=UTC
username: root
password: 12345678
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:1111
namespace: public
username:
password:
sentinel:
transport:
dashboard: 127.0.0.1:8080
port: 8719
alibaba:
seata:
tx-service-group: service-storage-group
server:
port: 8006
seata:
application-id: ${spring.application.name}
tx-service-group: service-storage-group
service:
grouplist:
seata-server: 127.0.0.1:8091
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:1111
config:
type: nacos
nacos:
server-addr: 127.0.0.1:1111
feign:
sentinel:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
##### MyBatis 配置 #####
mybatis:
# 指定 mapper.xml 的位置
mapper-locations: classpath:mybatis/mapper/*.xml
#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
type-aliases-package: com.sst.cx.domain
configuration:
#默认开启驼峰命名法,可以不用设置该属性
map-underscore-to-camel-case: true
4. 创建Storage.java
package com.sst.cx.domain;
public class Storage {
private Long id;
private Long productId;
private Integer total;
private Integer used;
private Integer residue;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
public Integer getUsed() {
return used;
}
public void setUsed(Integer used) {
this.used = used;
}
public Integer getResidue() {
return residue;
}
public void setResidue(Integer residue) {
this.residue = residue;
}
}
5. 创建StorageMapper.java
package com.sst.cx.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.sst.cx.domain.Storage;
@Mapper
public interface StorageMapper {
Storage selectByProductId(Long productId);
int decrease(Storage record);
}
6. 创建StorageMapper.xml(resouces/mybatis/mapper 目录下)
id
, product_id, total, used, residue
update t_storage
total = #{total,jdbcType=INTEGER},
used = #{used,jdbcType=INTEGER},
residue = #{residue,jdbcType=INTEGER},
where product_id = #{productId,jdbcType=BIGINT}
7. 创建StorageService.java
package com.sst.cx.service;
public interface StorageService {
int decrease(Long productId, Integer count);
}
8. 创建StorageServiceImpl.java
package com.sst.cx.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import com.sst.cx.domain.Storage;
import com.sst.cx.mapper.StorageMapper;
import com.sst.cx.service.StorageService;
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
StorageMapper storageMapper;
@Override
public int decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
log.info("------->storage-service 开始查询商品是否存在");
Storage storage = storageMapper.selectByProductId(productId);
if (storage != null && storage.getResidue().intValue() >= count.intValue()) {
Storage storage2 = new Storage();
storage2.setProductId(productId);
storage.setUsed(storage.getUsed() + count);
storage.setResidue(storage.getTotal().intValue() - storage.getUsed());
int decrease = storageMapper.decrease(storage);
log.info("------->storage-service 扣减库存成功");
return decrease;
} else {
log.info("------->storage-service 库存不足,开始回滚!");
throw new RuntimeException("库存不足,扣减库存失败!");
}
}
}
9. 创建StorageController.java
package com.sst.cx.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import com.sst.cx.domain.CommonResult;
import com.sst.cx.service.StorageService;
@RestController
@Slf4j
public class StorageController {
@Resource
private StorageService storageService;
@Value("${server.port}")
private String serverPort;
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {
int decrease = storageService.decrease(productId, count);
CommonResult result;
if (decrease > 0) {
result = new CommonResult(200, "from mysql,serverPort: " + serverPort, decrease);
} else {
result = new CommonResult(505, "from mysql,serverPort: " + serverPort, "库存扣减失败");
}
return result;
}
}
10. 给主启动类添加@EnableDiscoveryClient、@EnableFeignClients注解
package com.sst.cx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "com.sst.cx")
public class SpringCloudAlibabaSeataStorage8006Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaSeataStorage8006Application.class, args);
}
}
===》3. 搭建账户(Account)服务
sql
create database seata_account character set utf8 collate utf8_general_ci;
use seata_account;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint 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 `t_account` VALUES ('1', '1', '1000', '0', '1000');
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) 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 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 DEFAULT CHARSET=utf8;
创建spring-cloud-alibaba-seata-account-8007 子项目
1. 修改pom.xml文件
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-loadbalancer
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
com.sst.cx
spring-cloud-alibaba-api
${project.version}
junit
junit
mysql
mysql-connector-java
ch.qos.logback
logback-core
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.2.0
org.springframework.boot
spring-boot-starter-actuator
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.mybatis.generator
mybatis-generator-maven-plugin
1.4.0
src/main/resources/mybatis-generator/generatorConfig.xml
true
true
mysql
mysql-connector-java
8.0.19
org.mybatis.generator
mybatis-generator-core
1.4.0
org.springframework.boot
spring-boot-maven-plugin
2. 创建bootstrap.yml配置文件(类路径resources目录下)
spring:
cloud:
## Nacos认证信息
nacos:
config:
username: nacos
password: nacos
context-path: /nacos
server-addr: 127.0.0.1:1111 # 设置配置中心服务端地址
namespace: # Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
3. 创建application.yml配置文件(类路径resources目录下)
spring:
application:
name: spring-cloud-alibaba-seata-account-8007
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
name: defaultDataSource
url: jdbc:mysql://localhost:3306/seata_account?serverTimezone=UTC
username: root
password: 12345678
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:1111
namespace: public
username:
password:
sentinel:
transport:
dashboard: 127.0.0.1:8080
port: 8719
alibaba:
seata:
tx-service-group: service-account-group
server:
port: 8007
seata:
application-id: ${spring.application.name}
tx-service-group: service-account-group
service:
grouplist:
seata-server: 127.0.0.1:8091
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:1111
config:
type: nacos
nacos:
server-addr: 127.0.0.1:1111
feign:
sentinel:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
##### MyBatis 配置 ###
mybatis:
# 指定 mapper.xml 的位置
mapper-locations: classpath:mybatis/mapper/*.xml
#扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
type-aliases-package: com.sst.cx.domain
configuration:
#默认开启驼峰命名法,可以不用设置该属性
map-underscore-to-camel-case: true
4. 创建Account.java
package com.sst.cx.domain;
import java.math.BigDecimal;
public class Account {
private Long id;
private Long userId;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public BigDecimal getTotal() {
return total;
}
public void setTotal(BigDecimal total) {
this.total = total;
}
public BigDecimal getUsed() {
return used;
}
public void setUsed(BigDecimal used) {
this.used = used;
}
public BigDecimal getResidue() {
return residue;
}
public void setResidue(BigDecimal residue) {
this.residue = residue;
}
}
5. 创建AccountMapper.java
package com.sst.cx.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
import com.sst.cx.domain.Account;
@Mapper
public interface AccountMapper {
Account selectByUserId(Long userId);
int decrease(Long userId, BigDecimal money);
}
6. 创建AccountMapper.xml(resouces/mybatis/mapper 目录下)
id
, user_id, total, used, residue
UPDATE t_account
SET residue = residue - #{money},
used = used + #{money}
WHERE user_id = #{userId};
7. 创建AccountService.java
package com.sst.cx.service;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
public interface AccountService {
// 扣减账户余额
int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
8. 创建AccountServiceImpl.java
package com.sst.cx.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.math.BigDecimal;
import com.sst.cx.domain.Account;
import com.sst.cx.mapper.AccountMapper;
import com.sst.cx.service.AccountService;
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
AccountMapper accountMapper;
@Override
public int decrease(Long userId, BigDecimal money) {
log.info("------->account-service 开始查询账户余额");
Account account = accountMapper.selectByUserId(userId);
log.info("------->account-service 账户余额查询完成," + account);
if (account != null && account.getResidue().intValue() >= money.intValue()) {
log.info("------->account-service 开始从账户余额中扣钱!");
int decrease = accountMapper.decrease(userId, money);
log.info("------->account-service 从账户余额中扣钱完成");
return decrease;
} else {
log.info("账户余额不足,开始回滚!");
throw new RuntimeException("账户余额不足!");
}
}
}
9. 创建AccountController.java
package com.sst.cx.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import javax.annotation.Resource;
import com.sst.cx.domain.CommonResult;
import com.sst.cx.service.AccountService;
@RestController
@Slf4j
public class AccountController {
@Resource
private AccountService accountService;
@Value("${server.port}")
private String serverPort;
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) {
int decrease = accountService.decrease(userId, money);
CommonResult result;
if (decrease > 0) {
result = new CommonResult(200, "from mysql,serverPort: " + serverPort, decrease);
} else {
result = new CommonResult(505, "from mysql,serverPort: " + serverPort, "账户扣减失败");
}
return result;
}
}
10. 给主启动类添加@EnableDiscoveryClient、@EnableFeignClients注解
package com.sst.cx;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(scanBasePackages = "com.sst.cx")
public class SpringCloudAlibabaSeataAccount8007Application {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaSeataAccount8007Application.class, args);
}
}
11. 依次启动NacosServer集群、SeataServer、spring-cloud-alibaba-seata-order-8005、spring-cloud-alibaba-seata-storage-8006、spring-cloud-alibaba-seata-account-8007,当控制台出现register success日志时,说明该服务已经成功连接上SeataServer(TC)。
在浏览器中访问http://localhost:8005/order/create/1/2/20,结果如下:
{"code":200,"message":"订单创建成功","data":null}
执行以下SQL语句
1. 查询seata_order数据库中的t_order表:SELECT * FROM seata_order.t_order;
2. 查询seata_storage数据库中的t_storage表:SELECT * FROM seata_storage.t_storage;
3. 查询seata_account数据库中的t_account表:SELECT * FROM seata_account.t_account;
可以看到,已经创建一条订单数据,且订单状态(status)已修改为“已完成”;商品库存已经扣减 2 件,仓库中商品储量从 100 件减少到了 98 件;账户余额已经扣减,金额从 1000 元减少到了 980 元。
在浏览器中访问http://localhost:8005/order/create/1/10/1000,页面会显示异常信息,提示账户余额不足。再次查询t_order表、t_storage表、t_account表,从控制台可以看到异常后发生了回滚。
给类/方法添加@GlobalTransactional注解来实现分布式事务的开启、管理和控制(当调用该方法时TM会先向TC注册全局事务,由TC生成一个全局唯一的XID并返回给TM)。
@GlobalTransactional(name = "com-sst-cx-create-order", rollbackFor = Exception.class)
存疑:不添加@GlobalTransactional注解也开启了全局事务。