四、SpringCloud-Seata-07

分布式事务-Seata

分布式事务基础

事务

事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销。简单地说,事务提供一种“要么什么都不做,要么做全套”机制。

本地事务

本地事务其实可以认为是数据库提供的事务机制。说到数据库事务就不得不说,数据库事务中的四大特性:

A:原子性(Atomicity),一个事务中的所有操作,要么全部完成,要么全部不完成。

C:一致性(Consistency),在一个事务执行之前和执行之后数据库都必须处于一致性状态。

I:隔离性(Isolation),在并发环境中,当不同的事务同时操作相同的数据时,事务之间互不影响。

D:持久性(Durability),指的是只要事务成功结束,它对数据库所做的更新就必须永久的保存下来。

数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。

分布式事务

分布式事务指事务的参与者支持事务的服务器资源服务器以及事务管理器分别位于不同的分布式系

统的不同节点之上。 简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服

务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。 本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务的场景

  1. 单体系统访问多个数据库

一个服务需要调用多个数据库实例完成数据的增删改操作

image-20230911094801171

  1. 多个微服务访问同一个数据库

多个服务需要调用一个数据库实例完成数据的增删改操作

image-20230911094829917

  1. 多个微服务访问多个数据库

多个服务需要调用一个数据库实例完成数据的增删改操作

image-20230911094902344

分布式事务解决方案

全局事务

全局事务基于 DTP 模型实现。 DTP 是由 X/Open 组织提出的一种分布式事务模型—— X/Open

Distributed Transaction Processing Reference Model 。它规定了要实现分布式事务,需要三种

角色:

  • AP: Application 应用系统 (微服务)
  • TM: Transaction Manager 事务管理器 (全局事务管理)
  • RM: Resource Manager 资源管理器 (数据库)

整个事务分成两个阶段:

  • 阶段一:表决阶段,所有参与者都将本地事务执行预提交,并将能否成功的信息反馈发给协调者。
  • 阶段二:执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地执行提交或者回滚。

image-20230911094952435

优点

  • 提高了数据一致性的概率,实现成本较低

缺点

  • 单点问题: 事务协调者宕机。
  • 同步阻塞: 延迟了提交时间,加长了资源阻塞时间。
  • 数据不一致: 提交第二阶段,依然存在 commit 结果未知的情况,有可能导致数据不一致。

可靠消息服务

基于可靠消息服务的方案是通过消息中间件保证上、下游应用数据操作的一致性。假设有 A 和 B 两个

系统,分别可以处理任务 A 和任务 B 。此时存在一个业务流程,需要将任务 A 和任务 B 在同一个事务中处理。就可以使用消息中间件来实现这种分布式事务。

image-20230911095029668

第一步:消息由系统A投递到中间件
  1. 在系统 A 处理任务 A 前,首先向消息中间件发送一条消息

  2. 消息中间件收到后将该条消息持久化,但并不投递。持久化成功后,向 A 回复一个确认应答

  3. 系统 A 收到确认应答后,则可以开始处理任务 A

  4. 任务 A 处理完成后,向消息中间件发送 Commit 或者 Rollback 请求。该请求发送完成后,对系

统 A 而言,该事务的处理过程就结束了

  1. 如果消息中间件收到 Commit ,则向 B 系统投递消息;如果收到 Rollback ,则直接丢弃消息。

但是如果消息中间件收不到 Commit 和 Rollback 指令,那么就要依靠"超时询问机制"。

超时询问机制

系统 A 除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息

中间件收到发布消息便开始计时,如果到了超时没收到确认指令,就会主动调用系统 A 提供的事

务询问接口询问该系统目前的状态。该接口会返回三种结果,中间件根据三种结果做出不同反应:

  • 提交:将该消息投递给系统B
  • 回滚:直接将条消息丢弃
  • 处理中:继续等待
第二步:消息由中间件投递到系统B

消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。

  • 如果消息中间件收到确认应答后便认为该事务处理完毕
  • 如果消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要手工干预。这里之所以使用人工干预,而不是使用让 A 系统回滚,主要是考虑到整个系统设计的复杂度问题。 基于可靠消息服务的分布式事务,前半部分使用异步,注重性能;后半部分使用同步,注重开发成本。

最大努力通知

最大努力通知也被称为定期校对,其实是对第二种解决方案的进一步优化。它引入了本地消息表来记录错误消息,然后加入失败消息的定期校对功能,来进一步保证消息会被下游系统消费。

image-20230911095221950

第一步:消息由系统A投递到中间件
  1. 处理业务的同一事务中,向本地消息表中写入一条记录。

  2. 准备专门的消息发送者不断地发送本地消息表中的消息到消息中间件,如果发送失败则重试。

第二步:消息由中间件投递到系统B
  1. 消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行。

  2. 当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成。

  3. 对于投递失败的消息,利用重试机制进行重试,对于重试失败的,写入错误消息表。

  4. 消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费。

这种方式的优缺点:

  • 优点: 一种非常经典的实现,实现了最终一致性。
  • 缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

TCC事务

TCC 即为 Try Confirm Cancel ,它属于补偿型分布式事务。 TCC 实现分布式事务一共有三个步骤:

  1. Try:尝试待执行的业务

这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源

  1. Confirm:确认执行业务

确认执行业务操作,不做任何业务检查, 只使用 Try 阶段预留的业务资源。通常情况下,采用TCC 则认为 Confirm 阶段是不会出错的。即:只要 Try 成功, Confirm 一定成功。若Confirm 阶段真的出错了,需引入重试机制或人工处理。

  1. Cancel:取消待执行的业务

取消 Try 阶段预留的业务资源。通常情况下,采用 TCC 则认为 Cancel 阶段也是一定成功的。若 Cancel 阶段真的出错了,需引入重试机制或人工处理。

image-20230911095424361

TCC 两阶段提交与 XA 两阶段提交的区别是:

  • XA 是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
  • TCC 是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。

TCC 事务的优缺点:

  • 优点:把数据库层的二阶段提交上提到了应用层来实现,规避了数据库层的 2PC 性能低下问题。
  • 缺点: TCC 的 Try 、 Confirm 和 Cancel 操作功能需业务提供,开发成本高。

Seata 介绍

  • TC: Transaction Coordinator 事务协调器,它负责协调和管理分布式事务的整个过程。事务协调器协调各个参与者(包括事务管理器和资源管理器)的行为,确保事务的一致性和隔离性。它接收事务请求,协调各个参与者的事务操作,并最终决定事务的提交或回滚。
  • TM: Transaction Manager 事务管理器,负责管理和控制事务的生命周期。事务管理器与业务系统进行交互,接收事务请求,并根据事务协调器的指令执行相应的操作。它负责开启、提交或回滚事务,并保证事务的一致性和持久性。
  • RM: Resource Manager 资源管理器,用于分支事务上的资源管理,向 TC 注册分支事务,上报分支事务的状态,接受 TC 的命令来提交或者回滚分支事务。

image-20230911100046564

Seata 的执行流程

  1. A 服务的 TM 向 TC 申请开启一个全局事务, TC 就会创建一个全局事务并返回一个唯一的 XID

  2. A 服务的 RM 向 TC 注册分支事务,并及其纳入 XID 对应全局事务的管辖

  3. A 服务执行分支事务,向数据库做操作

  4. A 服务开始远程调用B服务,此时 XID 会在微服务的调用链上传播

  5. B 服务的 RM 向 TC 注册分支事务,并将其纳入 XID 对应的全局事务的管辖

  6. B 服务执行分支事务,向数据库做操作

  7. 全局事务调用链处理完毕, TM 根据有无异常向 TC 发起全局事务的提交或者回滚

  8. TC 协调其管辖之下的所有分支事务, 决定是否回滚

Seata 实现 2PC 与传统 2PC 的差别

  1. 架构层次方面,传统 2PC 方案的 RM 实际上是在数据库层, RM 本质上就是数据库自身,通过 XA协议实现,而 Seata 的 RM 是以 jar 包的形式作为中间件层部署在应用程序这一侧的。

  2. 两阶段提交方面,传统 2PC 无论第二阶段的决议是 commit 还是 rollback ,事务性资源的锁都要保持到 Phase2 完成才释放。而 Seata 的做法是在 Phase1 就将本地事务提交,这样就可以省去 Phase2 持锁的时间,整体提高效率。

Seata 的实现

本示例通过 Seata 中间件实现分布式事务,模拟电商中的下单扣库存的过程

我们通过订单微服务执行下单操作,然后由订单微服务调用商品微服务扣除库存

image-20230911111133877

创建项目

首先,创建2个微服务: 订单微服务 和 库存微服务 ,引入相应依赖,书写相应功能:

订单微服务:向订单表中存入一笔记录。

库存微服务: 扣减相应商品的库存记录。

然后在订单微服务中调用库存微服务的操作。

启动 Seata服务端
下载

到官网中下载 seata ,目前使用版本是 1.4.2 下载中心 (seata.io)

image-20230911151816438

解压到Linux中

image-20230911151622979

配置数据源

配置 seata 数据源,修改 conf 文件夹下的 file.conf 文件

image-20230911152748965

## transaction log store, only used in seata-server
store {
## store mode: file、db、redis 这里需要修改
mode = "db"
## rsa decryption public key
publicKey = ""
## database store property
db {
## the implement of javax.sql.DataSource, such as
DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc. 这里也需要修改:驱动、url、账号和密码
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
## if using mysql to store the data, recommend add
rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://localhost:3306/seata?
rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
创建数据库

创建一个数据库 seata ,这个名称对应上边配置文件的数据库名。

create database seata;
use 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_and_branch_id` (`xid` , `branch_id`)
) 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);
修改配置|注册中心

配置 seata 的配置中心和注册中心,这里我们使用 nacos 来管理。文件在 conf 下的registry.conf

image-20230911154309554

image-20230911154100680

registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 这里选择 nacos
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.65.3:8848"
group = "SEATA_GROUP"
namespace = "a1225c08-7b57-4ed2-a94e-18e85418193f"
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3 这里也选择 nacos
type = "nacos"
nacos {
serverAddr = "192.168.65.3:8848"
namespace = "a1225c08-7b57-4ed2-a94e-18e85418193f"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
nacos 配置中心配置 seata

在 nacos 配置中心指定的 namespace 中创建 seataServer.properties 文件,注意 1.4.2 不支持yaml 格式。

image-20230911160238963

下边内容直接复制,修改数据源和 service.vgroupMapping 部分即可。

#For details about configuration items, see https://seata.io/zhcn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
#Transaction routing rules configuration, only for the client
# service.vgroupMapping.default_tx_group=default
service.vgroupMapping.seata-order-seata-server-group=default
service.vgroupMapping.seata-stock-seata-server-group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
#Log rule configuration, for client and server
log.exceptionRate=100
#Transaction storage configuration, only for the server. The file, DB, and redis
configuration values are optional.
store.mode=db
store.lock.mode=file
store.session.mode=file
#Used for password encryption
store.publicKey=
#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you
can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

#These configurations are required if the `store mode` is `db`. If
`store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can
remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql

image-20230911160326628

修改第一处

image-20230911155828252

修改第二处

image-20230911160010924

启动 seata-server

双击启动 seata-server.bat 即可

image-20230911111805194

如果是 linux 则启动 seata-server.sh

image-20230911161003160

image-20230911161112772

**注意:**目前版本 seata-server 要求电脑必须配置 jdk 并且仅支持 1.8 版本

Seata 集成微服务

我们已经启动了 seata 服务端,下来我们将 seata 集成到需要用到的微服务中。

创建 undo_log 表

在微服务所属的数据库中创建如下表,如果微服务使用多个数据库,需要分别创建这张表。

-- 注意此处0.3.0+ 增加唯一索引 ux_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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
添加依赖

每个微服务项目中添加依赖

<!--        提供了 Seata 的集成支持-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--使用 <exclusions> 标签排除了 Seata 的 Spring Boot Starter 依赖项-->
    <exclusion>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
    </exclusion>
</exclusions>
<!--是 Seata 的核心依赖项,用于启用 Seata 的功能-->
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
配置微服务
seata:
# 开启seata
  enabled: true
# 自动创建seata的数据代理    
  enable-auto-data-source-proxy: true
# 引入对应的properties
  config:
    type: nacos
    nacos:
      namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
      serverAddr: 192.168.65.3:8848
      group: SEATA_GROUP
      data-id: seataServer.properties
# 开始注册
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.65.3:8848
      group: SEATA_GROUP
      namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
  tx-service-group: seata-stock-seata-server-group #这里名称和服务端名称对应
@GlobalTransactional 注解

@GlobalTransactional 是 Spring Cloud Alibaba Seata 提供的注解,用于在全局事务管理中声明一个方法需要进行事务管理。

当我们在一个方法上使用 @GlobalTransactional 注解时,Seata 会为该方法创建一个全局事务。在方法执行过程中,如果发生了异常或者抛出了异常,Seata 会自动回滚整个全局事务。如果方法执行成功,Seata 会提交整个全局事务。

在使用 @GlobalTransactional 注解时,我们还可以通过传递一些属性来配置全局事务的行为。例如,我们可以通过 rollbackFor 属性指定哪些异常需要回滚全局事务,通过 timeout 属性指定全局事务的超时时间等等。

总之,@GlobalTransactional 注解是 Seata 提供的一种简单而强大的全局事务管理方式,可以让我们在分布式系统中更加轻松地进行事务管理。

在业务类的方法上添加全局事务注解

@Service
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements
        StockService {
    @GlobalTransactional
    @Override
    public JsonResult updateStack(int goodsId, int number) {
        int row = 0;
        try {
            row = getBaseMapper().update(goodsId, number);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return ResultTool.success(row);
    }
}

注意:每个业务类上都需要添加这个注解。

seata 运行流程分析

image-20230911112009462

要点说明:

  1. 每个 RM 连接数据库,其目的是使用 ConnectionProxy ,使用数据源和数据连接代理的目的就是在第一阶段将 undo_log 和业务数据放在一个本地事务提交,这样就保存了只要有业务操作就一定有 undo_log 。

  2. 在第一阶段 undo_log 中存放了数据修改前和修改后的值,为事务回滚作好准备,所以第一阶段完成就已经将分支事务提交,也就释放了锁资源。

  3. TM 开启全局事务开始,将 XID 全局事务 id 放在事务上下文中,通过 feign 调用也将 XID 传入下游分支事务,每个分支事务将自己的 Branch ID 分支事务 ID 与 XID 关联。

  4. 第二阶段全局事务提交, TC 会通知各各分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各各参与者只需要删除 undo_log 即可,并且可以异步执行,第二阶段很快可以完成。

  5. 第二阶段全局事务回滚, TC 会通知各各分支参与者回滚分支事务,通过 XID 和 Branch ID 找到相应的回滚日志,通过回滚日志生成反向的 SQL 并执行,以完成分支事务回滚到之前的状态,如果回滚失败则会重试回滚操作。

案例1

准备数据库

-- 订单表
CREATE TABLE `order` (
                         `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
                         `goods_id` INT(11) NOT NULL COMMENT '商品 ID',
                         `order_number` INT(11) NOT NULL COMMENT '订单号',
                         `order_time` DATE NOT NULL COMMENT '下单时间',
                         PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

-- 库存表
CREATE TABLE `stock` (
                         `id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
                         `goods_id` INT(11) NOT NULL COMMENT '商品 ID',
                         `stock` INT(11) NOT NULL COMMENT '库存',
                         PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';

INSERT INTO `stock`(`id`, `goods_id`, `stock`) VALUES (1, 1, 3);
INSERT INTO `stock`(`id`, `goods_id`, `stock`) VALUES (2, 2, 20);
INSERT INTO `stock`(`id`, `goods_id`, `stock`) VALUES (3, 3, 14);

SELECT * FROM `order`;

SELECT * FROM `stock`;

搭建项目

image-20230911181144261

导入依赖

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring.boot.version>2.6.6</spring.boot.version>
    <spring.cloud.alibaba.version>2021.0.4.0</spring.cloud.alibaba.version>
    <spring.cloud.version>2021.0.4</spring.cloud.version>
</properties>


<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <!--SpringBoot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--SpringCloud依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring.cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--SpringCloud alibaba依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring.cloud.alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
1、seata-commons模块

搭建项目

image-20230911181542986

导入依赖

<dependencies>
<!--        用于连接 MySQL 数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
<!--        是 MyBatis Plus 的启动器,用于在 Spring Boot 应用程序中使用 MyBatis Plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
<!--        是阿里巴巴的 Druid 数据库连接池,用于优化数据库连接的管理和使用-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.9</version>
        </dependency>
<!--        是 Lombok 的依赖项,用于简化 Java 代码的编写-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

配置文件(application-sql.yml)

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

bean包

@TableName("`order`")
@Data
public class Order implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Integer goodsId;
    private Integer orderNumber;
    private String orderTime;
}
@Data
public class Stock implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Integer goodsId;
    private Integer stock;
}

mapper包

@Mapper
public interface OrderMapper {
    @Insert("insert into `order` values(null,#{goodsId},#{orderNumber},now())")
    void saveOrder(Order order);
}
@Mapper
public interface StockMapper {
    @Update("update stock set stock=stock-${number} where goods_id=#{goodsId}")
    void updateStock(@Param("goodsId") int goodsId,@Param("number") int number);
}
2、seata-order-8010模块

导入依赖

<dependencies>
    <!--        使用 Spring MVC 或 WebFlux 来构建 Web 应用程序-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--        可以使用 Nacos 服务发现来查找和访问其他服务-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--        可以使用 Feign 来调用其他服务-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--        可以使用 Spring Cloud LoadBalancer 来实现负载均衡-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
    <!--        公共模块-->
    <dependency>
        <groupId>com.fzj.spring</groupId>
        <artifactId>seata-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <!--        提供了 Seata 的集成支持-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <exclusions>
            <!--使用 <exclusions> 标签排除了 Seata 的 Spring Boot Starter 依赖项-->
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
            </exclusion>
        </exclusions>
        <!--是 Seata 的核心依赖项,用于启用 Seata 的功能-->
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.4.2</version>
    </dependency>
    <!--        使用 Nacos 配置中心来管理应用程序的配置-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!--        提供了一些基本的依赖项和配置,用于启动 Spring Cloud 应用程序-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
</dependencies>

配置文件

application.yml

spring:
  application:
    name: seata-order
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.65.3
        namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
  profiles:
    active: sql
server:
  port: 8010
seata:
  enabled: true
  enable-auto-data-source-proxy: true
  config:
    type: nacos
    nacos:
      namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
      serverAddr: 192.168.65.3:8848
      group: SEATA_GROUP
      data-id: seataServer.properties
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.65.3:8848
      group: SEATA_GROUP
      namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
  tx-service-group: seata-stock-seata-server-group  #这里名称和服务端名称对应

bootstrap.yml

spring:
  cloud:
    nacos:
      config:
        namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
        serverAddr: 192.168.65.3:8848

fegin包

@FeignClient("seata-stock")
public interface StockFeign {
    @PutMapping("/stock/{goodsId}/{number}")
    String update(@PathVariable("goodsId")int goodsId, @PathVariable("number") int number);
}

service包

public interface OrderService {
    String saveOrder(Order order);
}
@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderMapper mapper;
    @Resource
    private StockFeign feign;

//    用于在全局事务管理中声明一个方法需要进行事务管理
    @GlobalTransactional
    public String saveOrder(Order order) {
        //添加一笔订单
        mapper.saveOrder(order);
        //更新库存
        feign.update(order.getGoodsId(), order.getOrderNumber());
        return null;
    }
}

controller包

@RestController
@RequestMapping("/order")
public class OrderController {

    @Resource
    private OrderService service;

    @PostMapping("/save")
    public String save(Order order) {
        return service.saveOrder(order);
    }
}

启动类

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class SeataOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderApplication.class,args);
    }
}
3、seata-stock-8020模块

导入依赖

 <dependencies>
<!--        使用 Spring MVC 或 WebFlux 来构建 Web 应用程序-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        可以使用 Nacos 服务发现来查找和访问其他服务-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
<!--        可以使用 Feign 来调用其他服务-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
<!--        可以使用 Spring Cloud LoadBalancer 来实现负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
<!--        公共模块-->
        <dependency>
            <groupId>com.fzj.spring</groupId>
            <artifactId>seata-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--        提供了 Seata 的集成支持-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <!--使用 <exclusions> 标签排除了 Seata 的 Spring Boot Starter 依赖项-->
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
            <!--是 Seata 的核心依赖项,用于启用 Seata 的功能-->
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>
<!--        使用 Nacos 配置中心来管理应用程序的配置-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
<!--        提供了一些基本的依赖项和配置,用于启动 Spring Cloud 应用程序-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>

配置文件

application.yml

spring:
  application:
    name: seata-stock
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.65.3
        namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
  profiles:
    active: sql
server:
  port: 8020
seata:
  enabled: true
  enable-auto-data-source-proxy: true
  config:
    type: nacos
    nacos:
      namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
      serverAddr: 192.168.65.3:8848
      group: SEATA_GROUP
      data-id: seataServer.properties
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.65.3:8848
      group: SEATA_GROUP
      namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
  tx-service-group: seata-stock-seata-server-group  #这里名称和服务端名称对应

bootstrap.yml

spring:
  cloud:
    nacos:
      config:
        namespace: a1225c08-7b57-4ed2-a94e-18e85418193f
        serverAddr: 192.168.65.3:8848

service包

public interface StockService {
    String updateStock(int goodsId,int number);
}
@Service
public class StockServiceImpl implements StockService {
    @Resource
    private StockMapper mapper;

//    用于在全局事务管理中声明一个方法需要进行事务管理
    @GlobalTransactional
    public String updateStock(int goodsId, int number) {
        try {
            mapper.updateStock(goodsId,number);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return "success";
    }
}

controller包

@RestController
@RequestMapping("/stock")
public class StockController {

    @Resource
    private StockService service;

    @PutMapping("/{goodsId}/{number}")
    public String update(@PathVariable("goodsId")int goodsId, @PathVariable("number") int number){
        return service.updateStock(goodsId,number);
    }
}

启动类

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class SeataStockApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataStockApplication.class,args);
    }
}
测试结果

启动项目

image-20230911183351344

ApiPost测试结果

image-20230911185014565

控制台输出

image-20230911185200822

查询数据库

image-20230911185348382

image-20230911185414140

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值