分布式事务 Seata
- 事务介绍
- 分布式理论
- Seata 介绍
- Seata 部署与集成
- Seata TC Server 部署
- 微服务集成 Seata
- XA 模式
- AT 模式
- AT 模式执行过程
- 读写隔离
- 写隔离
- 读隔离
- 实现 AT 模式
- TCC 模式
- TCC 模式介绍
- 实现 TCC 模式
- Saga 模式
- Seata 四种模式对比
事务介绍
事务(Transaction)是计算机科学中的一个重要概念,主要是指一个完整的、不可分割的操作序列。在关系型数据库中,事务通常用于描述对数据库进行的一系列操作的执行单元。
事务的ACID特性:
- 原子性(Atomicity):事务是一个原子操作,要么全部执行,要么全部回滚。如果事务中的任何一步操作失败,那么整个事务都必须被回滚,以保证数据库的一致性。
- 一致性(Consistency):事务执行的结果必须使数据库从一个一致性状态变为另一个一致性状态。这意味着在事务执行期间,数据库中的数据必须遵循所有的约束、规则和触发器等。
- 隔离性(Isolation):多个事务同时执行时,应该相互隔离,即每个事务都应该独立地执行,不应该受到其他事务的干扰。
- 持久性(Durability):当一个事务成功提交后,其结果应该永久地保存在数据库中,即使发生系统故障或断电等异常情况,也不会丢失。
根据不同的隔离级别,事务可以分为多种类型。常见的隔离级别包括:
- 读未提交(Read Uncommitted):一个事务可以读取另一个事务尚未提交的数据,容易出现脏读、不可重复读和幻读等问题。
- 读已提交(Read Committed):一个事务只能读取另一个事务已经提交的数据,可以避免脏读问题,但可能导致不可重复读和幻读等问题。
- 可重复读(Repeatable Read):一个事务在执行期间,所读取的数据应该保持不变,即不能读取其他事务已经提交的新数据,可以避免不可重复读问题,但可能导致幻读问题。
- 序列化(Serializable):一个事务完全串行化执行,即所有并发的事务都按照一定的顺序执行,可以避免所有并发问题,但可能导致性能较低。
分布式事务:指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
分布式事务的产生是由于分库分表,服务的拆分等产生的,一个业务逻辑要操纵不同节点的多个数据库。
分布式理论
CAP定理
CAP理论是指在分布式系统中,当出现网络故障或节点故障时,系统无法同时满足以下三个特性:
- 一致性(Consistency):所有节点上的数据必须保持一致性,即在一个事务执行完毕后,所有节点的数据都必须更新为最新的值。
- 可用性(Availability):系统的响应时间必须满足用户的需求,即使在部分节点出现故障的情况下,系统仍然能够响应用户的请求。
- 分区容错性(Partition Tolerance):当节点之间的通信出现故障时,系统必须能够继续运行,并且保证数据的一致性和可用性。
根据CAP理论,当分布式系统出现网络故障或节点故障时(分区容错性),系统只能同时满足两个特性,要么满足可用性,舍弃一致性,要么满足一致性,放弃可用性。因此,在设计分布式系统时,需要根据具体的应用场景和需求来选择适当的特性进行权衡和取舍。
BASE理论
BASE理论也是分布式事务理论的一个重要理论。BASE理论是指在一个分布式系统中,当出现网络故障或节点故障时,系统应该满足以下三个特性:
- 基本可用(Basically Available):系统必须保证基本的可用性,即在出现故障时,系统仍然能够提供有限的服务。
- 软状态(Soft State):系统应该允许状态在一定时间内发生变化,而不是强制要求保持一致性。
- 最终一致性(Eventually Consistency):系统应该最终达到一致性状态,即所有节点的数据最终都应该保持一致。
与CAP理论不同的是,BASE理论强调了系统的可用性和软状态的重要性,并允许系统在一定时间内存在不一致性的情况。这种理论更加适合于处理大规模的、高可用的分布式系统。
解决分布式事务,各个分支事务必须能感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调者来协调每一个分支事务。有关联的各个分支事务在一起称为全局事务。
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
- AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措恢复数据即可,实现最终一致。
- CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据
强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚
Seata 介绍
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata官网,其中的文档、播客中提供了大量的使用说明、源码分析。
Seata 提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
Seata术语
在Seata的架构中,一共有三个角色:
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
在Seata中,一个分布式事务的生命周期如下:
- TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID(此时,由TM发起的全局事务已经开启)
- XID 通过服务的调用链传递到其他服务
- RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖(事务参与者执行本地事务,此时分支事务已经执行完成,并反馈给TC执行结果。可以理解为AT模式下的第一个阶段)
- TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议(事务协调者根据事务管理者的决议,发送提交或回滚的调度命令,可以理解为AT模式下的第二阶段)
- TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作
Seata 部署与集成
Seata TC Server下载地址
解压完成目录如下:
Seata TC Server 部署
修改conf 目录下的 registry.conf 文件,设置注册中心和配置中心
registry.conf
registry {# 注册中心 file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos"nacos {application = "seata-server"serverAddr = "127.0.0.1:8848"group = "DEFAULT_GROUP"namespace = ""cluster = "default"username = "nacos"password = "nacos"}
}config {#配置中心 file、nacos 、apollo、zk、consul、etcd3type = "nacos"nacos {serverAddr = "127.0.0.1:8848"namespace = ""group = "SEATA_GROUP"username = "nacos"password = "nacos"dataId = "seataServer.properties"}
}
启动 nacos,在配置中心新建配置文件。dataId 与 Group 要和 registry.conf 中的名称一致。
seataServer.properties 内容
其中的数据库地址、用户名、密码都需要修改成你自己的数据库信息。
# 数据存储方式,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=123456
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
创建数据库表
TC 服务在管理分布式事务时,需要记录事务相关数据到数据库中,需要提前创建好这些表。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
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;-- ----------------------------
-- Records of branch_table
-- ------------------------------ ----------------------------
-- Table structure for global_table
-- ----------------------------
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;-- ----------------------------
-- Records of global_table
-- ------------------------------ ----------------------------
-- Records of lock_table
-- ----------------------------SET FOREIGN_KEY_CHECKS = 1;
点击 运行 Seata TC Server
在 nacos 中服务列表可以看到 Seata TC Server 服务已经成功注册
微服务集成 Seata
在微服务中引入 Seata 相关依赖,需要使用 Seata 的每一个微服务都要进行配置。
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><!--版本较低,1.3.0,因此排除--><exclusion><artifactId>seata-spring-boot-starter</artifactId><groupId>io.seata</groupId></exclusion></exclusions>
</dependency>
<dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.4.2</version>
</dependency>
配置 application.yml,让微服务通过注册中心找到 seata-tc-server。
seata:# TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址# 参考tc服务自己的registry.conf中的配置registry:type: nacosnacos:# 地址、namespace、group、application-name、clusterserver-addr: 127.0.0.1:8848namespace: "" # 默认值为 publicgroup: DEFAULT_GROUPapplication: seata-serverusername: nacospassword: nacostx-service-group: seata-demo # 事务组名称service:vgroup-mapping: # 事务组与cluster的映射关系seata-demo: default
XA 模式
SEATA 的 XA 模式是一种基于 XA 协议的事务处理模式。XA 协议是一种由 X/Open 和 ISO 定义的分布式事务协议,它规定了事务管理器(Transaction Manager,TM)和资源管理器(Resource Manager,RM)之间的交互方式。
在 SEATA 的 XA 模式下,事务管理器会将事务划分为两个阶段:事务开始(Begin Phase)和 事务提交(Commit Phase)。
在事务开始阶段,事务管理器会锁定所有需要操作的资源,并在资源管理器中注册事务。
在事务提交阶段,事务管理器会通知所有资源管理器共同提交事务。
第一阶段执行各个分支事务执行成功,在第二阶段所有分支事务进行提交。
在第一阶段如果有分支事务执行失败,则第二阶段所有分支事务回滚。
XA 模式执行过程
RM 一阶段工作:
- 注册分支事务到 TC
- 执行分支业务 sql,但不提交
- 报告事务状态到 TC
TC 二阶段工作:
- TC检测各分支事务执行状态
- 如果都成功,通知所有 RM 提交事务
- 如果有失败,通知所有 RM 回滚事务
RM 二阶段工作:
- 接收TC指令,提交或回滚事务
XA 模式的优点:
- 事务的一致性强:由于 XA 协议的规定,所有资源管理器都会在事务提交前保证事务的一致性。
- 支持多种数据库:由于 XA 协议是一种标准协议,因此可以使用多种不同的资源管理器来实现事务管理。
- 实现简单:SEATA 的 XA 模式可以使用简单的 XML 配置来实现,无需编写大量的代码。
- 无代码侵入:使用 SEATA 的 XA 模式可以最小化代码侵入,只需在应用程序中添加少量的 SEATA 依赖项。
XA 模式的缺点:
- 性能较差:由于在事务开始阶段需要锁定所有资源,因此可能会影响性能。
- 依赖关系型数据库实现回滚:如果某些资源管理器无法回滚事务,那么整个事务可能会失败。
XA 模式的实现
Seata 的 starter 已经完成了 XA 模式的自动装配,实现非常简单,步骤如下:
1.修改application.yml文件(每个参与事务的微服务),开启 XA模式:
seata:data-source-proxy-mode: XA # 开启数据源代理的XA模式
2.给发起全局事务的入口方法添加 @GlobalTransactional 注解
@GlobalTransactional
public Long create(Order order) {// 创建订单orderMapper.insert(order);try {// 扣用户余额accountClient.deduct(order.getUserId(), order.getMoney());// 扣库存storageClient.deduct(order.getCommodityCode(), order.getCount());} catch (FeignException e) {log.error("下单失败,原因:{}", e.contentUTF8(), e);throw new RuntimeException(e.contentUTF8(), e);}return order.getId();
}
3.重启服务测试
当库存不足时,扣除的余额会回滚。
AT 模式
AT 模式执行过程
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
在AT模式下,用户只需关注自己的业务SQL,SEATA框架会自动生成事务的二阶段提交和回滚操作。
在一阶段,SEATA会拦截业务SQL,首先解析SQL语义,找到业务SQL要更新的业务数据,在更新前后保存数据快照,以便在二阶段回滚时使用。
在回滚阶段,SEATA需要回滚一阶段已经执行的业务SQL,还原业务数据。
AT模式执行过程:
RM 一阶段的工作:
- 注册分支事务到 TC
- 记录 undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态到 TC
TC 二阶段的工作:
- TC检测各分支事务执行状态
- 如果都成功,通知所有 RM 提交事务
- 如果有失败,通知所有 RM 回滚事务
RM二阶段的工作:
- 如果提交,则删除 undo-log(数据快照)
- 如果回滚,则根据 undo-log(数据快照)恢复数据到更新前
AT模式与XA模式最大的区别:
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交事务,不锁定资源
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
- XA模式强一致;AT模式最终一致。
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多
读写隔离
脏写问题:指的是在并发控制中,多个事务同时更新同一行数据,其中一个事务在更新数据后,还没有提交或回滚之前,另一个事务又对该行数据进行更新,导致之前的数据被覆盖或更改,从而导致数据的不一致。
事务1在一阶段提交后,释放了DB锁,事务2 在事务1后获得了DB锁,对数据进行了更改,并提交,释放了DB锁。事务1 在二阶段根据数据快照进行回滚,将事务2 的修改进行了覆盖。
写隔离
全局锁:由 TC 记录当前正在操作某行数据的事务,该事务持有全局锁,具备执行权。
全局锁解决脏写问题:
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到全局锁 ,不能提交本地事务。
- 拿全局锁的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
示例:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁 。
tx1 二阶段全局提交,释放全局锁 。tx2 拿到全局锁提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
读隔离
在数据库本地事务隔离级别读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是读未提交(Read Uncommitted)** 。
如果应用在特定场景下,必需要求全局的读已提交,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请全局锁 ,如果全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁拿到,即读取的相关数据是已提交的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
实现 AT 模式
lock_table表导入到 Seata TC Server 服务相关联的数据库,lock_table 用来保存全局锁的信息。
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
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表导入微服务相关联的数据库,undo_log表用来保存数据快照信息。
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
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;
1.修改application.yml文件,将事务模式修改为AT模式即可:
seata:data-source-proxy-mode: AT # 开启数据源代理的AT模式
2.给发起全局事务的入口方法添加 @GlobalTransactional 注解
3.重启服务测试
TCC 模式
TCC 模式介绍
Seata TCC模式的核心思想是基于二阶段提交协议(Try-Confirm-Cancel),将分布式事务拆分为两个阶段:Try阶段 和 Confirm/Cancel 阶段。
第一阶段(Try阶段):尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性);
第二阶段(Confirm/Cancel阶段):确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性,Cancel操作满足幂等性。
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为 Try 的反向操作。
TCC的工作模型图:
TCC的优点
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点
- 有代码侵入,需要编写 Try、Confirm 和 Cancel 接口
- 软状态,事务是最终一致
- 需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理
TCC的空回滚和业务悬挂
空回滚:当某分支事务的 Try 阶段阻塞时,可能导致全局事务超时而触发二阶段的 Cancel 操作。在未执行 Try 操作时先执行了 Cancel 操作。
业务悬挂:对于已经空回滚的业务,如果以后继续执行 Try,就永远不可能 Confirm 或 Cancel,这就是业务悬挂。
实现 TCC 模式
业务描述:在账户表扣减余额,然后去库存表扣减商品,完成业务,如果扣减库存失败,则将账户表余额进行回滚。
在数据库中增加账户冻结表,用于记录冻结的金额。
账户冻结表结构
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;
Try业务:添加冻结金额,扣减可用金额
Confirm业务:删除冻结金额
Cancel业务:删除冻结金额,恢复可用金额
在实现中需要保证Confirm、Cancel 接口的幂等性,允许空回滚,拒绝业务悬挂。
TCC的 Try、Confirm、Cancel 方法都需要在接口中基于注解来声明,语法如下:
@LocalTCC
public interface TCCService {/*** * Try逻辑* @TwoPhaseBusinessAction 中的name属性要与当前方法名一致,用于指定 Try 逻辑对应的方法* @BusinessActionContextParameter 设置上下文参数,可以在confirm,cancel方法中使用*/@TwoPhaseBusinessAction(name = "prepare", commitMethod = "confirm", rollbackMethod = "cancel")void prepare(@BusinessActionContextParameter(paramName = "param") String param);/**** 二阶段confirm确认方法、可以另命名,但要保证与commitMethod一致* @param context 上下文,可以传递Try方法的参数* @return boolean 执行是否成功*/boolean confirm (BusinessActionContext context);/**** 二阶段回滚方法,要保证与rollbackMethod一致*/boolean cancel (BusinessActionContext context);
}
声明接口
@LocalTCC
public interface AccountTCCService {@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,@BusinessActionContextParameter(paramName = "money") int money);boolean confirm(BusinessActionContext ctx);boolean cancel(BusinessActionContext ctx);
}
实现类
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {@Autowiredprivate AccountMapper accountMapper;@Autowiredprivate AccountFreezeMapper freezeMapper;@Override@Transactionalpublic void deduct(String userId, int money) {// 0.获取事务idString xid = RootContext.getXID();// 判断 freeze 中是否有冻结记录,如果有,一定是 CANCEL执行过,要拒绝执行 Try 业务AccountFreeze oldFreeze = freezeMapper.selectById(xid);if (oldFreeze != null) {// CANCEL执行过,拒绝执行 Try 业务,避免业务悬挂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);}@Overridepublic boolean confirm(BusinessActionContext ctx) {// 1.获取事务idString xid = ctx.getXid();// 2.根据id删除冻结记录int count = freezeMapper.deleteById(xid);return count == 1;}@Overridepublic boolean cancel(BusinessActionContext ctx) {// 0.查询冻结记录String xid = ctx.getXid();String userId = ctx.getActionContext("userId").toString();AccountFreeze freeze = freezeMapper.selectById(xid);// 查询冻结表,存在说明已经执行过 try 逻辑,不存在做空回滚AccountFreeze freeze = freezeMapper.selectById(xid);if (freeze == null) {// 证明 try 没执行,需要空回滚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 了,无需重复处理return true;}// 1.恢复可用余额accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());// 2.将冻结金额清零,状态改为CANCELfreeze.setFreezeMoney(0);freeze.setState(AccountFreeze.State.CANCEL);int count = freezeMapper.updateById(freeze);return count == 1;}
}
Saga 模式
Saga模式是SEATA提供的长事务解决方案。
分为两个阶段:
- 一阶段:直接提交本地事务
- 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚
Saga模式优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不用编写TCC中的三个阶段,实现简单
Saga模式缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会有脏写
Saga 模式不是很常用,使用示例省略