分布式事务的实现方式中,TCC是比较知名的模式。但是我一直不喜欢这种模式,原因是这种模式有很多问题要考虑。

之前写过一篇文章说了TCC的很多缺点,后来我把文章删了,原因是一位阿里大佬加我好友并指正了我的观点。

太感谢了!

1 TCC概要

简单来讲,TCC模式就是将整个事务分成两个阶段来提交,try阶段进行预留资源,如果所有分支都预留成功,则进入commit阶段提交所有分支事务,否则执行cancel取消所有分支事务。

以电商系统为例,假如有订单、库存和账户3个服务,客户购买一件商品,订单服务增加订单,库存服务扣减库存,账户服务扣减金额,这三个操作必须是原子性的,要么全部成功,要么全部失败。

try阶段

如下图:

订单服务增加一个订单,库存服务冻结订单上的库存,账户服务冻结订单上的金额。

订单、库存和账户这三个服务作为整个分布式事务的分支事务,在try阶段都是要提交本地事务的。上面库存和账户说的冻结,就是说这个订单对应的库存和金额已经不能再被其他事务使用了,所以必须提交本地事务。

但这个提交并不是真正的提交全局事务,而是把资源转到中间态,这个中间态需要在try方法的业务代码中实现,比如账户扣除的金额可以先存放到一个中间账户。

如果try阶段不提交本地事务会有什么问题呢?有可能其他事务在try阶段发现用户账户里面的金额还够,但是commit的时候发现金额不够了,commit阶段扣款只能失败,这时其他两个分支事务提交成功而账户服务的分支事务提交失败,最终数据就不一致了。

commit阶段

如下图:

commit阶段,数据从中间态转入终态,比如订单金额从中间账户转到最终账户。

cancel阶段跟commit阶段类似,比如订单金额从中间账户退回到客户账户。

2 问题代码

下面这段代码也可以理解为TCC,是在try阶段hold住了connection,不提交分支事务,到commit阶段再提交分支事务。代码如下:我们以扣减账户为例,首先定义2个变量来hold住connection:

private Map<String, Statement> statementMap = new ConcurrentHashMap<>(100);
private Map<String, Connection> connectionMap = new ConcurrentHashMap<>(100);

try方法代码如下:

public boolean try(String xid, Long userId, BigDecimal payAmount) {LOGGER.info("decrease, xid:{}", xid);LOGGER.info("------->尝试扣减账户开始account");try {//尝试扣减账户金额,事务不提交Connection connection = hikariDataSource.getConnection();connection.setAutoCommit(false);String sql = "UPDATE account SET balance = balance - ?,used = used + ? where user_id = ?";PreparedStatement stmt = connection.prepareStatement(sql);stmt.setBigDecimal(1, payAmount);stmt.setBigDecimal(2, payAmount);stmt.setLong(3, userId);stmt.executeUpdate();statementMap.put(xid, stmt);connectionMap.put(xid, connection);} catch (Exception e) {LOGGER.error("decrease parepare failure:", e);return false;}LOGGER.info("------->尝试扣减账户结束account");return true;
}

commit方法代码如下:

public boolean commit(BusinessActionContext actionContext){String xid = actionContext.getXid();PreparedStatement statement = (PreparedStatement) statementMap.get(xid);Connection connection = connectionMap.get(xid);try {if (null != connection){connection.commit();}} catch (SQLException e) {LOGGER.error("扣减账户失败:", e);return false;}finally {try {statementMap.remove(xid);connectionMap.remove(xid);if (null != statement){statement.close();}if (null != connection){connection.close();}} catch (SQLException e) {LOGGER.error("扣减账户提交事务后关闭连接池失败:", e);}}return true;
}

cancel方法代码如下:

public boolean rollback(BusinessActionContext actionContext){String xid = actionContext.getXid();PreparedStatement statement = (PreparedStatement) statementMap.get(xid);Connection connection = connectionMap.get(xid);try {connection.rollback();} catch (SQLException e) {return false;}finally {try {if (null != statement){statement.close();}if (null != connection){connection.close();}statementMap.remove(xid);connectionMap.remove(xid);} catch (SQLException e) {LOGGER.error("扣减账户回滚事务后关闭连接池失败:", e);}}return true;
}

这段代码是问题代码,不能用,不能用,不能用

这个代码存在两个问题:

2.1 阻塞等待

如果当前事务不提交,比如账户服务,那就相当于是锁定了资源,后面的事务只能等待资源释放。

2.2 服务集群

以订单服务为例,假如订单服务是一个3个机器的集群,如下图:

协调节点使用注册中心客户端来调用订单服务,如果try请求发送到了订单服务1,而commit请求发送到了订单服务2,那订单服务2上的connectionMap里不会有xid=123这个connection,只能提交失败。

3 TCC存在的问题

上面的问题代码就是给大家一个思路,如果真要hold住connection,也算是实现了TCC的思想,但是在系统中,我们是不可能这样做的,所以把它叫做问题代码。

3.1 空回滚

如下图,订单服务1节点故障,如果不考虑重试,try方法失败:

try虽然失败了,但是全局事务已经开启,框架必须要把这个全局事务推向结束状态,这就不得不调用订单服务cancel方法进行回滚,结果订单服务空跑了一次cancel方法。

解决这个问题,可以记录一张事务控制表,保存全局事务xid和分支事务branchId,try阶段会插入一条记录,表示try阶段执行了。cancel方法读取该记录,如果记录存在,正常回滚;如果该记录不存在,那就是空回滚。

3.2 幂等

幂等是指在commit/cancel阶段,因为TC没有收到分支事务的响应,需要进行重试,这就要分支事务支持幂等。以订单服务为例。如下图:

要支持幂等,可以记录一张事务控制表,保存全局事务xid和分支事务branchId,以及分支事务状态,在第二阶段commit/cancel之前先检查分支事务状态是否已经是终态,如果不是,再执行第二阶段的逻辑。

3.3 悬挂

悬挂是指事务的cancel方法比try方法先执行。上面讲了seata的使用过程中会发生空回滚,如果发生了空回滚,执行了cancel方法后全局事务结束了,但是因为网络问题,订单服务又收到了try请求,执行try方法后预留资源成功,这些资源最终不能释放了。

解决这个问题的方法就是在cancel方法中记录xid对应的分支事务回滚记录,try阶段执行的时候先判断分支事务是否已经回滚,如果存在回滚记录,则直接退出。

3.4 业务代码侵入

TCC的try/commit/cancel,对业务代码都有侵入,而且每个方法都是一个本地事务。再加上需要考虑幂等、空回滚、悬挂等,代码侵入会更高。

4.TCC优势

这里以seata实现的四种模式来比较,包括XA、SAGA、TCC、AT。

效率

使用TCC模式时,在try阶段就提交了本地事务,并不会锁定资源,所以没有其他额外的性能开销。相比之下,来看其他几种模式:

  • AT模式,需要记录undolog,性能损耗很大。

  • XA模式,执行xa start | sql | xa end之后,执行commit/rollback之前,会锁定资源,后面的事务需要等待。

saga模式

更适合长流程的业务场景。

5.性能优化

参考[1]

5.1 异步提交

优化思路是try阶段成功后,不立即执行confirm/cancel阶段,而是等系统空闲的时候异步执行。如下图:

这样在try阶段结束后,就认为全局事务结束了,可以定时(比如10分钟)来异步执行第二阶段,性能大幅提升。

当然,带来的一点问题就是如果全局事务回滚,会有短暂的数据不一致。比如扣款的场景,定时10分钟执行一次异步任务,如果第二阶段是cancel,那客户会在这10分钟内不能使用这笔金额。

这个异步执行的时间也可以根据业务来决定,比如不需要及时从中间账户转移到最终账户的场景可以设置更长。

5.2 同库模式

首先回顾一下TCC中各个角色:

  • TM管理全局事务,包括开启全局事务,提交/回滚全局事务

  • RM管理分支事务

  • TC管理全局事务和分支事务的状态

先看一下优化之前的通信模型,如下图:

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

优化之后的模型如下图:

TM开启全局事务时,不再需要向TC注册分支事务,而是把分支事务状态保存在了本地。TM向TC发送提交或回滚消息时,TC保存全局事务的状态。而RM则启动异步线程检测本地记录的未提交分支事务,向TC发送RPC消息获取整体事务状态,以决定是提交还是回滚本地事务。可见,优化后的模型,RPC次数减少了50%,性能大幅提升。

6.总结

TCC的问题确实不少,但是除了侵入业务代码这一个问题,其他问题都有对应的解决方案。

阿里针对TCC做了一些优化,包括第二阶段异步提交和同库模式,性能提升很明显。

参考资料

[1]

参考: https://tech.antfin.com/community/live/462/data/736

分布式事务,阿里为什么钟爱TCC相关推荐

  1. 《深入理解分布式事务》第八章 TCC 分布式事务原理

    <深入理解分布式事务>第八章 TCC 分布式事务原理 文章目录 <深入理解分布式事务>第八章 TCC 分布式事务原理 一.TCC 核心思想 二.TCC 实现原理 1.TCC 核 ...

  2. 关于分布式事务: 阿里开源的分布式事务框架 Seata 和 LCN的分析

    之前使用过LCN分布式事务, 最近看到面试者简历中另一种方案 Seata, 通过它来在实战中解决分布式事务的问题.故 去简单了解了一下Seata是什么, 和LCN的区别在哪里, 如果是你 你怎么选择解 ...

  3. 分布式事务:XA,2PC,3PC,TCC,SEATA(AT)

    一.分布式事务产生原因 1.原本的数据是单库单表存储,随着业务的不断扩大数据量不断增多,单库性能支撑不了数据的更新与访问.为了解决数据库上的瓶颈,将数据库进行水平拆分,原来一个库里的事务操作,现在变成 ...

  4. tcc分布式事务_什么是 TCC分布式事务?

    近两年微服务变得越来越火热,各种框架与组件的出现,更是为微服务的开发提供了便利. 我们都知道,每个微服务都是一个对应的小服务,多个服务之间可以方便的进行功能的组合,来形成功能更强大的服务.服务间数据与 ...

  5. 【分布式系统】分布式事务(2PC 3PC TCC 最终一致性)

    目录 问题场景 2PC:两阶段提交 3PC:三阶段提交 TCC(Try-Confirm-Cancel) 基于MQ的最终一致性

  6. seata 1.3.0 四种模式解决分布式事务(AT、TCC、SAGA、XA)

    前言 1.seata版本 1.3.0 2.基础项目结构,大家只需要关注 设备模块 device和工单模块 order即可. - 项目 说明 api-gateway 网关模块 common 基础模块 d ...

  7. 阿里P8架构师谈:分布式事务的解决方案,以及原理、总结

    分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在这几年越来越火的微服务架构中,几乎可以说是无法避免,本文就围绕分布式事务各方面与大家进行介绍. 事务 1. ...

  8. Day124.分布式事务:Seata、2PC两段式、代码补偿TCC、本地消息表、MQ事物消息

    目录 一.相关概念回顾 二.分布式事务 三.分布式事务解决方案 1.基于XA协议的两段式提交(2PC)  - 强一致性 2.代码补偿事务(TCC) - 最终一致性 3.本地消息表(异步确保)- 最终一 ...

  9. 分布式事务XA、TCC、AT总结

    TCC和AT在第一阶段都会直接将事务提交(commit),如果需要回滚,TCC则需要在Cancel阶段自己实现一段业务逻辑来完成数据的回滚.注意,此时是写补偿sql来完成回滚保证数据的一致性. 而AT ...

最新文章

  1. 四月青少年编程组队学习(图形化四级)Task01
  2. 如何优化计算机网络课程,计算机论文:探究如何优化计算机网络课程教学方法.docx...
  3. Android启动界面优化技巧-Splash Screens的正确方式
  4. 进程间通信 - 动态链接库实现
  5. str.length() 与 str.getBytes().length
  6. 利用dos管道命令获取屏幕内容_汇编语言--常用DOS功能
  7. linux挂载磁盘分区,Linux 新磁盘分区与挂载
  8. [BZOJ1500][NOI2005]维修数列(splay)
  9. 某电子工厂老板感叹创业开厂人生
  10. 绵阳创客开发长语音识别平台 1小时语音10分钟转化为文字
  11. 关于初级java程序员面试题总结(每月更新中)
  12. 采用计算机发布调度命令时 必须严格遵守,调度命令规范格式(公文命令).doc...
  13. dplayer安装php_Dplayer播放器集成p2p加速源码分享
  14. 安装eclipse c++版本neno
  15. Shell脚本——免交互
  16. AtCoder Beginner Contest 285 青大蒟蒻训练日常(A-F) 上分场(可惜unr)
  17. 万维网互联网计算机网络的区别,互联网、局域网、万维网三者区别
  18. sqlserver实现抽奖Demo
  19. 2021最新 腾讯云从零搭建PHP运行环境
  20. 手机网站常用的推广方式有哪些

热门文章

  1. @scheduled cron动态修改_spring boot实现动态增删启停定时任务
  2. Linux 环境下如何安装部署 RocketMQ 教程
  3. Centos8中恢复根目录为默认权限
  4. 怎么使一个浮点数删除小数部分C语言,如何得出一个浮点数的小数部分,要把各个位保存到一个数组里边。...
  5. 思维dp ---- Codeforces Round #722 (Div. 1) B. Kavi on Pairing Duty [思维dp + 数学]
  6. UVA10296 Jogging Trails(中国邮递员问题)(欧拉回路、一般图最大权匹配 / 状压DP)
  7. 模板 - 计算几何相关公式大全
  8. 解题报告:NOIP2013 车站分级(拓扑序递推求解差分约束、建图优化O(n+m)) 超详细讲解
  9. linux mint root激活,Linux mint root登录无声音的问题解决方法
  10. python 条件选择语句_Python趣味入门4:选择往往是最重要的-条件语句