本文来谈谈我对若干分布式事务框架的看法,只谈设计时导致无法轻易改变的硬伤(或者说我的偏见),其优点应该已表现在其文档中,不再赘述。至于我的偏见能不能成为你的偏见,请自行思考核实,仅供大家选型时开拓思路使用。
靶子
以下我略有了解的框架将成为靶子:
- TransactionsEssentials(atomikos免费版)
- tcc-transaction
- ByteTCC
- hmily
- tx-lcn
- GTS
- EasyTransaction
TransactionsEssentials
其是atomikos公司的两阶段提交事务的免费版本,说到两阶段提交大家第一印象应该都是慢,第二印象应该就是很方便,编码少。
但实际上有多慢呢?可能大家都不太确定,我这里有一组数字供大家参考,相同业务场景,两个服务有数据协同需求:
- 若两个服务在同一库中,单库事务可达600TPS+
- 但Atomikos速度为40-70TPS
当然,这里没有给出具体的场景与配置,确实差值也有点大,我当时也是不太相信的。但我从多个不同测试数据源获得的数据都较为类似,大家有空可以协助验证下,然后评论给下数据。
所以对于TransactionsEssentials这个框架,我的偏见就是,太慢。
tcc-transaction
这是一个在GITHUB上开源了很久的框架了,其STAR数量接近3K,其代码简洁易懂。但其有三个缺点:
- 慢
- 应用Crash时有几率导致持久化的事务状态不正确
- 不支持框架幂
慢的原因
- 事务日志使用一个数据库的变长字段存储
- 会多次更新该字段
- 该字段存储的内容不断变长,使其不能在磁盘中原地更新,导致页的分裂,或者导致该字段从页中移除,涉及大量IO
CRASH不一致的原因
- 其CRASH后纯粹依赖事务日志判断全局事务状态(Trying,Confirming,Canceling)
- 然而该事务日志记录的事务状态是无法与业务数据库的事务状态保持强一致的(若能,则需要引入2PC等手段,是不是很矛盾)
- 因此在Crash时导致事务日志状态不正确时,按照目前的设计是需要人工介入排查问题的
不支持框架幂等
这会导致业务开发工作量大大增加。
ByteTCC
ByteTCC是一个兼容JTA规范的基于TCC机制的分布式事务管理器。
其实个人觉得基JTA规范扩展的TCC实现并非一个特别好的想法,其
- 强制Spring的PlatformTransactionManager要支持JTA,需要用户修改原有的PlatformTransactionManager
- 使用了ByteTCC自行实现的UserTransaction(JTA相关接口),并在里层整合"TCC"及“真JTA”的控制逻辑,这违反了编程里的开闭原则
- 用自定义实现类替换掉了客户原有实现中可能更为可靠的JTA/JDBC事务,即单机事务的代码逻辑也被改了
- 在JTA接口中整合“真JTA”及“TCC”的逻辑交错在整个实现中,没有很好地分离逻辑,不利于阅读,也不利于修改
- 限制了使用其他的JTA实现
不知道大家有没有听懂我上面说了什么,其实就是说,如果让我来设计,我是尽量不会对原有逻辑进行修改,而是对逻辑进行扩展,这样才能最大程度的程序的安全性,也能更好地与原有逻辑整合。
举个例子,EasyTransaction里就是基于扩展实现了各种功能,其能保证原有事务处理逻辑完全不变,仅仅只是外挂了TCC、可靠消息等等的实现,同等情况下,其实现的理论风险会更小,并且EasyTransaction能无缝兼容JTA事务以及EasyTransaction内的各种事务,并协调一起工作,而ByteTCC则由于其实现形式,难以简单做到。
另外一个方面是其代码变得过于复杂,至少对我来说有理解难度,需要一些额外的知识支撑,不知道其他人的看法是怎样的。
同时关于幂等,ByteTCC只支持Confirm及Cancel操作的幂等,不过这比很多框架都要强了。
hmily
这个框架的主要描述是“高性能分布式事务tcc方案开源框架”,个人感觉其之所以这么声称是因为“采用disruptor框架进行事务日志的异步读写,与RPC框架的性能毫无差别”。
这里有两个个人认为的硬伤(也许是偏见吧):
- 异步写入事务日志就等于TCC是不可靠的
- 持久化IO瓶颈才是一个分布式事务框架的主要瓶颈,其并非Disruptor框架主要针对的CPU瓶颈
为什么不可靠
就一个简单的问题吧,事务日志存储连接不上(网络断掉/掉电了),这时异步写入的日志放到内存了,然后远程的访问请求TRY发出去了,这个时候应用CRASH了。这就会导致TCC日志不完整,从而导致事务无法恢复。
有一些观点认为这些情况极其少见,不需处理,那我们并发编程时volatile之类的同步手段还需要用么?
并且异常都是连锁的,它并不是孤立出现的,我们无法预判会出现什么异常情况,也有墨菲定律说,越担心的事情越有可能发生,因此我们对于这类情况必然是虽然我们不能保证实现完美,但是我们的理论至少要使完美的。
异步的Disruptor并不能解决矛盾的主要方面
我们知道,CPU的速度会比持久化IO的速度高很多个数量级,因此,基本上涉及持久化时,IO必然才是主要优化的目标。
因此我们做优化时,仿照KAFKA等,批量汇集数据,批量IO才是正确的解决之道。不做这个而去优化CPU性能这有点本末倒置,同时据我了解的多个测试结果中,hmily的性能都大幅不及EasyTransaction。
也不支持框架幂等
同上Tcc-transaction
tx-lcn
这个框架本质上是一个BestEffors 1PC的框架,是什么意思呢,也就是大多数情况下,只要应用不Crash就不会导致不一致。
这里带来什么矛盾呢?除非你不关心不一致,允许数据出错不修复,但一旦出现数据不一致一定要修复的情况的话,就要走人工补偿处理,或者调用相关的修复程序。
而人工补偿处理,实际上,也就相当于人肉写了一遍修复程序,而且人肉执行还没留下代码,下次出问题还要人工再分析处理一遍。
因此,结论很明显:
- 对于允许数据不一致的数据来说
- 用BestEffors 1PC挺好的,性能高于2PC,代码量与2PC一样。
- 但是对于数据出现不一致时,必须修复的情况
- 我们必须要写对应的修复程序
- 这实际上跟TCC/补偿等工作量一样了
- 为啥不切换到TCC/补偿等性能更加高的形式
- 并且使用BestEffors1PC写的业务代码在出现数据异常时,并不能保证其后续的补偿是可执行的。
- 举个例子,转账,出现了给别人加钱了,但是自己没有扣钱的数据异常,此时两位客户就有可能立马把钱取出来用掉
tx-lcn这个框架是有适用场景的,但是我个人觉得最好把相关的厉害关系放到险要位置,不然有巨多小白无脑就用了,而不知道其中的坑,我觉得不太好。
GTS
这个框架的偏见嘛,主要就是脏读了。贴一段之前写过的文字。
GTS确实很赞,其核心原理是补偿。
但这个补偿做得很屌,补偿操作由框架自动生成,无需业务干预,框架会记录修改前的记录值到上面的txc_undo_log里,若需要回滚,则拿出undo_log的记录覆盖回原有记录
同时这里存在一个事务隔离级别的问题,GTS的做法是默认脏读,那么就可以直接拿数据库记录展示(但个人觉得应该可以不做脏读,直接拿undo_log里的记录做mvcc,只要undo_log记录不大,都可以加载到内存里)。
还有另外一个问题是如何禁止其他事务对进行中的全局事务记录的更新,GTS的做法是需要接管APP中的数据源,这样就可以解析控制业务要执行的SQL,对于update操作(或者select for update),予以禁止或等待。
不过整体的做法相当于魔改数据库,将数据库的部分功能拉到了业务APP里进行,并修改了默认隔离级别(脏读,如果业务有用数据库记录乐观锁来控制并发的话,将会失效),还有就是,不通过GTS的定制数据源访问会访问修改到未提交数据
EasyTransaction
这个嘛,是我自己写的框架,上面出现的偏见,在我这里都不会有,这篇文章本质是个软文,哈哈,所以ET在我这没有偏见,但我汇总下ET的优点把:
- 真正的高性能,对IO做了大量优化,多个独立三方公司横向评测分布式事务框架时,同等场景及同等可靠性保证下性能最佳(可自行验证测试)
- 理论上只要外部组件不丢数据,在ET内部是不会出现事务不完整的情况(相对于上面一些框架,其原理就不可靠,运行出现异常可以说是必然的)
- 支持框架幂等,业务程序程序不再需要接管 幂等、调用时序错乱处理等繁琐重复逻辑(这个是一个繁琐重复的工作,貌似只有ET对本项进行了完整支持)
- 基于扩展的实现,而非基于修改的实现,更易理解,风险更小,功能更强大
- 支持多种事务形态(TCC,补偿,可靠消息,SAGAs等)混合使用,可按照最适合业务的选择最贴切的事务形态(这时ET创建之初的理念及特点,也是其他框架所不具有的特性)
总结
以上的偏见是不成熟的小想法,若有不正确,各位大佬尽管在评论区拍砖。同时本文仅供各位大佬选型时开拓思路,我认为的偏见不一定就是你的偏见,可能仅仅只是考虑角度、设计理念不一样而已。
个人认为EasyTransaction的理念、设计、可靠性、性能等都不会比上面的框架差,但ET的STAR数量却不及上面的各个开源框架,我觉得一定是ET有什么我自己没有察觉到的缺陷,请大家拍砖以促进本框架进步,可以直接评论,或者到GITHUB上提ISSUE,感谢你的改进建议!
https://github.com/QNJR-GROUP/EasyTransaction