SpringCloud框架学习(第七部分:分布式事务Seata)

目录

十五、SpringCloud Alibaba Seata处理分布式事务

1.分布式事务背景

2.Seata简介 

(1)介绍

(2)工作流程 

(3)各事务模式 

(4)下载安装

3.Seata案例实战-数据库和表准备

(1)创建 3 个业务数据库DATABASE 

(2)按照上述 3 库分别建对应的 undo_log 回滚日志表

(3)按照上述 3 库分别建对应业务表

4.Seata案例实战-微服务编码落地实现

(1)Mybaits一键生成

(2)修改公共 cloud-api-commons(新增库存和账户两个Feign服务接口)

(3)新建订单Order微服务

(4)新建库存Storage微服务

(5)新建账户Account微服务

5.Seata案例实战-测试

(1)服务启动情况

(2)数据库初始情况

 (3)正常下单测试

(4)超时异常出错,没有 @GlobalTransactional

 (5)超时异常解决,添加 @GlobalTransactional

(6)正常下单,添加 @GlobalTransactional

6.Seata原理

 (1)一阶段加载

(2)二阶段分 2 种情况 

Ⅰ. 正常提交

Ⅱ. 异常回滚 


十五、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 dataCREATE 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 = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store BranchSession dataCREATE 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 = InnoDBDEFAULT CHARSET = utf8mb4;-- the table to store lock dataCREATE 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 = InnoDBDEFAULT 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 = InnoDBDEFAULT 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: 7091spring:application:name: seata-serverlogging:config: classpath:logback-spring.xmlfile:path: ${log.home:${user.home}/logs/seata}extend:logstash-appender:destination: 127.0.0.1:4560kafka-appender:bootstrap-servers: 127.0.0.1:9092topic: logback_to_logstashconsole:user:username: seatapassword: seataseata:config:type: nacosnacos:server-addr: 127.0.0.1:8848namespace:group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUPusername: nacospassword: nacosregistry:type: nacosnacos:application: seata-serverserver-addr: 127.0.0.1:8848group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUPnamespace:cluster: defaultusername: nacospassword: nacos    store:mode: dbdb:datasource: druiddb-type: mysqldriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueuser: rootpassword: 123456min-conn: 10max-conn: 100global-table: global_tablebranch-table: branch_tablelock-table: lock_tabledistributed-lock-table: distributed_lockquery-limit: 1000max-wait: 5000#  server:#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore: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项目-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/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 generatorConfigurationPUBLIC "-//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: 2001spring:application:name: seata-order-servicecloud:nacos:discovery:server-addr: localhost:8848         #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueusername: rootpassword: 123456
# ========================mybatis===================
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.mihoyo.cloud.entitiesconfiguration:map-underscore-to-camel-case: true# ========================seata===================
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping: # 点击源码分析default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: ATlogging: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 Seataicon-default.png?t=O83Ahttps://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
{@Resourceprivate OrderMapper orderMapper;@Resource//订单微服务通过OpenFeign去调用库存微服务private StorageFeignApi storageFeignApi;@Resource//订单微服务通过OpenFeign去调用账户微服务private AccountFeignApi accountFeignApi;@Overridepublic 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 = 0Example 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 {@Resourceprivate 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: 2002spring:application:name: seata-storage-servicecloud:nacos:discovery:server-addr: localhost:8848         #Nacos服务注册中心地址# ==========applicationName + druid-mysql8 driver===================datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueusername: rootpassword: 123456
# ========================mybatis===================
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.mihoyo.cloud.entitiesconfiguration:map-underscore-to-camel-case: true
# ========================seata===================
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping:default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: ATlogging: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">UPDATEt_storageSETused = 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
{@Resourceprivate StorageMapper storageMapper;//扣减库存@Overridepublic void decrease(Long productId, Integer count) {log.info("------->storage-service中扣减库存开始");storageMapper.decrease(productId,count);log.info("------->storage-service中扣减库存结束");}
}

 Ⅳ. controller 层

@RestController
public class StorageController
{@Resourceprivate 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()) 时:

  1. Feign 会读取 @RequestParam 注解的值,知道第一个参数的名称是 productId,第二个参数的名称是 count
  2. 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: 2003spring:application:name: seata-account-servicecloud:nacos:discovery:server-addr: localhost:8848         #Nacos服务注册中心地址# ==========applicationName + druid-mysql8 driver===================datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=trueusername: rootpassword: 123456
# ========================mybatis===================
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.mihoyo.cloud.entitiesconfiguration:map-underscore-to-camel-case: true# ========================seata===================
seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848namespace: ""group: SEATA_GROUPapplication: seata-servertx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称service:vgroup-mapping:default_tx_group: default # 事务组与TC服务集群的映射关系data-source-proxy-mode: ATlogging: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">UPDATEt_accountSETresidue = 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
{@ResourceAccountMapper accountMapper;/*** 扣减账户余额*/@Overridepublic 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 {@ResourceAccountService 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,超过这个时间就会报错,不记得可以看下方:

SpringCloud框架学习(第二部分:Consul、LoadBalancer和openFeign)_spring openfeign consul-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/xpy2428507302/article/details/143636256?spm=1001.2014.3001.5501

访问地址: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”,

如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/63073.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python打包深度学习虚拟环境

今天师兄让我把环境打包发给他&#xff0c;我才知道可以直接打包深度学习虚拟环境&#xff0c;这样另一个人就不用辛辛苦苦的去装环境了&#xff0c;我们都知道有些论文他需要的环境很难装上。比如装Apex&#xff0c;装 DCN&#xff0c;mmcv-full 我现在把3090机子上的ppft虚拟…

基于MobileNet深度学习网络的MQAM调制类型识别matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频&#xff09…

<工具 Claude Desktop> 配置 MCP server 连接本地 SQLite, 本机文件夹(目录) 网络驱动器 Windows 11 系统

也是在学习中... 起因&#xff1a; 抖音博客 艾克AI分享 他的视频 #143《Claude开源MCP彻底打破AI的信息孤岛》 提到: Claude开源的MCP太强了&#xff0c;视频后面是快速演示&#xff0c;反正看了好几遍也没弄明白。菜单都不一样&#xff0c;感觉用的不是同一家 Claude. 探…

(78)MPSK基带调制通信系统瑞利平坦衰落信道传输性能的MATLAB仿真

文章目录 前言一、MATLAB仿真1.仿真代码2.仿真结果 二、子函数与完整代码总结 前言 本文给出瑞利平坦衰落信道上的M-PSK通信系统性能仿真的MATLAB源代码与仿真结果。其中&#xff0c;调制方式M-PSK包括BPSK、QPSK、8-PSK、16-PSK、32-PSK等方式。 一、MATLAB仿真 1.仿真代码 …

go语言 Pool实现资源池管理数据库连接资源或其他常用需要共享的资源

go Pool Pool用于展示如何使用有缓冲的通道实现资源池&#xff0c;来管理可以在任意数量的goroutine之间共享及独立使用的资源。这种模式在需要共享一组静态资源的情况&#xff08;如共享数据库连接或者内存缓冲区&#xff09;下非 常有用。如果goroutine需要从池里得到这些资…

Android 系统之Init进程分析

1、Init进程流程 2、Init细节逻辑 2.1 Init触发shutdown init进程触发系统重启是一个很合理的逻辑&#xff0c;为什么合理&#xff1f; init进程是android世界的一切基石&#xff0c;如果android世界的某些服务或者进程出现异常&#xff0c;那么会导致整个系统无法正常使用…

用micropython 操作stm32f4单片机的定时器实现蜂鸣器驱动

import pyb import time # 初始化引脚和定时器通道作为PWM输出 # 注意&#xff1a;这里我们假设您使用的是支持PWM的引脚和定时器 # 在不同的MicroPython板上&#xff0c;支持的引脚和定时器可能不同 # 请查阅您的板的文档以确认正确的引脚和定时器 buzzer_pin pyb.Pin(PD15,…

长沙市的科技查新单位

1、中南大学图书馆科技查新站&#xff1a; 中南大学图书馆科技查新站成立于2003年12月&#xff0c;中南大学图书馆科技查新站作为教育部首批批准的科技查新工作站之一&#xff0c;具备了在全国范围内开展科技查新工作的专业资质。 2、湖南大学科技查新站&#xff1a; 湖南大学…

java基础语法光速入门

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理Java的基础语法部分 适合有编程基础的人快点掌握语法使用 没学过一两门语言的话。。还是不建议看了 极致的浓缩没有一点解释 注释 单行注释 // 多行注释 /**/ 数据类型 布尔型:true false 整型:int,lon…

【redis】集群详解

redis集群 一、集群的概念二、数据分片算法2.1哈希求余算法2.2一致性哈希算法2.3哈希槽分区算法 三、集群的搭建3.1配置docker-compose.yml文件3.2配置generate.sh脚本文件3.3构建redis集群3.4简单测试redis集群 四、故障处理流程4.1故障判定4.2故障转移 五、集群扩容 一、集群…

Linux | Linux的开机启动流程

对于linux系统的初学者来说&#xff0c;理解并掌握linux系统启动流程能够使你够深入的理解linux系统&#xff0c;还可以通过系统的启动过程来分析问题解决问题。 Linux开机启动的流程如下图 power on 开机 post自检&#xff08;检查一部分大的硬件&#xff09; BIOS&#xf…

TiDB如何保证数据一致性

1. 分布式事务协议 TiDB 采用了类似 Google Percolator 的分布式事务协议来处理分布式事务。这个协议基于两阶段提交&#xff08;2PC&#xff09;的思想&#xff0c;但进行了优化和改进&#xff0c;以适应分布式环境的特殊需求。在 TiDB 中&#xff0c;当一个事务需要跨多个节…

【Maven系列】深入解析 Maven 常用命令

前言 在当今的软件开发过程中&#xff0c;项目管理是至关重要的一环。项目管理包括了项目构建、依赖管理以及发布部署等诸多方面。而在Java生态系统中&#xff0c;Maven已经成为了最受欢迎的项目管理工具之一。Maven 是一套用于构建、依赖管理和项目管理的工具&#xff0c;主要…

DBA面试题-1

面临失业&#xff0c;整理一下面试题&#xff0c;找下家继续搬砖 主要参考&#xff1a;https://www.csdn.net/?spm1001.2101.3001.4476 略有修改 一、mysql有哪些数据类型 1&#xff0c; 整形 tinyint,smallint,medumint,int,bigint&#xff1b;分别占用1字节、2字节、3字节…

【Rust WebAssembly 入门实操遇到的问题】

Rust WebAssembly 入门实操遇到的问题 什么是WebAssembly跟着教程走wasm-pack build error总结 什么是WebAssembly WebAssembly&#xff08;简称Wasm&#xff09;是一种基于堆栈的虚拟机的二进制指令 格式。Wasm 被设计为编程语言的可移植编译目标&#xff0c;支持在 Web 上部…

数据挖掘之数据预处理

​​​​​​​ 引言 数据挖掘是从大量数据中提取有用信息和知识的过程。在这个过程中&#xff0c;数据预处理是不可或缺的关键步骤。数据预处理旨在清理和转换数据&#xff0c;以提高数据质量&#xff0c;从而为后续的数据挖掘任务奠定坚实的基础。由于现实世界中的数据通常…

21个Python脚本自动执行日常任务(1)

引言 作为编程领域摸爬滚打超过十年的老手&#xff0c;我深刻体会到&#xff0c;自动化那些重复性工作能大大节省我们的时间和精力。 Python以其简洁的语法和功能强大的库支持&#xff0c;成为了编写自动化脚本的首选语言。无论你是专业的程序员&#xff0c;还是希望简化日常工…

从 HTML 到 CSS:开启网页样式之旅(五)—— CSS盒子模型

从 HTML 到 CSS&#xff1a;开启网页样式之旅&#xff08;五&#xff09;—— CSS盒子模型 前言一、盒子模型的组成margin&#xff08;外边距&#xff09;&#xff1a;border&#xff08;边框&#xff09;&#xff1a;padding&#xff08;内边距&#xff09;&#xff1a;conten…

使用Feign远程调用丢失请求头问题

在使用Feign进行远程调用时&#xff0c;当前服务是能拿到请求头信息的&#xff0c;请求头包含有登录认证Cookie等重要信息&#xff0c;但是在调用远程服务时&#xff0c;远程服务却拿不到请求头信息&#xff0c;因为使用Feign进行远程调用实际上是发起新的Request请求了&#x…

2021数学分析【南昌大学】

2021 数学分析 求极限 lim ⁡ n → ∞ 1 n ( n + 1 ) ( n + 2 ) ⋯ ( n + n ) n \lim_{n \to \infty} \frac{1}{n} \sqrt [n]{(n+1)(n+2) \cdots (n+n)} n→∞lim​n1​n(n+1)(n+2)⋯(n+n) ​ lim ⁡ n → ∞ 1 n ( n + 1 ) ( n + 2 ) ⋯ ( n + n ) n = lim ⁡ n → ∞ ( n + …