Apache Seata如何解决TCC 模式的幂等、悬挂和空回滚问题

server/2024/9/18 12:32:09/ 标签: seata, 分布式事务, 分布式, apache

title: 阿里 Seata 新版本终于解决了 TCC 模式的幂等、悬挂和空回滚问题
author: 朱晋君
keywords: [Seata、TCC、幂等、悬挂、空回滚]
description: Seata 在 1.5.1 版本解决了 TCC 模式的幂等、悬挂和空回滚问题,这篇文章主要讲解 Seata 是怎么解决的。


今天来聊一聊阿里巴巴 Seata 新版本(1.5.1)是怎么解决 TCC 模式下的幂等、悬挂和空回滚问题的。

本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。

1 TCC 回顾

TCC 模式是最经典的分布式事务>分布式事务解决方案,它将分布式事务>分布式事务分为两个阶段来执行,try 阶段对每个分支事务进行预留资源,如果所有分支事务都预留资源成功,则进入 commit 阶段提交全局事务,如果有一个节点预留资源失败则进入 cancel 阶段回滚全局事务。

以传统的订单、库存、账户服务为例,在 try 阶段尝试预留资源,插入订单、扣减库存、扣减金额,这三个服务都是要提交本地事务的,这里可以把资源转入中间表。在 commit 阶段,再把 try 阶段预留的资源转入最终表。而在 cancel 阶段,把 try 阶段预留的资源进行释放,比如把账户金额返回给客户的账户。

注意:try 阶段必须是要提交本地事务的,比如扣减订单金额,必须把钱从客户账户扣掉,如果不扣掉,在 commit 阶段客户账户钱不够了,就会出问题。

1.1 try-commit

try 阶段首先进行预留资源,然后在 commit 阶段扣除资源。如下图:

在这里插入图片描述

1.2 try-cancel

try 阶段首先进行预留资源,预留资源时扣减库存失败导致全局事务回滚,在 cancel 阶段释放资源。如下图:

在这里插入图片描述

2 TCC 优势

TCC 模式最大的优势是效率高。TCC 模式在 try 阶段的锁定资源并不是真正意义上的锁定,而是真实提交了本地事务,将资源预留到中间态,并不需要阻塞等待,因此效率比其他模式要高。

同时 TCC 模式还可以进行如下优化:

2.1 异步提交

try 阶段成功后,不立即进入 confirm/cancel 阶段,而是认为全局事务已经结束了,启动定时任务来异步执行 confirm/cancel,扣减或释放资源,这样会有很大的性能提升。

2.2 同库模式

TCC 模式中有三个角色:

  • TM:管理全局事务,包括开启全局事务,提交/回滚全局事务;
  • RM:管理分支事务;
  • TC: 管理全局事务和分支事务的状态。

下图来自 Seata 官网:

在这里插入图片描述

TM 开启全局事务时,RM 需要向 TC 发送注册消息,TC 保存分支事务的状态。TM 请求提交或回滚时,TC 需要向 RM 发送提交或回滚消息。这样包含两个个分支事务的分布式事务>分布式事务中,TC 和 RM 之间有四次 RPC。

优化后的流程如下图:

在这里插入图片描述

TC 保存全局事务的状态。TM 开启全局事务时,RM 不再需要向 TC 发送注册消息,而是把分支事务状态保存在了本地。TM 向 TC 发送提交或回滚消息后,RM 异步线程首先查出本地保存的未提交分支事务,然后向 TC 发送消息获取(本地分支事务)所在的全局事务状态,以决定是提交还是回滚本地事务。

这样优化后,RPC 次数减少了 50%,性能大幅提升。

3 RM 代码示例

以库存服务为例,RM 库存服务接口代码如下:

@LocalTCC
public interface StorageService {/*** 扣减库存* @param xid 全局xid* @param productId 产品id* @param count 数量* @return*/@TwoPhaseBusinessAction(name = "storageApi", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)boolean decrease(String xid, Long productId, Integer count);/*** 提交事务* @param actionContext* @return*/boolean commit(BusinessActionContext actionContext);/*** 回滚事务* @param actionContext* @return*/boolean rollback(BusinessActionContext actionContext);
}

通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。在 try 阶段的方法(decrease方法)上有一个 @TwoPhaseBusinessAction 注解,这里定义了分支事务的 resourceId,commit 方法和 cancel 方法,useTCCFence 这个属性下一节再讲。

4 TCC 存在问题

TCC 模式中存在的三大问题是幂等、悬挂和空回滚。在 Seata1.5.1 版本中,增加了一张事务控制表,表名是 tcc_fence_log 来解决这个问题。而在上一节 @TwoPhaseBusinessAction 注解中提到的属性 useTCCFence 就是来指定是否开启这个机制,这个属性值默认是 false。

tcc_fence_log 建表语句如下(MySQL 语法):

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(`xid`           VARCHAR(128)  NOT NULL COMMENT 'global id',`branch_id`     BIGINT        NOT NULL COMMENT 'branch id',`action_name`   VARCHAR(64)   NOT NULL COMMENT 'action name',`status`        TINYINT       NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',`gmt_create`    DATETIME(3)   NOT NULL COMMENT 'create time',`gmt_modified`  DATETIME(3)   NOT NULL COMMENT 'update time',PRIMARY KEY (`xid`, `branch_id`),KEY `idx_gmt_modified` (`gmt_modified`),KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

4.1 幂等

在 commit/cancel 阶段,因为 TC 没有收到分支事务的响应,需要进行重试,这就要分支事务支持幂等。

我们看一下新版本是怎么解决的。下面的代码在 TCCResourceManager 类:

@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,String applicationData) throws TransactionException {TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);//省略判断Object targetTCCBean = tccResource.getTargetBean();Method commitMethod = tccResource.getCommitMethod();//省略判断try {//BusinessActionContextBusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,applicationData);Object[] args = this.getTwoPhaseCommitArgs(tccResource, businessActionContext);Object ret;boolean result;//注解 useTCCFence 属性是否设置为 trueif (Boolean.TRUE.equals(businessActionContext.getActionContext(Constants.USE_TCC_FENCE))) {try {result = TCCFenceHandler.commitFence(commitMethod, targetTCCBean, xid, branchId, args);} catch (SkipCallbackWrapperException | UndeclaredThrowableException e) {throw e.getCause();}} else {//省略逻辑}LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId);return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;} catch (Throwable t) {//省略return BranchStatus.PhaseTwo_CommitFailed_Retryable;}
}

上面的代码可以看到,执行分支事务提交方法时,首先判断 useTCCFence 属性是否为 true,如果为 true,则走 TCCFenceHandler 类中的 commitFence 逻辑,否则走普通提交逻辑。

TCCFenceHandler 类中的 commitFence 方法调用了 TCCFenceHandler 类的 commitFence 方法,代码如下:

public static boolean commitFence(Method commitMethod, Object targetTCCBean,String xid, Long branchId, Object[] args) {return transactionTemplate.execute(status -> {try {Connection conn = DataSourceUtils.getConnection(dataSource);TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);if (tccFenceDO == null) {throw new TCCFenceException(String.format("TCC fence record not exists, commit fence method failed. xid= %s, branchId= %s", xid, branchId),FrameworkErrorCode.RecordAlreadyExists);}if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {LOGGER.info("Branch transaction has already committed before. idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());return true;}if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {if (LOGGER.isWarnEnabled()) {LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());}return false;}return updateStatusAndInvokeTargetMethod(conn, commitMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_COMMITTED, status, args);} catch (Throwable t) {status.setRollbackOnly();throw new SkipCallbackWrapperException(t);}});
}

从代码中可以看到,提交事务时首先会判断 tcc_fence_log 表中是否已经有记录,如果有记录,则判断事务执行状态并返回。这样如果判断到事务的状态已经是 STATUS_COMMITTED,就不会再次提交,保证了幂等。如果 tcc_fence_log 表中没有记录,则插入一条记录,供后面重试时判断。

Rollback 的逻辑跟 commit 类似,逻辑在类 TCCFenceHandler 的 rollbackFence 方法。

4.2 空回滚

如下图,账户服务是两个节点的集群,在 try 阶段账户服务 1 这个节点发生了故障,try 阶段在不考虑重试的情况下,全局事务必须要走向结束状态,这样就需要在账户服务上执行一次 cancel 操作。

在这里插入图片描述

Seata 的解决方案是在 try 阶段 往 tcc_fence_log 表插入一条记录,status 字段值是 STATUS_TRIED,在 Rollback 阶段判断记录是否存在,如果不存在,则不执行回滚操作。代码如下:

//TCCFenceHandler 类
public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {return transactionTemplate.execute(status -> {try {Connection conn = DataSourceUtils.getConnection(dataSource);boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);LOGGER.info("TCC fence prepare result: {}. xid: {}, branchId: {}", result, xid, branchId);if (result) {return targetCallback.execute();} else {throw new TCCFenceException(String.format("Insert tcc fence record error, prepare fence failed. xid= %s, branchId= %s", xid, branchId),FrameworkErrorCode.InsertRecordError);}} catch (TCCFenceException e) {//省略} catch (Throwable t) {//省略}});
}

在 Rollback 阶段的处理逻辑如下:

//TCCFenceHandler 类
public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,String xid, Long branchId, Object[] args, String actionName) {return transactionTemplate.execute(status -> {try {Connection conn = DataSourceUtils.getConnection(dataSource);TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);// non_rollbackif (tccFenceDO == null) {//不执行回滚逻辑return true;} else {if (TCCFenceConstant.STATUS_ROLLBACKED == tccFenceDO.getStatus() || TCCFenceConstant.STATUS_SUSPENDED == tccFenceDO.getStatus()) {LOGGER.info("Branch transaction had already rollbacked before, idempotency rejected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());return true;}if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {if (LOGGER.isWarnEnabled()) {LOGGER.warn("Branch transaction status is unexpected. xid: {}, branchId: {}, status: {}", xid, branchId, tccFenceDO.getStatus());}return false;}}return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);} catch (Throwable t) {status.setRollbackOnly();throw new SkipCallbackWrapperException(t);}});
}

updateStatusAndInvokeTargetMethod 方法执行的 sql 如下:

update tcc_fence_log set status = ?, gmt_modified = ?where xid = ? and  branch_id = ? and status = ? ;

可见就是把 tcc_fence_log 表记录的 status 字段值从 STATUS_TRIED 改为 STATUS_ROLLBACKED,如果更新成功,就执行回滚逻辑。

4.3 悬挂

悬挂是指因为网络问题,RM 开始没有收到 try 指令,但是执行了 Rollback 后 RM 又收到了 try 指令并且预留资源成功,这时全局事务已经结束,最终导致预留的资源不能释放。如下图:

在这里插入图片描述

Seata 解决这个问题的方法是执行 Rollback 方法时先判断 tcc_fence_log 是否存在当前 xid 的记录,如果没有则向 tcc_fence_log 表插入一条记录,状态是 STATUS_SUSPENDED,并且不再执行回滚操作。代码如下:

public static boolean rollbackFence(Method rollbackMethod, Object targetTCCBean,String xid, Long branchId, Object[] args, String actionName) {return transactionTemplate.execute(status -> {try {Connection conn = DataSourceUtils.getConnection(dataSource);TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);// non_rollbackif (tccFenceDO == null) {//插入防悬挂记录boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);//省略逻辑return true;} else {//省略逻辑}return updateStatusAndInvokeTargetMethod(conn, rollbackMethod, targetTCCBean, xid, branchId, TCCFenceConstant.STATUS_ROLLBACKED, status, args);} catch (Throwable t) {//省略逻辑}});
}

而后面执行 try 阶段方法时首先会向 tcc_fence_log 表插入一条当前 xid 的记录,这样就造成了主键冲突。代码如下:

//TCCFenceHandler 类
public static Object prepareFence(String xid, Long branchId, String actionName, Callback<Object> targetCallback) {return transactionTemplate.execute(status -> {try {Connection conn = DataSourceUtils.getConnection(dataSource);boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_TRIED);//省略逻辑} catch (TCCFenceException e) {if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {LOGGER.error("Branch transaction has already rollbacked before,prepare fence failed. xid= {},branchId = {}", xid, branchId);addToLogCleanQueue(xid, branchId);}status.setRollbackOnly();throw new SkipCallbackWrapperException(e);} catch (Throwable t) {//省略}});
}

注意:queryTCCFenceDO 方法 sql 中使用了 for update,这样就不用担心 Rollback 方法中获取不到 tcc_fence_log 表记录而无法判断 try 阶段本地事务的执行结果了。

5 总结

TCC 模式是分布式事务>分布式事务中非常重要的事务模式,但是幂等、悬挂和空回滚一直是 TCC 模式需要考虑的问题,Seata 框架在 1.5.1 版本完美解决了这些问题。

对 tcc_fence_log 表的操作也需要考虑事务的控制,Seata 使用了代理数据源,使 tcc_fence_log 表操作和 RM 业务操作在同一个本地事务中执行,这样就能保证本地操作和对 tcc_fence_log 的操作同时成功或失败。


http://www.ppmy.cn/server/25799.html

相关文章

c语言指针的应用场景

​ 1.什么是指针&#xff1f; 当我们提起指针的时候&#xff0c;可能第一反应会露出惊喜的表情 &#xff08;但是我们其实没必要那么慌&#xff0c;因为当我们随着我们学习的越来越深入就会发现&#xff0c;指针虽然看起来难&#xff0c;实际上也不怎么简单。哈哈哈开玩笑的&a…

ARP学习及断网攻击

1.什么是ARP ARP&#xff08;Address Resolution Protocol&#xff09;是一种用于在IPv4网络中将IP地址映射到MAC地址的协议。在计算机网络中&#xff0c;每个网络接口都有一个唯一的MAC地址&#xff08;Media Access Control address&#xff09;&#xff0c;用于识别网络设备…

【redis】Redis数据类型(一)——String类型(包含redis通用命令)

目录 Redis通用命令String类型常用的操作命令一些特殊命令详解setnx示例使用 setrange示例 mset示例 msetnx示例 append示例 getset示例 incr示例使用1.计数器2.限速器 bitcount示例使用&#xff1a;使用 bitmap 实现用户上线次数统计性能 String类型String类型简介String类型的…

Linux——web服务配置

一、HTTP概念 HTTP请求报文&#xff1a;客户端发送给服务器的消息&#xff0c;用于请求特定资源或执行特定操作。HTTP请求报文由 请求行、请求头部和请求正文三部分组成。 请求方法&#xff1a; HTTP 请求方法是指客户端与服务器通信时&#xff0c;客户端所请求执行的动作。常…

值得买科技新思路,导购电商的终点是“AI+出海”?

在以往&#xff0c;大众普遍认为品牌的消费者大多是高度忠诚人群&#xff0c;而事实上&#xff0c;非品牌忠诚者相比重度消费者&#xff0c;对促进品牌增长更为重要。 这类非品牌忠诚者被定义为摇摆的消费者群体&#xff0c;也就是那些购买品牌产品概率在20%-80%之间的消费者。…

使用Vue实现返回到上一个页面的时候进行参数的传递

需求&#xff1a;点击按钮进入到下一个页面&#xff0c;在新的页面进行一系列操作&#xff0c;操作完成之后点击按钮会返回到上一个页面&#xff0c;返回的时候还要携带这个页面的一些数据。 实现方式&#xff1a;使用组件内守卫&#xff0c;在组件进入到上一个页面时使用路由…

uni-app中配置自定义条件编译

前提&#xff1a;官网提供的自定义编译不满足条件 package.json | uni-app官网 下文&#xff1a;不详细写&#xff0c;主要写关键思路 package.json文件 主要看scripts的执行命令&#xff0c;其他依赖就是用vue-cli方式创建uni-app项目生成的 {"name": "un…

2024年华东杯数学建模思路+论文+代码

A 题 比赛出场顺序问题 “ 五羽轮比 ” 是一种独特的羽毛球双打比赛形式&#xff0c;采用田忌赛马的方式进行&#xff0c;该赛事形 式首先由李宁公司倡导&#xff0c;在业余比赛中&#xff0c;尤其是公司或高校中广受欢迎。五羽轮比&#xff0c;就是 五名羽毛球队员轮流比…

基于Springboot的教师人事档案管理系统

基于SpringbootVue的教师人事档案管理系统的设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页 培训信息 论坛信息 系统公告 后台登录 教师管理 个人档案管理 奖惩…

蓝桥杯国赛算法复习

复习内容 1.spfa 2.背包问题 3.动态规划其他常考问题 4.dfs 5.bfs 6.并查集 一、基础题回顾 1.spfa 问题描述 蒜头君准备去参加骑车比赛&#xff0c;比赛在 n 个城市间进行&#xff0c;编号从 1 到 n。选手们都从城市 1 出发&#xff0c;终点在城市 n。 已知城市间有 m 条道…

json库源码阅读

JSON.h** #ifndef cJSON__h #define cJSON__h#ifdef __cplusplus //extern "C"的主要作用就是为了能够正确实现C代码调用其他C语言代码。加上extern "C"后&#xff0c;会指示编译器这部分代码按C语言的进行编译&#xff0c;而不是C的。这样的话cjson库在c…

Flutter笔记:Widgets Easier组件库(1)使用各式边框

Flutter笔记 Widgets Easier组件库&#xff08;1&#xff09;&#xff1a;使用边框 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress o…

Ant-design中表单多级对象做嵌套表单校验

在 useForm中拿到一个validateInfos&#xff0c;并传入formData和rules&#xff0c;在FormItem中绑定name名 完整代码如下 <template><Form class"enter-x pt-4" :model"formData" ref"formRef"><Row><Col class"mb…

图神经网络 | 混合神经网络模型GCTN地铁客流预测

随着城市人口的不断增加,城市交通也在迅速扩张,这对城市的可持续发展提出了新的挑战。与私家车相比,城市轨道交通可以减少与交通相关的能源消耗、出行成本、交通拥堵和环境污染。同时,研究表明,在城市轨道交通强度较高的城市,汽车保有量的增长相对较慢。因此,地铁、公交…

RK3568 学习笔记 : 单独编译 Linux version 4.19 内核

前言 开发板型号&#xff1a; 【正点原子】 的 RK3568 开发板 AtomPi-CA1 使用 VMware 虚拟机 ubuntu 20.04 编译 rockchip RK3568 Linux 内核 【方法】不使用 庞大的 Rockchip Linux SDK&#xff0c;下载 rockchip Linux kernel 并单独编译 下载 rockchip Linux kernel 这…

javaweb学习(day13-数据交换和异步请求)

一、JSON 1.介绍 1.1 在线文档 JSon 在线文档&#xff1a;https://www.w3school.com.cn/js/js_json_intro.aspAjax 在线文档&#xff1a;https://www.w3school.com.cn/js/js_ajax_intro.asp 1.2 JSON 介绍 JSON 指的是 JavaScript 对象表示法&#xff08;JavaScript Objec…

嵌入式学习——C语言基础——day13

1. 结构体类型的定义 struct 类型名 { 数据类型1 成员变量1; 数据类型2 成员变量2; 数据类型3 成员变量3; ... }; 定义结构体中可以使用的数据类型有 1.基本数据类型&#xff1a;int long short char doub…

网络之路29:三层链路聚合

正文共&#xff1a;1666 字 17 图&#xff0c;预估阅读时间&#xff1a;3 分钟 目录 网络之路第一章&#xff1a;Windows系统中的网络 0、序言 1、Windows系统中的网络1.1、桌面中的网卡1.2、命令行中的网卡1.3、路由表1.4、家用路由器 网络之路第二章&#xff1a;认识企业设备…

【论文阅读】Image Super-Resolution with Non-Local Sparse Attention

Image Super-Resolution with Non-Local Sparse Attention 论文地址摘要1. 简介2. 相关工作2.1.稀疏表示形式。2.2 Non-Local Attention (NLA) for image SR. 3. 非局部稀疏注意力&#xff08;NLSA&#xff09;3.1 稀疏注意力的一般形式3.2. Attention Bucket from Locality Se…

微信公众平台接口调试工具body怎么填写?微信公众号服务器配置获取令牌Token步骤

微信公众平台接口调试工具body怎么填写&#xff1f;微信公众号服务器配置获取令牌Token步骤分享 1、登录微信公众平台&#xff0c;在左侧功能栏依次点击设置与开发——开发者工具——在线接口调试工具 2、接口类型选择基础支持&#xff0c;接口列表选择获取access_token接口/…