SpringCloud-快速通关(三)
七、Seata - 分布式事务
官网:https://seata.apache.org/zh-cn/
seata服务器的web界面的端口是7091,而8091是TC协调者的TCP端口
7.1、环境搭建
7.1.1、简介
seata
有服务器端和客户端,客户端要连上服务器才能使用。- TC(事务协调者)在服务器端:得官网下载: 全局事务的管理者。用于维护全局和分支事务的状态,驱动TM的全局提交和回滚。TM和RM通过TC注册分支和汇报状态。
- TM(事务管理器)在客户端:发起全局事务,定义全局事务的范围,操作全局事务的提交和回滚。
- RM(资源管理器)在客户端:操作自己分支的事务提交和回滚。
- 注意:
seata
的稳定性非常重要,如果TC崩了,那所有的事务管控都失效。
7.1.2、环境搭建
微服务
下载
seata
工程文件,导入到项目中,并在 services 中添加 module 聚合
SQL
在mysql
中执行sql创建库表
CREATE DATABASE IF NOT EXISTS `storage_db`;
USE `storage_db`;
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO storage_tbl (commodity_code, count) VALUES ('P0001', 100);
INSERT INTO storage_tbl (commodity_code, count) VALUES ('B1234', 10);
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `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;
CREATE DATABASE IF NOT EXISTS `order_db`;
USE `order_db`;
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `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;
CREATE DATABASE IF NOT EXISTS `account_db`;
USE `account_db`;
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO account_tbl (user_id, money) VALUES ('1', 10000);
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `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;
7.1.3、seata-server
解压并启动:
seata-server.bat
;访问:http://localhost:7091/
账号密码都是seata
7.1.4、微服务配置
在微服务中导入spring-cloud-starter-alibaba-seata依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
在resources里配置file.conf
文件,每个seata服务都配置
service {
#transaction service group mapping
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
file.conf
文件完整内容如下:【微服务只需要复制 service
块配置即可】
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
#
transport {
# tcp, unix-domain-socket
type = "TCP"
#NIO, NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the tm client batch send request enable
enableTmClientBatchSendRequest = false
# the rm client batch send request enable
enableRmClientBatchSendRequest = true
# the rm client rpc request timeout
rpcRmRequestTimeout = 2000
# the tm client rpc request timeout
rpcTmRequestTimeout = 30000
# the rm client rpc request timeout
rpcRmRequestTimeout = 15000
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
vgroupMapping.default_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
tableMetaCheckerInterval = 60000
reportSuccessEnable = false
sagaBranchRegisterEnable = false
sagaJsonParser = "fastjson"
sagaRetryPersistModeUpdate = false
sagaCompensatePersistModeUpdate = false
tccActionInterceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
sqlParserType = "druid"
branchExecutionTimeoutXA = 60000
connectionTwoPhaseHoldTimeoutXA = 10000
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
defaultGlobalTransactionTimeout = 60000
degradeCheck = false
degradeCheckPeriod = 2000
degradeCheckAllowTimes = 10
interceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
}
undo {
dataValidation = true
onlyCareUpdateColumns = true
logSerialization = "jackson"
logTable = "undo_log"
compress {
enable = true
# allow zip, gzip, deflater, lz4, bzip2, zstd default is zip
type = zip
# if rollback info size > threshold, then will be compress
# allow k m g t
threshold = 64k
}
}
loadBalance {
type = "XID"
virtualNodes = 10
}
}
log {
exceptionRate = 100
}
tcc {
fence {
# tcc fence log table name
logTableName = tcc_fence_log
# tcc fence log clean period
cleanPeriod = 1h
}
}
7.1.5、测试
- 在RM分支事务微服务中启动类上加入
@EnableTransactionManagement
,在具体事务service方法上加@Transactional
- 在TM全局事务微服务中启动类上不加
@EnableTransactionManagement
,但在事务service方法上要加上@GlobalTransactional
。这个注解是核心 - 测试结果
测试本地事务:没有@EnableTransactionManagement
,@Transactional
两注解
有@Transactional
,但没有@EnableTransactionManagement
会产生一个问题:没有@EnableTransactionManagement
,异常数据也能回滚,为什么?
在 Spring Boot 中,不需要 @EnableTransactionManagement
也能回滚数据,因为 Spring Boot 默认开启了事务管理。 如果你在 Spring 传统项目里,可能就需要手动启用事务管理。
所以还是启动类加上@EnableTransactionManagement
有@EnableTransactionManagement
,@Transactional
两注解
seata-account
, seata-storage
单个本地事务是有效的,但是对于seata-order
要远程调用seata-account
,有异常订单不会创建,但是远程调用的余额扣减却成功了。
这就导致:分布式事务用这两个注解不管用,怎么办?
修改seata-order
去掉@EnableTransactionManagement
将@Transactional
改成@GlobalTransactional
效果:seata-order
出现异常,订单没创建,远程调用账户扣减数据能回滚
再测试有@EnableTransactionManagement
,有@GlobalTransactional
效果:和上面一样。再一次证明spring boot默认开启事务
测试分布式事务——模拟情景
采购服务要远程调用 扣库存和下订单服务,订单服务又要远程调用扣减余额服务,怎么保证分布式事务的数据一致性?
操作:
- 先在各个模块启动类上加入
@EnableTransactionManagement
,全局事务(采购模块)除外。 - 各分支事务加
@Transactional
,全局事务加@GlobalTransactional
- 在订单模块模拟错误进行测试
情景:采购服务中事务上没有加@GlobalTransactional
情况,远程调用扣减库存服务和创建订单服务(订单服务要远程调用扣减余额服务)。模拟订单服务出错,看扣减余额远程调用能否回滚
结果:库存扣减,余额扣减,订单没有生成;只有订单服务回滚,其他皆无效。
其他
如果导入了seata的依赖:spring-cloud-starter-alibaba-seata
所在模块的file.conf
配置文件先自动配置,不行再将格式改成properties
7.2、事务模式
seata各模式原理
区分狭义事务和广义事务
狭义事务:数据库的操作
广义事务:业务的事务(包括数据库操作),举例:事务中需要发邮件和发短信,这种不能撤回的情况,AT和XA模式不管用了。
7.2.1、AT模式
图片里是AT模式的二阶段提交协议,其他模式也是二阶段提交(一阶段:本地事务 二阶段:提交/回滚 );
AT:系统默认使用,各分支事务要经过两阶段提交协议。
二阶提交协议原理
第一阶段:本地提交
- 生成前镜像:将要操作的数据记录下来
- 执行SQL操作数据(期间启动MySQL的行锁,先对要操作的数据select …for update(此时数据被锁住,普通的select可以读取,要看隔离级别。还有如果读操作的select…for update这种想加锁的读是不可以的),再执行后续sql,执行完则释放行锁)
- 生成后镜像:将操作后的数据记录下
- 前镜像和后镜像等待保存到uodo_log日志表中
- 向TC注册分支,在TC中申请一个全局锁,锁定操作的数据防止其他人操作,读同上,有锁才能操作。注意这里TC的全局锁不是MySQL的全局锁,MySQL的全局锁是锁整个数据库,而TC的全局锁相当于MySQL的行级别锁,只锁操作的数据。
- 本地事务提交;将业务数据和uodo_log日志表数据一起保存到当前事务的的表中
- 和TC汇报事务执行状态
第二阶段:
- 若各分支事务都成功:删除uodo_log记录
- TC能感知到每个事务的状态,通知他们进行提交
- 给异步任务对列添加异步任务,异步+批量删除对应的uodo_log日志表的记录
- 若某个事务失败,TC会通知所有分支事务回滚:拿到前镜像,数据恢复,删除uodo_log记录
- 先找到uodo_log记录(通过XID,BranchID)
- 数据校验,后镜像和当前数据是否一致,一致就ok执行回滚;不一致说明当前数据被其他操作给篡改了,需要配置相应的策略(怎么处理这个脏数据,忽略还是人工处理?之类)
- 若一致,则回滚数据到前镜像的内容,完成后删除uodo_log记录
- 只要有分支事务没处理完,全局锁会一直存在。但是第一阶段执行事务是真正提交了的,不会在第二阶段一直阻塞数据库。
7.2.2、XA模式
第一阶段不会提交数据,阻塞事务请求,在第二阶段确认提交再提交或者回滚。全局锁+MySQL行锁在第一阶段就开启,事务一开始就用阻塞模式,性能差。AT和XA区别是AT第一阶段执行完SQL释放行锁,XA是到第二阶段才提交SQL导致行锁从开始到最后,阻塞时间长性能差。但二者都是一直持有seata
的全局锁的。
7.2.3、TCC模式
主要是广义上的事务,需要写侵入式的代码。举例业务需要三个事务,一个事务改数据库,一个发短信,一个发邮件,这就用AT和XA行不通了,无法回滚,如果全局事务失败,只能进行补偿性操作,例如再发邮件和短信提醒对方扣款失败或者订单失败等。
7.2.4、saga模式
用于长事务,一时半会执行不完的事务。例如请假审批,其他模式都用了锁,如果长期锁在那是对系统是非常大的阻塞。saga是基于消息队列做的,后续有替代方案,所以这个几乎不用。
7.3 小节
debug查看seata执行流程
seata要点:
- 使用。引入
alibaba-seata
依赖,只要在分布式事务的方法加@GlobalTransactional
,在其他本地事务加@EnableTransactionManagement
,@Transactional
即可。这样分布式事务中无论哪个环节异常,都会回滚。 - 原理,以及四种模式的特点
参考链接:
https://blog.csdn.net/weixin_56884174/article/details/145573890
https://www.yuque.com/leifengyang/sutong/oz4gbyh5maa0rmxu#tHTwd