若文章内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
- 学习资料链接
SpringCloud 微服务技术栈_实用篇②_黑马旅游案例
本地事务,也就是传统的单机事务。
在传统数据库事务中,必须要满足以下四个原则:原子性、一致性、隔离性、持久性
分布式事务
例如
在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。
例如电商行业中比较常见的下单付款案例,包括下面几个行为:
完成上面的操作需要访问三个不同的微服务和三个不同的数据库。
订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证 ACID 原则。
但是当我们把三件事情看做一个 “业务”,要满足保证 “业务” 的原子性
此时 ACID 难以满足,这是分布式事务要解决的问题
我们通过一个案例来演示分布式事务的问题
seata_demo
,然后导入课前资料提供的 SQL 文件数据库中有三张表,表结构和表中内容如下
此处只是演示,所以没有创建三个数据库,但三个表之间也没有外键联系,这里演示用还是行的通的。
导入项目 seata-demo
的项目结构如下
seata-demo
:父工程,负责管理项目依赖
account-service
:账户服务,负责管理用户的资金账户。提供扣减余额的接口storage-service
:库存服务,负责管理商品库存。提供扣减库存的接口order-service
:订单服务,负责管理订单。创建订单时,需要调用 account-service
和 storage-service
在 ~\nacos\bin
目录下启动 nacos 的单机模式
startup.cmd -m standalone
请求如下
curl --location --request POST 'http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&count=20&money=200'
这里是借助了 Postman 工具来测试
经过测试发现:当库存不足时,如果余额已经扣减,并不会回滚,这就是出现了分布式事务问题。
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
Eric Brewer 说,分布式系统无法同时满足这三个指标。这个结论就叫做 CAP 定理。
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
比如现在包含两个节点,其中的初始数据是一致的
当我们修改其中一个节点的数据时,两者的数据产生了差异
要想保住一致性,就必须实现 node01 到 node02 的数据 同步
Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
如图,有三个节点的集群,访问任何一个都可以及时得到响应
当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
在分布式系统中,系统间的网络不能 100% 保证健康,一定会有故障的时候,而服务有必须对外保证服务。
因此 Partition Tolerance 不可避免。
当节点接收到新的数据变更时,就会出现问题了
如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那 node01、node02 与node03 之间就会出现数据不一致。
也就是说,在 P 一定会出现的情况下,A 和 C 之间只能实现一个。
BASE 理论是对 CAP 的一种解决思路,包含三个思想
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴 CAP 定理和 BASE 理论,有两种解决思路
但不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC)
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为 全局事务。
简述 CAP 定理内容
思考:elasticsearch 集群是 CP 还是 AP?
简述 BASE 理论三个思想
解决分布式事务的思想和模型
Seata 是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。
致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/
该网站的文档、播客中提供了大量的使用说明、源码分析。
Seata 事务管理中有三个重要的角色
Seata 基于上述架构提供了四种不同的分布式事务解决方案:
无论哪种方案,都离不开 TC,也就是事务的协调者。
参考课前资料提供的文档 seata的部署和集成.md
部署 Seata 的 tc-server |
首先我们要下载 seata-server 包
当然,课前资料也准备好了
在非中文目录解压缩这个 zip 包,其目录结构如下
修改 conf
目录下的 registry.conf
文件
内容如下
registry {
# tc 服务的注册中心类,这里选择 nacos,也可以是 eureka、zookeeper 等
type = "nacos"
nacos {
# seata tc 服务注册到 nacos 的服务名称,可以自定义
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP" # 要和项目中开的三个服务的组名相同
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 读取 tc 服务端的配置文件的方式,这里是从 nacos 配置中心读取,这样如果 tc 是集群,可以共享配置
type = "nacos"
# 配置 nacos 地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP" # 这是配置管理的一个组,可以和上面的不一样
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
特别注意:为了让 tc 服务的集群可以共享配置,我们选择了 nacos 作为统一配置中心。
因此服务端配置文件 seataServer.properties
文件需要在 nacos 中配好。
GROUP 是 DEFAULT_GROUP
格式如下:
上面的界面中,需要填写的配置内容如下
# 数据存储方式,db 代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭 metrics 功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
上面的配置中的数据库地址、用户名、密码都需要修改成你自己的数据库信息。
视频中用的是 MySQL5,而我用的是 MySQL8,所以我这里还需要改两个地方
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=Asia/Shanghai&useUnicode=true&rewriteBatchedStatements=true
store.db.user=username # 你自己设置的用户名
store.db.password=password # 你自己设置的密码
特别注意:tc 服务在管理分布式事务时,需要记录事务相关数据到数据库中,你需要提前创建好这些表。
新建一个名为 seata 的数据库,运行课前资料提供的 sql 文件
这些表主要记录全局事务、分支事务、全局锁信息
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
进入 bin
目录,运行其中的 seata-server.bat
即可
启动成功后,seata-server
应该已经注册到 nacos 注册中心了。
打开浏览器,访问 nacos 地址:http://localhost:8848
,然后进入服务列表页面,可以看到 seata-tc-server
的信息
我们以 order-service
为例来演示。
首先,在 order-service
中引入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<exclusions>
<exclusion>
<artifactId>seata-spring-boot-starterartifactId>
<groupId>io.seatagroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>${seata.version}version>
dependency>
在 order-service
中的 application.yml
中,配置 TC 服务信息,通过注册中心 nacos,结合服务名称获取 TC 地址
seata:
registry: # TC 服务注册中心的配置,微服务根据这些信息去注册中心获取 tc 服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos 地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是 DEFAULT_GROUP
application: seata-tc-server # seata 服务名称
username: nacos
password: nacos
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping: # 事务组与 cluster 的映射关系
seata-demo: SH
微服务如何根据这些配置寻找 TC 的地址呢?
我们知道注册到 Nacos 中的微服务,确定一个具体实例需要四个信息
namespace
:命名空间group
:分组application
:服务名cluster
:集群名以上四个信息,在刚才的 yaml 文件中都能找到
namespace 为空,就是默认的 public
结合起来,TC 服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH
这样就能确定 TC 服务集群了。然后就可以去 Nacos 拉取对应的实例信息了。
当正在运行的 seata-server.bat
的终端出现如下信息时,就说明微服务集成 Seata 成功
RM register success,\
message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/seata_demo', \
applicationId='account-service', \
transactionServiceGroup='seata-demo'},\
channel:[id: 0xbc148488, \
L:/IPd地址:8091 - R:/IP地址:15452],\
client version:1.4.2
nacos 服务名称组成包括?
seata 客户端获取 tc 的 cluster 名称方式?
tx-group-service
的值为 key 到 vgroupMapping 中查找下面学习下 Seata 中的四种不同的事务模式。
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准
XA 规范 描述了全局的 TM 与局部的 RM 之间的接口
几乎所有主流的数据库都对 XA 规范 提供了支持。
XA 是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
Seata 对原始的 XA 模式做了简单的封装和改造,以适应自己的事务模型
其基本架构如下图所示
RM 一阶段的工作
TC 二阶段的工作
RM 二阶段的工作
XA 模式的优点是什么?
XA 模式的缺点是什么?
Seata 的 starter 已经完成了 XA 模式的自动装配,实现非常简单,步骤如下
application.yml
文件(每个参与事务的微服务),开启 XA 模式seata:
data-source-proxy-mode: XA # 开启数据源代理的 XA 模式
@GlobalTransactional
注解本例中是 OrderServiceImpl 中的 create 方法
@Override
//@Transactional
@GlobalTransactional
public Long create(Order order) {
//省略......
}
重启 order-service
,再次测试。
在库存不足的情况下,三个微服务都能成功回滚(回滚之后,订单和库存的表的数据都会恢复原样)
java.lang.NoSuchMethodException: com.mysql.cj.conf.PropertySet.getBooleanReadableProperty(java.lang.String)
at java.lang.Class.getMethod(Class.java:1786)
... ...
java.lang.NullPointerException
... ...
... ...
java.sql.SQLFeatureNotSupportedException: null
... ...
核心在于 NoSuchMethodException
,找不到方法,亦即 @GlobalTransactional
注解失效
解决办法是作 MySQL 降级处理(我用的是 8.0.17,更改 maven 配置中的 MySQL 版本为 8.0.11)
<properties>
<mysql.version>8.0.11mysql.version>
properties>
现在再次重启服务,结果发现服务启动不了了…
java.sql.SQLException:
The connection property 'zeroDateTimeBehavior' acceptable values are: 'CONVERT_TO_NULL', 'EXCEPTION' or 'ROUND'.
The value 'convertToNull' is not acceptable.
不过通过输出信息就可以得知问题出在 zeroDateTimeBehavior
这里
查看服务中的 application.yml
中关于 MySQL 的配置,发现 zeroDateTimeBehavior=convertToNull
url: jdbc:mysql://localhost:3306/seata_demo?serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
解决办法就是删除掉 zeroDateTimeBehavior=convertToNull
url: jdbc:mysql://localhost:3306/seata_demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
然后服务就可以正常启动了,@GlobalTransactional
注解也生效了
AT 模式同样是分阶段提交的事务模型,不过缺弥补了 XA 模型中资源锁定周期过长的缺陷。
阶段一 RM 的工作
阶段二提交时 RM 的工作
阶段二回滚时 RM 的工作
我们用一个真实的业务来梳理下 AT 模式的原理。
比如,现在又一个数据库表,记录用户余额
id | money |
---|---|
1 | 100 |
其中一个分支业务要执行的 SQL 为
update tb_account set money = money - 10 where id = 1
AT 模式下,当前分支事务执行流程如下
一阶段
{
"id": 1,
"money": 100
}
money = 90
二阶段
{"id": 1, "money": 100}
),将快照恢复到数据库。此时数据库再次恢复为 100简述 AT 模式与 XA 模式最大的区别是什么?
在 多线程 并发 访问 AT 模式的分布式事务时,有可能出现脏写问题,如图
解决思路就是引入 全局锁 的概念。
在释放 DB 锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
情景1:事务1 和 事务2 都处于 seata 管理下
情景2:事务1 归 seata 管理,事务2 不归 seata 管理
AT 模式的优点
AT 模式的缺点
AT 模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
只不过,AT 模式需要一个表来记录全局锁、另一张表来记录数据快照 undo_log。
SQL 文件(seata-at.sql
)中创建了两张表:lock_table、undo_log
将 lock_table 表导入到 TC 服务关联的数据库 seata
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
将 undo_log 表导入到微服务关联的数据库 seata_demo
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
application.yml
文件,将事务模式修改为 AT 模式即可seata:
data-source-proxy-mode: AT # 默认就是 AT
TCC 模式与 AT 模式非常相似,每阶段都是独立事务。
不同的是 TCC 通过人工编码来实现数据恢复。
需要实现三个方法
Try
:资源的检测和预留;Confirm
:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。Cancel
:预留资源释放,可以理解为 try 的反向操作。举例:一个扣减用户余额的业务。假设账户 A 原来余额是 100,需要余额扣减 30 元。
初识余额
余额充足,可以冻结
此时,总金额 = 冻结金额 + 可用金额,数量依然是 100 不变。
事务直接提交无需等待其它事务
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
需要回滚,那么就要释放冻结金额,恢复可用金额
Seata 中的 TCC 模型依然延续之前的事务架构
TCC 模式的每个阶段是做什么的?
Try
:资源检查和预留Confirm
:业务执行和提交Cancel
:预留资源的释放TCC 的优点是什么?
TCC 的缺点是什么?
当某分支事务的 try
阶段阻塞时,可能导致全局事务超时而触发二阶段的 cancel
操作。
在未执行 try
操作时先执行了 cancel
操作,这时 cancel
不能做回滚,就是空回滚。
执行 cancel
操作时,应当判断 try
是否已经执行,如果尚未执行,则应该空回滚。
对于已经空回滚的业务,之前被阻塞的 try
操作恢复,继续执行 try
,就永远不可能 confirm
或 cancel
,事务一直处于中间状态
这就是业务悬挂。
执行 try
操作时,应当判断 cancel
是否已经执行过了,如果已经执行,应当阻止空回滚后的 try
操作,避免悬挂
案例
account-service
服务,利用 TCC 实现分布式事务需求
account-service
,编写 try、confirm、cancel 逻辑try
、还是 cancel
。为了实现空回滚、防止业务悬挂,以及幂等性要求。
我们必须在数据库记录冻结金额的同时,记录当前事务 id 和执行状态。
为此我们设计了一张表
DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl` (
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
`state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
其中:
xid
:是全局事务 idfreeze_money
:用来记录用户冻结金额state
:用来记录事务状态,0 为 try,1 为 confirm,2 为 cancel那此时,我们的业务开怎么做呢?
Try
业务:
account_freeze
表account
表可用金额Confirm
业务
account_freeze
表的冻结记录Cancel
业务
account_freeze
表,冻结金额为 0,state
为 2account
表,恢复可用金额cancel
业务中,根据 xid 查询 account_freeze
,如果为 null 则说明 try
还没做,需要空回滚try
业务中,根据 xid 查询 account_freeze
,如果已经存在则证明 Cancel
已经执行,拒绝执行 try
业务接下来,我们改造 account-service
,利用 TCC 实现余额扣减功能。
TCC 的 Try、Confirm、Cancel 方法都需要在接口中基于注解来声明,
我们在 account-service
项目中的 cn.itcast.account.service
包中新建一个接口,声明 TCC 三个接口
package cn.itcast.account.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface AccountTCCService {
/**
* Try 逻辑
* CTwoPhaseBusinessaction 中的 name 属性要与当前方法名一致
* 用于指定 Try 逻辑对应的方法
*/
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money")int money);
/**
* 二阶段 confirm 确认方法、可以另命名,但要保证与 commitMethod 一致
*
* @param ctx 上下文(context),可以传递 try 方法的参数
* @return boolean 执行是否成功
*/
boolean confirm(BusinessActionContext ctx);
/**
* 二阶段回滚方法,要保证与 rollbackMethod 一致
*/
boolean cancel(BusinessActionContext ctx);
}
在 account-service
服务中的 cn.itcast.account.service.impl
包下新建一个类,实现 TCC 业务
package cn.itcast.account.service.impl;
import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper freezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
// 0.获取事务 id
String xid = RootContext.getXID();
//【悬挂】
//判断 freeze 中是否有冻结记录,如果有,一定是 cancel 执行过了,我要拒绝业务
AccountFreeze oldFreeze = freezeMapper.selectById(xid);
if (oldFreeze != null) {
return;
}
// 1.扣减可用余额
accountMapper.deduct(userId, money);
// 2.记录冻结金额,事务状态
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
// 1.获取事务 id
String xid = ctx.getXid();
// 2.根据 id 删除冻结记录
int count = freezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
// 0.查询冻结记录
String xid = ctx.getXid();
AccountFreeze freeze = freezeMapper.selectById(xid);
String userId = ctx.getActionContext("userId").toString();
// 【空回滚】的判断,判断 freeze 是否是 null
if (freeze == null) {
//若是 null,则证明 try 没有执行,需要空回滚
//不过空回滚也不是什么都不做,需要把记录记入 account_freeze 表中
freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
freezeMapper.insert(freeze);
return true;
}
// 【幂等判断】
if (freeze.getState() == AccountFreeze.State.CANCEL) {
//若为 cancel,则说明已经处理过一次 cancel 了,无需重复处理
return true;
}
// 1.恢复可用余额
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 2.将冻结金额清零,状态改为 CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
}
//@Autowired
//private AccountService accountService;
@Autowired
private AccountTCCService accountService;
Saga 模式是 Seata 即将开源的长事务解决方案,将由蚂蚁金服主要贡献。
其理论基础是 Hector & Kenneth 在 1987 年发表的论文 Sagas。
Seata 官网对于 Saga 的指南:https://seata.io/zh-cn/docs/user/saga.html
在 Saga 模式下
分布式事务执行过程中,依次执行各参与者的正向操作
Saga 也分为两个阶段
优点
缺点
我们从以下几个方面来对比四种实现
XA | AT | TCC | SAGA | |
---|---|---|---|---|
一致性 | 强一致 | 弱一致 | 弱一致 | 最终一致 |
隔离性 | 完全隔离 | 基于全局锁隔离 | 基于资源预留隔离 | 无隔离 |
代码侵入 | 无 | 无 | 有,要编写三个接口 | 有,要编写状态机和补偿业务 |
性能 | 差 | 好 | 非常好 | 非常好 |
场景 | 对一致性、隔离性有高要求的业务 | 基于关系型数据库的大多数分布式事务场景都可以 | 对性能要求较高的事务。 有非关系型数据库要参与的事务。 | 业务流程长、业务流程多 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口 |
搭建 TC 服务集群非常简单,启动多个 TC 服务,注册到 nacos 即可。
但集群并不能确保 100% 安全,万一集群所在机房故障怎么办?
所以如果要求较高,一般都会做异地多机房容灾。
比如一个 TC 集群在上海,另一个 TC 集群在杭州
微服务基于事务组(tx-service-group
)与 TC 集群的映射关系,来查找当前应该使用哪个 TC 集群。
当 SH 集群故障时,只需要将 vgroup-mapping
中的映射关系改成 HZ。
则所有微服务就会切换到 HZ 的 TC 集群了。
具体实现请参考课前资料提供的文档 seata的部署和集成.md
中的第三章节
计划启动两台 seata 的 tc 服务节点
节点名称 | ip地址 | 端口号 | 集群名称 |
---|---|---|---|
seata | 127.0.0.1 | 8091 | SH |
seata2 | 127.0.0.1 | 8092 | HZ |
之前我们已经启动了一台 seata 服务,端口是 8091,集群名为 SH。
现在,将 seata
目录复制一份,起名为 seata2
修改 seata2/conf/registry.conf
内容如下
registry {
# tc 服务的注册中心类,这里选择 nacos,也可以是 eureka、zookeeper 等
type = "nacos"
nacos {
# seata tc 服务注册到 nacos 的服务名称,可以自定义
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "HZ"
username = "nacos"
password = "nacos"
}
}
config {
# 读取 tc 服务端的配置文件的方式,这里是从 nacos 配置中心读取,这样如果 tc 是集群,可以共享配置
type = "nacos"
# 配置 nacos 地址等信息
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
进入 seata2/bin
目录,然后运行命令
seata-server.bat -p 8092
打开 nacos 控制台,查看服务列表
点进详情查看:
接下来,我们需要将 tx-service-group
与 cluster
的映射关系都配置到 nacos 配置中心。
新建一个配置
配置的内容如下
# 事务组映射关系
service.vgroupMapping.seata-demo=SH
service.enableDegrade=false
service.disableGlobalTransaction=false
# 与 TC 服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
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
# RM 配置
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=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM 配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
# undo 日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100
接下来,需要修改每一个微服务的 application.yml
文件,让微服务读取 nacos 中的 client.properties
文件
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
data-id: client.properties
重启微服务,现在微服务到底是连接 tc 的 SH 集群,还是 tc 的 HZ 集群,都统一由 nacos 的 client.properties
来决定了。