实战!阿里神器 Seata 实现 TCC 模式解决分布式事务

今天这篇文章介绍一下Seata如何实现TCC事务模式,文章目录如下:

d7fbbb6f51ea6c25f3d387c9b90972a4.png
目录

什么是TCC模式?

TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。

TCC分为两个阶段,分别如下:

  • 第一阶段:Try(尝试),主要是对业务系统做检测及资源预留 (加锁,锁住资源)

  • 第二阶段:本阶段根据第一阶段的结果,决定是执行confirm还是cancel

  1. Confirm(确认):执行真正的业务(执行业务,释放锁)

  2. Cancle(取消):是预留资源的取消(出问题,释放锁)

e547503b98a722e1af9cdeb771e4c105.png
TCC

为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。

假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。

①Try 阶段

TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:

  • 完成所有业务检查( 一致性 ) 。

  • 预留必须业务资源( 准隔离性 ) 。

  • Try 尝试执行业务。

66a51c5cea720d047fe40c205f94b36c.png
Try阶段

②Confirm / Cancel 阶段

根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。

Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。

Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作,业务如下图:

dc3f2806d9e7a57841719155dd9041f9.png
Try->Confirm

这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。

Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。

Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段,业务如下图:

2b9b84846163b3252c807f1096438546.png
Try-Cancel

Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。

TCC模式的三种类型?

业内实际生产中对TCC模式进行了扩展,总结出了如下三种类型,其实从官方的定义中无此说法,不过是企业生产中根据实际的需求衍生出来的三种方案。

1、通用型 TCC 解决方案

通用型TCC解决方案是最经典的TCC事务模型的实现,正如第一节介绍的模型,所有的从业务都参与到主业务的决策中。

5d0635094f02fa3312d4b34b669d5004.png
通用型TCC

适用场景:

由于从业务服务是同步调用,其结果会影响到主业务服务的决策,因此通用型 TCC 分布式事务解决方案适用于执行时间确定且较短的业务,比如电商系统的三个核心服务:订单服务、账户服务、库存服务。

这个三个服务要么同时成功,要么同时失败。

39ff95bb9d84e2cdf33503d91718981f.png

当库存服务、账户服务的第二阶段调用完成后,整个分布式事务完成。

2、异步确保型 TCC 解决方案

异步确保型 TCC 解决方案的直接从业务服务是可靠消息服务,而真正的从业务服务则通过消息服务解耦,作为消息服务的消费端,异步地执行。

3cb641c36cd01ed58ac74d1f7b9d465a.png
异步确保型

可靠消息服务需要提供 Try,Confirm,Cancel 三个接口。Try 接口预发送,只负责持久化存储消息数据;Confirm 接口确认发送,这时才开始真正的投递消息;Cancel 接口取消发送,删除消息数据。

消息服务的消息数据独立存储,独立伸缩,降低从业务服务与消息系统间的耦合,在消息服务可靠的前提下,实现分布式事务的最终一致性。

此解决方案虽然增加了消息服务的维护成本,但由于消息服务代替从业务服务实现了 TCC 接口,从业务服务不需要任何改造,接入成本非常低。

适用场景:

由于从业务服务消费消息是一个异步的过程,执行时间不确定,可能会导致不一致时间窗口增加。因此,异步确保性 TCC 分布式事务解决方案只适用于对最终一致性时间敏感度较低的一些被动型业务(从业务服务的处理结果不影响主业务服务的决策,只被动的接收主业务服务的决策结果)。比如会员注册服务和邮件发送服务:

0d9275f4c8de43faab047c2cdeb41efd.png

3、补偿型 TCC 解决方案

补偿型 TCC 解决方案与通用型 TCC 解决方案的结构相似,其从业务服务也需要参与到主业务服务的活动决策当中。但不一样的是,前者的从业务服务只需要提供 Do 和 Compensate 两个接口,而后者需要提供三个接口。

eb80091f3cca58ef1909fa757e03aef6.png

Do 接口直接执行真正的完整业务逻辑,完成业务处理,业务执行结果外部可见;Compensate 操作用于业务补偿,抵消或部分抵消正向业务操作的业务结果,Compensate操作需满足幂等性。

与通用型解决方案相比,补偿型解决方案的从业务服务不需要改造原有业务逻辑,只需要额外增加一个补偿回滚逻辑即可,业务改造量较小。但要注意的是,业务在一阶段就执行完整个业务逻辑,无法做到有效的事务隔离,当需要回滚时,可能存在补偿失败的情况,还需要额外的异常处理机制,比如人工介入。

适用场景:

由于存在回滚补偿失败的情况,补偿型 TCC 分布式事务解决方案只适用于一些并发冲突较少或者需要与外部交互的业务,这些外部业务不属于被动型业务,其执行结果会影响主业务服务的决策。

以上部分内容参考自:https://seata.io/zh-cn/blog/tcc-mode-applicable-scenario-analysis.html?utm_source=gold_browser_extension

TCC事务模式的落地实现

在前面文章中介绍了Seata的AT模式,当然Seata支持的事务模式不局限于AT模式,还有TCC模式、SAGA模式、XA模式,下面整合一下TCC模式。

1、演示场景

就以电商系统中下订单为例,为了演示,直接去掉账户服务,以订单服务、库存服务为例介绍。

具体的逻辑如下:

  1. 客户端调用下订单接口

  2. 扣库存

  3. 创建订单

  4. 请求完成

根据上面的逻辑可知,订单服务肯定是主业务服务,事务的发起方,库存服务是从业务服务,参与事务的决策。

Seata的AT模式解决方案伪代码如下:

@GlobalTransactional
public Result<Void> createOrder(Long productId,Long num,.....){//1、扣库存reduceStorage();//2、创建订单saveOrder();
}

@GlobalTransactional这个注解用于发起一个全局事务。

但是AT模式有局限性,如下:

  • 性能低,锁定资源时间太长

  • 无法解决跨应用的事务

因此对于要求性能的下单接口,可以考虑使用TCC模式进行拆分成两阶段执行,这样整个流程锁定资源的时间将会变短,性能也能提高。

此时的TCC模式的拆分如下:

1、一阶段的Try操作

TCC模式中的Try阶段其实就是预留资源,在这个过程中可以将需要的商品数量的库存冻结,这样就要在库存表中维护一个冻结的库存这个字段。

伪代码如下:

@Transactional
public boolean try(){//冻结库存frozenStorage();//生成订单,状态为待确认saveOrder();
}

注意:@Transactional开启了本地事务,只要出现了异常,本地事务将会回滚,同时执行第二阶段的cancel操作。

2、二阶段的confirm操作

confirm操作在一阶段try操作成功之后提交事务,涉及到的操作如下:

  1. 释放try操作冻结的库存(冻结库存-购买数量)

  2. 生成订单

伪代码如下:

@Transactional
public boolean confirm(){//释放掉try操作预留的库存cleanFrozen();//修改订单,状态为已完成updateOrder();return true;
}

注意:这里如果返回false,遵循TCC规范,应该要不断重试,直到confirm完成。

3、二阶段的cancel操作

cancel操作在一阶段try操作出现异常之后执行,用于回滚资源,涉及到的操作如下:

  1. 恢复冻结的库存(冻结库存-购买数量、库存+购买数量)

  2. 删除订单

伪代码如下:

@Transactional
public boolean cancel(){//释放掉try操作预留的库存rollbackFrozen();//修改订单,状态为已完成delOrder();return true;
}

注意:这里如果返回false,遵循TCC规范,应该要不断重试,直到cancel完成。

2、TCC事务模型的三个异常

实现TCC事务模型涉及到的三个异常是不可避免的,实际生产中必须要规避这三大异常。

1、空回滚

定义:在未调用try方法或try方法未执行成功的情况下,就执行了cancel方法进行了回滚。

怎么理解呢?未调用try方法就执行了cancel方法,这个很容易理解,既然没有预留资源,那么肯定是不能回滚。

try方法未执行成功是什么意思?

可以看上节中的第一阶段try方法的伪代码,由于try方法开启了本地事务,一旦try方法执行过程中出现了异常,将会导致try方法的本地事务回滚(注意这里不是cancel方法回滚,而是try方法的本地事务回滚),这样其实try方法中的所有操作都将会回滚,也就没有必要调用cancel方法。

但是实际上一旦try方法抛出了异常,那么必定是要调用cancel方法进行回滚,这样就导致了空回滚。

解决方案:

解决逻辑很简单:在cancel方法执行操作之前,必须要知道try方法是否执行成功。

2、幂等性

TCC模式定义中提到:如果confirm或者cancel方法执行失败,要一直重试直到成功。

这里就涉及了幂等性,confirm和cancel方法必须保证同一个全局事务中的幂等性。

解决方案:

解决逻辑很简单:对付幂等,自然是要利用幂等标识进行防重操作。

3、悬挂

事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因网络拥堵而导致的超时,此时事务管理器会触发二阶段回滚,调用 TCC 服务的 Cancel 操作,Cancel 调用未超时;

在此之后,拥堵在网络上的一阶段 Try 数据包被 TCC 服务收到,出现了二阶段 Cancel 请求比一阶段 Try 请求先执行的情况,此 TCC 服务在执行晚到的 Try 之后,将永远不会再收到二阶段的 Confirm 或者 Cancel ,造成 TCC 服务悬挂。

解决方案:

解决逻辑很简单:在执行try方法操作资源之前判断cancel方法是否已经执行;同样的在cancel方法执行后要记录执行的状态。

4、总结

针对以上三个异常,落地的解决方案很多,比如维护一个事务状态表,每个事务的执行阶段全部记录下来。

  • 幂等:在执行confirm或者cancel之前根据事务状态表查询当前全局事务是否已经执行过confirm或者cancel方法

  • 空回滚:在执行cancel之前才能根据事务状态表查询当前全局事务是否已经执行成功try方法

  • 悬挂:在执行try方法之前,根据事务状态表查询当前全局事务是否已经执行过cancel方法

Seata整合TCC实现

关于如何搭建项目、添加依赖这里就不再细说了,本节介绍一下关键代码。

源码目录如下:

a02ca95d05add665510c0d4b9bc1d59d.png
源码目录

项目启动所需要的相关文件如下图:

a03eeb7da2256008fab6a7efa730aec0.png

nacos目录中的SEATA_GROUP是Seata事务服务端和客户端所需要的相关配置,直接导入nacos即可。

seata目录中的conf是1.3.0版本服务端的配置

SQL目录是相关的几个数据库。

1、TCC接口定义

在order-boot模块创建OrderTccService,代码如下:

83bf5c4f8e7eb5d70ced71314ecf9c55.png

代码中注释已经很完整了,下面挑几个重点介绍一下:

  1. @LocalTCC:该注解开启TCC事务

  2. @TwoPhaseBusinessAction:该注解标注在try方法上,其中的三个属性如下:

    1. name:TCC事务的名称,必须是唯一的

    2. commitMethod:confirm方法的名称,默认是commit

    3. rollbackMethod:cancel方法的名称,,默认是rollback

  3. confirm和cancel的返回值尤为重要,返回false则会不断的重试。

2、TCC接口实现

定义有了,总要实现,如下:

1、try方法

c1e893785940e1938df62fafe61b11e1.png
try方法

①处的代码是为了防止悬挂异常,从事务日志表中获取全局事务ID的状态,如果是cancel状态则不执行。

②处的代码冻结库存

③处的代码生成订单,状态为待确认

④处的代码向幂等工具类中添加一个标记,key为当前类和全局事务ID,value为当前时间戳。

注意:必须要开启本地事务,如上代码使用@Transactional开启本地事务

2、confirm方法

b0758708b17afc1fc4888bf6cbc178d0.png
confirm方法

①处的代码从幂等工具类中根据当前类和全局事务ID获取值,由于try阶段执行成功会向其中添加值,confirm方法执行成功会移出这个值,因此在confirm开头判断这个值是否存在就起到了幂等效果,防止重试的效果。

⑥处的代码从幂等工具类中移出try方法中添加的值。

②处的代码是从BusinessActionContext中获取try方法中的入参。

③处的代码是释放掉冻结的库存

④处的代码是修改订单的状态为已完成。

注意:1. 开启本地事务  2. 注意返回值,返回false时将会重试

3、cancel方法

74ffe61bf00c4352ace29a17351a073f.png
cancel方法

①处的代码是向事务日志记录表中插入一条数据,标记当前事务进入cancel方法,用来防止悬挂,这个和try方法中的①处的代码相呼应。

②处的代码是为了防止幂等和空回滚,因为只有当try方法中执行成功幂等工具类中对应的当前类和全局事务ID才会存储该值。这样既防止了幂等,也防止了空回滚。

③处的代码恢复冻结的库存。

④处的代码删除这笔订单

⑤处的代码是移出幂等工具类当前类和全局事务ID对应的值。

3、如何防止TCC模型的三个异常?

实现方法有很多,有些案例是全部使用事务日志表记录当前的状态,这样完美的解决了幂等、空回滚、悬挂的问题。

陈某这里为了方便,使用了两种方案,如下:

1、幂等、空回滚

使用了一个幂等工具类,其中是个Map,key为当前类和全局事务ID,value是时间戳。

代码如下:

562aa39239c807b80f56640117d3c4e7.png

思路如下:

  1. 在try方法最后使用幂等工具类中的add方法添加值

  2. 在confirm、cancel方法中使用幂等工具类中的remove方法移出值

  3. 在confirm、cancel方法中使用幂等工具类中get方法获取值,如果为空,则表示已经执行过了,直接返回true,这样既防止了幂等,也防止了空回滚。

2、悬挂

悬挂的实现依靠的是事务日志表,表结构如下:

CREATE TABLE `transactional_record` (`id` bigint(11) NOT NULL AUTO_INCREMENT,`xid` varchar(100) NOT NULL,`status` int(1) DEFAULT NULL COMMENT '1. try  2 commit 3 cancel ',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

其中的xid是全局事务ID,status是事务的状态。

其他的字段自己可以扩展

解决悬挂问题的逻辑如下:

  1. cancel方法中将当前全局事务ID记录到事务日志表中,状态为cancel

  2. try方法执行资源操作前检查事务日志表中当前全局事务ID是否已经是cancel状态

4、创建订单的业务方法

上面只是完成了TCC的三个方法,主业务事务发起方还未提供,代码如下:

ecd61c6f1a7582b7ae57c5f38d6a04f8.png

@GlobalTransactional这个注解开启了全局事务,是事务的发起方。

内部直接调用的TCC的try方法。

5、其他的配置

以上只是列出了关键的步骤,剩余其他的配置自己根据案例源码完善,如下:

  1. 接口测试

  2. 整合nacos

  3. 整合feign

  4. 整合seata,TCC模式中的配置和AT模式的Seata配置相同

注意:一定要配置Seata的事务组tx-service-group,配置方法见之前的文章。

6、总结

TCC事务模型相对来说比较简单的一种,有兴趣的可以下载源码试试。

fb2c1458c6aacf6e747490e2f308a1af.gif

往期推荐

f228d692e1be5e4d2ebe52183505ac40.png

synchronized和ReentrantLock的5个区别!


e37b6a36181b1266ddd9c3f368b35d73.png

oppo后端16连问


1a8230c700d0cc5b519e7f23d829a32e.png

怎么解决MySQL死锁问题的?


8a5570da7615c8a8989996a6f274b93e.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544195.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java Dictionary get()方法与示例

字典类的get()方法 (Dictionary Class get() method) get() method is available in java.util package. get()方法在java.util包中可用。 get() method is used to get the value on the specified key element (key_ele) in this dictionary. get()方法用于获取此字典中指定键…

[CareerCup] 8.10 Implement a Hash Table 实现一个哈希表

8.10 Design and implement a hash table which uses chaining (linked lists) to handle collisions. 这道题让我们实现一个简单的哈希表&#xff0c;我们采用了最简单的那种取余映射的方式来实现&#xff0c;我们使用Cell来保存一对对的key和value的映射关系&#xff0c;然后…

Spring Boot 中实现跨域的 5 种方式,你一定要知道!

一、为什么会出现跨域问题出于浏览器的同源策略限制。同源策略&#xff08;Sameoriginpolicy&#xff09;是一种约定&#xff0c;它是浏览器最核心也最基本的安全功能&#xff0c;如果缺少了同源策略&#xff0c;则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略…

stl:queue 源码_C ++ STL中的queue :: empty()和queue :: size()

stl:queue 源码In C STL, Queue is a type of container that follows FIFO (First-in-First-out) elements arrangement i.e. the elements which inserts first will be removed first. In queue, elements are inserted at one end known as "back" and are delet…

术中导航_密码术中的计数器(CTR)模式

术中导航The Counter Mode or CTR is a simple counter based block cipher implementation in cryptography. Each or every time a counter initiated value is encrypted and given as input to XOR with plaintext or original text which results in ciphertext block. Th…

Android社交类APP动态详情代码实现通用模板

&#xfeff;&#xfeff;Android社交类APP动态详情代码实现通用模板 Android平台上一些比较流行的社交类APP比如微信、陌陌等&#xff0c;都有动态详情页&#xff0c;在该页面&#xff0c;用户发表的动态详情&#xff0c;好友可以发起评论、点赞等等。这种设计在微信和陌陌上大…

聊聊并发编程的12种业务场景

前言并发编程是一项非常重要的技术&#xff0c;无论在面试&#xff0c;还是工作中出现的频率非常高。并发编程说白了就是多线程编程&#xff0c;但多线程一定比单线程效率更高&#xff1f;答&#xff1a;不一定&#xff0c;要看具体业务场景。毕竟如果使用了多线程&#xff0c;…

软件工程编码阶段_软件工程的编码阶段

软件工程编码阶段The coding phase in the software engineering paradigm is usually defined after the designing phase. In this phase, the developers or the coders have to implement the software design practically using any computer language(s) so that the sof…

梳理50道经典计算机网络面试题

我梳理了50道计算机网络面试题&#xff0c;每一道题目都特别经典&#xff0c;大厂也非常喜欢问。相信大家看完&#xff0c;会有新的收获滴~1. 说说HTTP常用的状态码及其含义&#xff1f;思路: 这道面试题主要考察候选人&#xff0c;是否掌握HTTP状态码这个基础知识点。不管是不…

A successful Git branching model

原文&#xff1a;http://nvie.com/posts/a-successful-git-branching-model/ In this post I present the development model that I’ve introduced for all of my projects (both at work and private) about a year ago, and which has turned out to be very successful. I…

一文详解读写锁

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;读写锁&#xff08;Readers-Writer Lock&#xff09;顾名思义是一把锁分为两部分&#xff1a;读锁和写锁&#xff0c…

ruby array_Ruby中带有示例的Array.keep_if方法

ruby arrayRuby Array.keep_if方法 (Ruby Array.keep_if Method) In the last articles, we have studied the Array methods namely Array.select, Array.reject and Array.drop_while, all these methods are non–destructive methods which means that they do not impose …

[实战]MVC5+EF6+MySql企业网盘实战(2)——用户注册

写在前面 上篇文章简单介绍了项目的结构&#xff0c;这篇文章将实现用户的注册。当然关于漂亮的ui&#xff0c;这在追后再去添加了&#xff0c;先将功能实现。也许代码中有不合适的地方&#xff0c;也只有在之后慢慢去优化了。 系列文章 [EF]vs15ef6mysql code first方式 [实战…

Calico IP_AUTODETECTION_METHOD

在 Calico 中&#xff0c;IP_AUTODETECTION_METHOD 的配置项用于指定 Calico 如何检测容器的 IP 地址。 一、kubernetes-internal-ip模式 其中&#xff0c;kubernetes-internal-ip 是一种特殊的模式&#xff0c;用于在 Kubernetes 环境中检测容器的 IP 地址。具体作用如下&…

下个十年高性能 JSON 库来了:fastjson2!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;fastjson2 是 fastjson 项目的重要升级&#xff0c;目标是为下一个十年提供一个高性能的 JSON 库&#xff0c;同一套 API 支…

ascii非打印控制字符表_C程序打印ASCII表/图表

ascii非打印控制字符表什么是ASCII码&#xff1f; (What are ASCII Codes?) ASCII stands for American Standard Code for Information Interchange; it is a character encoding standards for information interchange in electronics communication. Each alphabets, spec…

THEOS的第一个TWeak的成功创建

THEOS的第一个TWeak的成功创建THEOS的第一个TWeak的成功创建参考资料:成功的创建一个TWeak的弹出步骤1:安装Xcode和Xcode command line步骤2:安装theosa:下载theos前,设置保存的路径:环境变量b:下载theosc:下载头文件d:下载ldid签名工具e:配置MoblieSubstrate环境f:安装dpkg步骤…

查询中,有没有可能多个索引一起用呢?

其实我们之前所讲的回表&#xff0c;就是两个索引树同时使用&#xff0c;先在二级索引树中搜索到对应的主键值&#xff0c;然后在再去主键索引树中查询完整的记录。但是我今天的问题是&#xff0c;两个不同的二级索引树&#xff0c;会同时生效吗&#xff1f;理论上来说&#xf…

ruby array_Ruby中带有示例的Array.sample()方法

ruby arrayArray.sample()方法 (Array.sample() Method) In this article, we will study about Array.sample() method. You all must be thinking the method must be doing something which is quite different from all those methods which we have studied. It is not as…

ThreadLocal夺命11连问

前言前一段时间&#xff0c;有同事使用ThreadLocal踩坑了&#xff0c;正好引起了我的兴趣。所以近期&#xff0c;我抽空把ThreadLocal的源码再研究了一下&#xff0c;越看越有意思&#xff0c;发现里面的东西还真不少。我把精华浓缩了一下&#xff0c;汇集成了下面11个问题&…