目录
十五、SpringCloud Alibaba Seata处理分布式事务
(2)按照上述 3 库分别建对应的 undo_log 回滚日志表
(2)修改公共 cloud-api-commons(新增库存和账户两个Feign服务接口)
(4)超时异常出错,没有 @GlobalTransactional
(5)超时异常解决,添加 @GlobalTransactional
(6)正常下单,添加 @GlobalTransactional
十五、SpringCloud Alibaba Seata处理分布式事务
1.分布式事务背景
一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题
但是,关系型数据库提供的能力是基于单机事务的(表结构的关系从1:1 → 1:N → N:N)。
一旦遇到分布式事务场景,就需要通过更多其他技术手段来解决问题。
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。
此时每个服务自己内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
所以,我们迫切希望提供一种分布式事务框架,解决微服务架构下的分布式事务问题!
2.Seata简介
(1)介绍
Simple Extensible Autonomous Transaction Architecture:简单可扩展自治事务框架
发展历程:
阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。
- 2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案:
- 2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。
- 2016 年,TXC 在经过产品化改造后,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品。在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。
- 2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。
- 2019 年 fescar(全称fast easy commit and rollback) 被重命名为了seata(simple extensiable autonomous transaction architecture)。TXC、GTS、Fescar 以及 seata 一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。
(2)工作流程
纵观整个分布式事务的管理,就是全局事务 ID 的传递和变更,要让开发者无感知:
Seata 对分布式事务的协调和控制就是 1+3:
1个XID:
XID 是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。
3个概念(TC→TM→RM) :
TC(Transaction Coordinator)事务协调器(唯一):
就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
TM(Transaction Manager)事务管理器(唯一):
就是标注全局 @GlobalTransactional 启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议
RM(Resource Manager)资源管理器(可不唯一):
就是 mysql 数据库本身,可以有多个RM,负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚
执行流程总结:
① TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
② XID 在微服务调用链路的上下文中传播;
③ RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
④ TM 向 TC 发起针对 XID 的全局提交或回滚决议;
⑤ TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
(3)各事务模式
共有四种事务模式,本案例基于 AT 模式学习
(4)下载安装
① 下载网址:Seata Java Download | Apache Seata
② 建库 seata
CREATE DATABASE seata;
USE seata;
③ seata 数据库下建表(我们的存储方式是 db)
网址:incubator-seata/script/server/db/mysql.sql at develop · apache/incubator-seata
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- 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 = utf8mb4;
-- 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),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
结果:
④ 更改配置
先备份 application.yml
再修改 application.yml
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${log.home:${user.home}/logs/seata}
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
namespace:
cluster: default
username: nacos
password: nacos
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
user: root
password: 123456
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
我们配置和注册都通过 nacos,存储方式为 db,可以参照 application-example.yml 文件,按照它配置即可。
⑤ 启动 nacos
命令:startup.cmd -m standalone
⑥ 启动 seata
⑦ 检测是否配置成功
访问地址:http://localhost:8848
访问地址:http://localhost:7091(账号和密码都是 seata)
页面打开正常,说明 seata 启动成功!
3.Seata案例实战-数据库和表准备
前提:nacos,seata 已成功启动
需求说明:
这里我们创建三个服务,一个订单服务,一个库存服务,一个账户服务。
- 当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,
- 再通过远程调用账户服务来扣减用户账户里面的余额,
- 最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
下订单 → 减库存 → 扣余额 → 改(订单)状态
(1)创建 3 个业务数据库DATABASE
seata_order | 存储订单的数据库 |
seata_storage | 存储库存的数据库 |
seata_account | 存储账户信息的数据库 |
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
(2)按照上述 3 库分别建对应的 undo_log 回滚日志表
订单-库存-账户 3 个库下都需要建各自的 undo_log
undo_log 建表SQL:
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT 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(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
也可自行去官网复制: incubator-seata/script/client/at/db/mysql.sql at 2.x · apache/incubator-seata · GitHub
注意:
① AT 模式才需要建 undo_log 表,其他模式不需要
② 三个库分别都要建自己的 undo_log 表,结果如下:
(3)按照上述 3 库分别建对应业务表
t_order 脚本 SQL:
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
t_account 脚本SQL:
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');
SELECT * FROM t_account;
t_storage脚本SQL:
CREATE TABLE t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
最终效果:
4.Seata案例实战-微服务编码落地实现
业务需求:下订单 → 减库存 → 扣余额 → 改(订单)状态
(1)Mybaits一键生成
还记得我们一开始,通过 Mapper 4 进行一键生成吗?
当时创建了一个子模块,用于暂时存储生成的代码,等到业务工程使用的时候,会将对应的类复制过去
SpringCloud框架学习(第一部分:初始项目搭建)_idea2024创建springcloud项目-优快云博客https://blog.youkuaiyun.com/xpy2428507302/article/details/143419140?spm=1001.2014.3001.5501 我们需要修改该模块中的两个文件:config.properties 和 generatorConfig.xml
config.properties:
#表包名
package.name=com.mihoyo.cloud
# seata_order
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456
# seata_storage
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
# seata_account
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
generatorConfig.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<!-- seata_order -->
<table tableName="t_order" domainObjectName="Order">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
<!--seata_storage-->
<!--<table tableName="t_storage" domainObjectName="Storage">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
<!--seata_account-->
<!--<table tableName="t_account" domainObjectName="Account">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
</context>
</generatorConfiguration>
当我们需要生成哪个库的表时,就把对应的注释打开即可。
再双击运行 Maven 中的插件,一键生成
生成结果:
(2)修改公共 cloud-api-commons(新增库存和账户两个Feign服务接口)
订单服务需要调用 库存 和账户 两个服务,所以需要新增对应的 feign 服务接口
库存 feign 接口:StorageFeignApi
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi
{
//扣减库存
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
账户 feign 接口:AccountFeignApi
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi
{
//扣减账户余额
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
(3)新建订单Order微服务
步骤:
① 新建 module(seata-order-service2001)
② 导入依赖
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud-api-commons-->
<dependency>
<groupId>com.mihoyo.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
③ 修改 application.yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.mihoyo.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping: # 点击源码分析
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
对应说明:
由于我们现在只有一个服务器,也就是只有一个集群(cluster),所以事务组的集群名称就默认(default_tx_group)。
同时,default_tx_group 对应的映射的值就是 default。
源码分析:
io.seata.spring.boot.autoconfigure.properties.client.ServiceProperties
这里的 vgroupMapping 就是一个HashMap,存放 KV 键值对。
default_tx_group 和 my_test_tx_group 对应的 value 都是 default,但是视频中说 my_test_tx_group 已经过时,不推荐继续使用。
这里了解即可,如果想继续理解,可以查看官网对于事务分组的介绍
事务分组介绍 | Apache Seatahttps://seata.apache.org/zh-cn/docs/v2.0/user/txgroup/transaction-group有关 seata 的详细配置如下,但太详细后续不好维护
#seata:
# registry: # seata注册配置
# type: nacos # seata注册类型
# nacos:
# application: seata-server #seata应用名称
# server-addr: 127.0.0.1:8848
# namespace: ""
# group: SEATA_GROUP
# cluster: default
# config: # seata配置抓取
# nacos:
# server-addr: 127.0.0.1:8848
# namespace: ""
# group: SEATA_GROUP
# username: nacos
# password: nacos
# tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
# service:
# vgroup-mapping:
# default_tx_group: default # 事务群组的映射配置关系
# data-source-proxy-mode: AT
# application-id: seata-server
④ 修改主启动类
@SpringBootApplication
@MapperScan("com.mihoyo.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataOrderMainApp2001
{
public static void main(String[] args)
{
SpringApplication.run(SeataOrderMainApp2001.class,args);
}
}
⑤ 修改业务类
Ⅰ. 实体类 entities
将之前 mapper4 生成的实体类拷贝过来,加上 @ToString 注解,自动生成 toString 方法
/**
* 表名:t_order
*/
@Table(name = "t_order")
@ToString
public class Order implements Serializable {
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 用户id
*/
@Column(name = "user_id")
private Long userId;
/**
* 产品id
*/
@Column(name = "product_id")
private Long productId;
/**
* 数量
*/
private Integer count;
/**
* 金额
*/
private Long money;
/**
* 订单状态: 0:创建中; 1:已完结
*/
private Integer status;
/**
* @return id
*/
public Long getId() {
return id;
}
/**
* @param id
*/
public void setId(Long id) {
this.id = id;
}
/**
* 获取用户id
*
* @return userId - 用户id
*/
public Long getUserId() {
return userId;
}
/**
* 设置用户id
*
* @param userId 用户id
*/
public void setUserId(Long userId) {
this.userId = userId;
}
/**
* 获取产品id
*
* @return productId - 产品id
*/
public Long getProductId() {
return productId;
}
/**
* 设置产品id
*
* @param productId 产品id
*/
public void setProductId(Long productId) {
this.productId = productId;
}
/**
* 获取数量
*
* @return count - 数量
*/
public Integer getCount() {
return count;
}
/**
* 设置数量
*
* @param count 数量
*/
public void setCount(Integer count) {
this.count = count;
}
/**
* 获取金额
*
* @return money - 金额
*/
public Long getMoney() {
return money;
}
/**
* 设置金额
*
* @param money 金额
*/
public void setMoney(Long money) {
this.money = money;
}
/**
* 获取订单状态: 0:创建中; 1:已完结
*
* @return status - 订单状态: 0:创建中; 1:已完结
*/
public Integer getStatus() {
return status;
}
/**
* 设置订单状态: 0:创建中; 1:已完结
*
* @param status 订单状态: 0:创建中; 1:已完结
*/
public void setStatus(Integer status) {
this.status = status;
}
}
Ⅱ. mapper 层
将之前 mapper4 生成的 mapper 层的文件拷贝过来,不用做任何修改
mapper 接口:
public interface OrderMapper extends Mapper<Order> {
}
mapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mihoyo.cloud.mapper.OrderMapper">
<resultMap id="BaseResultMap" type="com.mihoyo.cloud.entities.Order">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="product_id" jdbcType="BIGINT" property="productId" />
<result column="count" jdbcType="INTEGER" property="count" />
<result column="money" jdbcType="DECIMAL" property="money" />
<result column="status" jdbcType="INTEGER" property="status" />
</resultMap>
</mapper>
注意:mapper.xml 放在 resource/mapper 文件夹下
Ⅲ. service 层
service 接口:
public interface OrderService {
/**
* 创建订单
*/
void create(Order order);
}
实现类:
//下订单->减库存->扣余额->改(订单)状态
@Slf4j
@Service
public class OrderServiceImpl implements OrderService
{
@Resource
private OrderMapper orderMapper;
@Resource//订单微服务通过OpenFeign去调用库存微服务
private StorageFeignApi storageFeignApi;
@Resource//订单微服务通过OpenFeign去调用账户微服务
private AccountFeignApi accountFeignApi;
@Override
public void create(Order order) {
//xid全局事务id的检查,重要!
String xid = RootContext.getXID();
//1. 新建订单
log.info("==================>开始新建订单"+"\t"+"xid_order:" +xid);
//订单状态status:0:创建中;1:已完结
order.setStatus(0);
int result = orderMapper.insertSelective(order);
//插入订单成功后获得插入mysql的实体对象
Order orderFromDB = null;
if(result > 0)
{
//从mysql中查出刚插入的记录
orderFromDB = orderMapper.selectOne(order);
//orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
log.info("-------> 新建订单成功,orderFromDB info: "+orderFromDB);
System.out.println();
//2. 扣减库存
log.info("-------> 订单微服务开始调用Storage库存,做扣减count");
storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
log.info("-------> 订单微服务结束调用Storage库存,做扣减完成");
System.out.println();
//3. 扣减账号余额
log.info("-------> 订单微服务开始调用Account账号,做扣减money");
accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
log.info("-------> 订单微服务结束调用Account账号,做扣减完成");
System.out.println();
//4. 修改订单状态
//订单状态status:0:创建中;1:已完结
log.info("-------> 修改订单状态");
orderFromDB.setStatus(1);
//where(查询条件):userId = 用户id and status = 0
Example whereCondition=new Example(Order.class);
Example.Criteria criteria=whereCondition.createCriteria();
criteria.andEqualTo("userId",orderFromDB.getUserId());
criteria.andEqualTo("status",0);
//拼接条件进行更新订单数据
int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
log.info("-------> 修改订单状态完成"+"\t"+updateResult);
log.info("-------> orderFromDB info: "+orderFromDB);
}
System.out.println();
log.info("==================>结束新建订单"+"\t"+"xid_order:" +xid);
}
}
Ⅳ. controller 层
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 创建订单
*/
@GetMapping("/order/create")
public ResultData create(Order order)
{
orderService.create(order);
return ResultData.success(order);
}
}
(4)新建库存Storage微服务
步骤:
① 新建 module(seata-storage-service2002)
② 导入依赖
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud_commons_utils-->
<dependency>
<groupId>com.mihoyo.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
③ 修改 application.yml
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.mihoyo.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping:
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
④ 修改主启动类
@SpringBootApplication
@MapperScan("com.mihoyo.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服务注册和发现
@EnableFeignClients
public class SeataStorageMainApp2002
{
public static void main(String[] args)
{
SpringApplication.run(SeataStorageMainApp2002.class,args);
}
}
⑤ 修改业务类
Ⅰ. 实体类 entities
/**
* 表名:t_storage
*/
@Table(name = "t_storage")
@ToString
public class Storage implements Serializable {
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 产品id
*/
@Column(name = "product_id")
private Long productId;
/**
* 总库存
*/
private Integer total;
/**
* 已用库存
*/
private Integer used;
/**
* 剩余库存
*/
private Integer residue;
/**
* @return id
*/
public Long getId() {
return id;
}
/**
* @param id
*/
public void setId(Long id) {
this.id = id;
}
/**
* 获取产品id
*
* @return productId - 产品id
*/
public Long getProductId() {
return productId;
}
/**
* 设置产品id
*
* @param productId 产品id
*/
public void setProductId(Long productId) {
this.productId = productId;
}
/**
* 获取总库存
*
* @return total - 总库存
*/
public Integer getTotal() {
return total;
}
/**
* 设置总库存
*
* @param total 总库存
*/
public void setTotal(Integer total) {
this.total = total;
}
/**
* 获取已用库存
*
* @return used - 已用库存
*/
public Integer getUsed() {
return used;
}
/**
* 设置已用库存
*
* @param used 已用库存
*/
public void setUsed(Integer used) {
this.used = used;
}
/**
* 获取剩余库存
*
* @return residue - 剩余库存
*/
public Integer getResidue() {
return residue;
}
/**
* 设置剩余库存
*
* @param residue 剩余库存
*/
public void setResidue(Integer residue) {
this.residue = residue;
}
}
Ⅱ. mapper 层
mapper 接口:
public interface StorageMapper extends Mapper<Storage>
{
//扣减库(操作复杂,不会自动生成,需要自定义)
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
mapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mihoyo.cloud.mapper.StorageMapper">
<resultMap id="BaseResultMap" type="com.mihoyo.cloud.entities.Storage">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="product_id" jdbcType="BIGINT" property="productId" />
<result column="total" jdbcType="INTEGER" property="total" />
<result column="used" jdbcType="INTEGER" property="used" />
<result column="residue" jdbcType="INTEGER" property="residue" />
</resultMap>
<update id="decrease">
UPDATE
t_storage
SET
used = used + #{count},
residue = residue - #{count}
WHERE product_id = #{productId}
</update>
</mapper>
Ⅲ. service 层
service 接口:
public interface StorageService {
/**
* 扣减库存
*/
void decrease(Long productId, Integer count);
}
实现类:
@Service
@Slf4j
public class StorageServiceImpl implements StorageService
{
@Resource
private StorageMapper storageMapper;
//扣减库存
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣减库存开始");
storageMapper.decrease(productId,count);
log.info("------->storage-service中扣减库存结束");
}
}
Ⅳ. controller 层
@RestController
public class StorageController
{
@Resource
private StorageService storageService;
/**
* 扣减库存
*/
@PostMapping("/storage/decrease")
public ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {
storageService.decrease(productId, count);
return ResultData.success("扣减库存成功!");
}
}
细节:
@
RequestParam
的作用:
@RequestParam("productId")
明确告诉 Feign,这个参数应该被作为 请求参数,并且它的名称在 HTTP 请求中是productId
。@RequestParam("count")
明确对应count
。因此,当你调用
storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount())
时:
- Feign 会读取
@RequestParam
注解的值,知道第一个参数的名称是productId
,第二个参数的名称是count
。- Feign 生成的 HTTP 请求会附带如下参数:
POST /storage/decrease?productId=123&count=10
目录结构:
(5)新建账户Account微服务
步骤:
① 新建 module(seata-account-service2003)
② 导入依赖
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud_commons_utils-->
<dependency>
<groupId>com.mihoyo.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
③ 修改 application.yml
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.mihoyo.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
service:
vgroup-mapping:
default_tx_group: default # 事务组与TC服务集群的映射关系
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
④ 修改主启动类
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.mihoyo.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
public class SeataAccountMainApp2003
{
public static void main(String[] args)
{
SpringApplication.run(SeataAccountMainApp2003.class,args);
}
}
⑤ 修改业务类
Ⅰ. 实体类 entities
/**
* 表名:t_account
*/
@Table(name = "t_account")
@ToString
public class Account implements Serializable {
/**
* id
*/
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 用户id
*/
@Column(name = "user_id")
private Long userId;
/**
* 总额度
*/
private Long total;
/**
* 已用账户余额
*/
private Long used;
/**
* 剩余可用额度
*/
private Long residue;
/**
* 获取id
*
* @return id - id
*/
public Long getId() {
return id;
}
/**
* 设置id
*
* @param id id
*/
public void setId(Long id) {
this.id = id;
}
/**
* 获取用户id
*
* @return userId - 用户id
*/
public Long getUserId() {
return userId;
}
/**
* 设置用户id
*
* @param userId 用户id
*/
public void setUserId(Long userId) {
this.userId = userId;
}
/**
* 获取总额度
*
* @return total - 总额度
*/
public Long getTotal() {
return total;
}
/**
* 设置总额度
*
* @param total 总额度
*/
public void setTotal(Long total) {
this.total = total;
}
/**
* 获取已用账户余额
*
* @return used - 已用账户余额
*/
public Long getUsed() {
return used;
}
/**
* 设置已用账户余额
*
* @param used 已用账户余额
*/
public void setUsed(Long used) {
this.used = used;
}
/**
* 获取剩余可用额度
*
* @return residue - 剩余可用额度
*/
public Long getResidue() {
return residue;
}
/**
* 设置剩余可用额度
*
* @param residue 剩余可用额度
*/
public void setResidue(Long residue) {
this.residue = residue;
}
}
Ⅱ. mapper 层
mapper 接口:
public interface AccountMapper extends Mapper<Account>
{
//扣减余额(操作复杂,不会自动生成,需要自定义)
void decrease(@Param("userId") Long userId, @Param("money") Long money);
}
mapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mihoyo.cloud.mapper.AccountMapper">
<resultMap id="BaseResultMap" type="com.mihoyo.cloud.entities.Account">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="total" jdbcType="DECIMAL" property="total" />
<result column="used" jdbcType="DECIMAL" property="used" />
<result column="residue" jdbcType="DECIMAL" property="residue" />
</resultMap>
<!--
扣减余额
money 本次消费金额
total总额度 = 累计已消费金额(used) + 剩余可用额度(residue)
-->
<update id="decrease">
UPDATE
t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE user_id = #{userId};
</update>
</mapper>
Ⅲ. service 层
service 接口:
public interface AccountService {
//扣减账户余额
void decrease(Long userId, Long money);
}
实现类:
@Service
@Slf4j
public class AccountServiceImpl implements AccountService
{
@Resource
AccountMapper accountMapper;
/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service中扣减账户余额开始");
accountMapper.decrease(userId,money);
//myTimeOut();
//int age = 10/0;
log.info("------->account-service中扣减账户余额结束");
}
/**
* 模拟超时异常,全局事务回滚
*/
private static void myTimeOut()
{
try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
细节:这里设置了两个异常:超时异常和算数异常,用于后续测试事务回滚
Ⅳ. controller 层
@RestController
public class AccountController {
@Resource
AccountService accountService;
/**
* 扣减账户余额
*/
@PostMapping("/account/decrease")
public ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){
accountService.decrease(userId,money);
return ResultData.success("扣减账户余额成功!");
}
}
5.Seata案例实战-测试
(1)服务启动情况
启动Nacos
启动Seata
启动订单微服务2001
启动库存微服务2002
启动账户微服务2003
(2)数据库初始情况
SELECT * FROM `seata_order`.`t_order`;
SELECT * FROM `seata_storage`.`t_storage`;
SELECT * FROM `seata_account`.`t_account`;
(3)正常下单测试
需求:下订单→减库存→扣余额→改(订单)状态
注意:此时我们没有在订单模块添加 @GlobalTransactional,还没有开启分布式事务管理
模拟:1号用户花费100块钱买了10个1号产品
访问地址:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
运行结果:
控制台结果:
数据库结果:
整个流程全部正常,成功执行!
扩展:
视频中会报错,原因说是因为 springboot + springcloud版本太高导致和阿里巴巴Seata不兼容。
这里我看了官网的版本说明,目前已经是可以兼容的了!
如果还不行的话,注意一下 StorageController 和 StorageFeignApi,AccountController 和 AccountFeignApi 的定义要保证一致性:
- controller 和 feign 接口中都要有 @RequestParam
- controller 和 feign 接口中的方法签名保持一致
- controller 和 feign 接口中的请求类型要一致(@PostMapping...)
原视频中这些并没有保持一致!注意修改,修改后就可以正常运行了!
(4)超时异常出错,没有 @GlobalTransactional
修改 seata-account-service2003 微服务,AccountServiceImpl 添加超时
OpenFeign 客户端的默认等待时间 60S,超过这个时间就会报错,不记得可以看下方:
访问地址:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
运行结果:
故障情况:
当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为 1
(5)超时异常解决,添加 @GlobalTransactional
步骤:
① 打开 AccountServiceImpl 超时方法
② 在 OrderServiceImpl 中加上 @GlobalTransactional 注解
此时,订单模块就是TM,也是其中一个RM
③ 重启 2001,2002,2003
访问地址:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
打开 seata 控制台:
全局锁:
数据库结果:
运行中:
Order 库:
Account 库:
Storage 库:
说明:
这里由于时间问题,65 s 内来不及截这么多图。截图中的 xid 和 branch_id 可能有的不一致,因为我运行了很多次来截图,所以有的 xid 和 brach_id 不同是正常的。
但是实际运行时,同一次的 xid 和 brach_id 肯定都和 seata 控制台中是一致的。
运行结束:
Order 库:
Account 库:
Storage 库:
控制台:
可以发现:运行结束后,数据库 3 个库数据并没有任何改变,都被回滚了。同时 3 个 库中的undo_log 表中的数据也都被回滚了!
(6)正常下单,添加 @GlobalTransactional
关闭 AccountServiceImpl 超时方法
重启 2001,2002,2003
访问地址:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100、
数据库情况:
日志情况:
可以发现,三个阶段(分支)成功都被提交(commit)!
6.Seata原理
Question:AT模式如何做到对业务的无侵入?
(1)一阶段加载
在一阶段,Seata 会拦截“业务 SQL”,
① 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,
② 执行“业务 SQL”更新业务数据,
③ 在业务数据更新之后,其保存成“after image”,
④ 最后生成行锁。
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
(2)二阶段分 2 种情况
Ⅰ. 正常提交
二阶段如是顺利提交的话,
因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
Ⅱ. 异常回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。
回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,
如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。