首页 > 编程知识 正文

mysql事务提交两阶段,两阶段提交和三阶段提交的区别

时间:2023-05-04 05:19:36 阅读:242079 作者:4893

缘起

现在用的比较多的分布式事务方案主要有TCC和可靠消息。TCC需要对业务进行改造,分别实现try/confirm/cancel方法,侵入性太强,工作量大。而可靠消息主要适合可以异步调用的业务,对于需要跨服务同步调用的业务,实现困难。以订机票为例,从深圳->新疆乌鲁木齐,假设没有直达,第一步先要确定中转地,选择一家航空公司订票,假设这步的订票结果是成功调用南航的服务订到了深圳->西安,那第二步调用就要根据第一步的结果订第二张票,比如优先选择同一家航空公司,间隔时间要大于两小时,出发地要限制在西安。如果第二步订票失败,那么第一步订的票也需要回滚。像这种存在上下文逻辑关系的跨服务调用,用消息同步方案很难处理。

所以需要一个好的支持跨服务调用的分布式方案。

理想实现

1) 简洁明了,最好和普通的无事务或一阶段事务的远程调用相接近

2) 侵入性小,尽量不要因为引进分布式事务引起业务代码结构的明显变化

3) 危险期短

4) 性能损失少

现实情况

TCC方案满足条件3,4,条件1勉强,不满足条件2。

Atomikos方案貌似也比较流行,但有很多限制。比较奇怪的是网上的例子基本上是用Atomikos实现基于XA的单例多数据源的事务控制,看不到跨服务调用的例子。官方倒是提供了各种场景的使用例子,看了后,我猜到了原因:官方的例子很不清爽,而且高性能方案要收费。我在测试的时候发现免费版限制了50个事务/每秒,就直接放弃了对它的进一步研究。

我最满意的方案是阿里的GTS,它是基于XA的两阶段提交,使用时只要一个注解和极少量的代码侵入,不破坏原有代码结构,而且性能损失只有10%左右。看它的文档,我估计它是直接本地一阶段提交,同时记录操作前像,后像undo日志,当需要回滚时,使用undo日志定位记录,对比当前数据和后像,如果一致,直接使用前像覆盖回滚,如果不一致,则报警让人工介入。这个方案很对我胃口,然而不是免费开源,且要在云端使用。

自己造轮子

TPCTransaction: https://github.com/johnhuang-cn/TPCTransaction

TPC == Two Phase Commit

先看成品效果(基于Spring Cloud)

以经典的银行转帐为例,从alpha银行帐户转出到bravo银行同名帐户。

调用方代码

服务端Service实现

使用Mybatis mapper

调用方是不是和Spring的手工事务控制代码差不多?服务端只是将Spring的@Transactional换成了@TPCTransactional注解。

再看看性能

为了对比,同时做了一个一阶段提交的实现,也就是说调用即提交,没有后期同步commit/rollback的功能。如果出现转出成功,转入失败,那就会出现数据不一致。测试进行了1000次的转帐业务,数据库两个银行各有50个帐户,两边各账户初始存款100万,为了测试事务的有效性,引入3%的随机失败。

测试结果(Transaction/Second):

Non transaction serial TPS: 35.57 // 单线程

Non transaction paralle TPS: 337.60 // 并发

TPC transaction serial TPS: 42.34 // 单线程

TPC transaction paralle TPS: 282.16 // 并发

基于TPC的转帐,经过1000次转帐并等待10秒后(需待事务完成或超时回滚),两边帐户数据一致(加起来200万),证明事务控制是有效的。而一阶段直接提交在有随机异常的情况下,毫无疑问肯定是不一致了。

TPC事务在并发情况下,大约损失15%,这个是可以接受的。线程情况下TPC事务还更快一点。这是因为单线程下两个方案的锁的粒度是一样的,而@TPCTransactional目前实现还比较简单,@TPCTransactional的代码比@Transactional少很多很多,所以性能还快一点。但在并发下,锁的粒度成了影响性能的关键,这时两阶段方案就相对慢了。

以上测试,所有的实例和DB都在一台机子上,Mysql运行在虚拟机的Docker里,所以绝对性能不高,主要看相对性能。

实现原理 先上原理图

发起方的实现 事务的开始

TPCTransactionManager.begin(),创建了全局事务UID,并保存ThreadLocal里

远程调用的背后

当通过feign client调用远程服务时,如果当前线程存在未完成的TPC事务,则将事务ID和剩余timeout通过头部传给服务方。通过Header传送,避免了修改调用参数,减少对代码的侵入。

服务端@TPCTransactional的实现

@TPCTransactional实现了around注入,将被注解的对象方法转换成异步执行。主线程启动Executor后,就把自己hold住了。

而executor执行完目标方法后,事务并不提交,而只是将结果返回给主线程,并把主线程唤醒,接着把自己hold住。主线程拿到结果后,直接将结果返回给调用方交差,完成了使命。executor还需要继续等待调用方最终的commit/rollback指令。

发起方的commit/rollback

调用方(也是事务发起方)继续其它业务逻辑和远程调用,各参与的远程服务也一个个将结果返回后将事务异步挂起。最后一切顺利的话,发起方调用TPCTransactionManager.commit()/rollback()。该方法将commit/rollback指令同时发向各参与方。这里使用了并发发送,以减少危险期。

服务方的TrasactionController

TPCTransaction的服务端安排了TransactionController来监听commit/rollback指令,根据全局transaction id检查本地是否存在隶属于该ID的executor,若有将该executor唤醒,executor解除锁定后完成最后的commit/rollback操作。

其它细节

因为发起方在commit/rollback前有可能宕机,那么服务端的executor会一直锁定资源,所以必须设置timeout。超时后,服务端的ExecutorManager自行唤醒executor完成回滚以解除记录锁定。

在Spring Cloud环境下,每个服务端有可能存在多个实例,在Loadbalance作用下,调用和发送commit/rollback可能会被负载到不同的实例上。所以TPCTransaction框架对默认的RibbonRule进行了wrapper,对同一线程的调用始终返回同一个实例。和普通的本地事务一样,TPC发起端的事务也必须在同一个线程完成。

其它细节请移步参观Github上的项目源码。

TODO

不论TCC还是其它两阶段方案,都存在危险期,最后提交的时候,若某一服务端宕机或网络故障,会存在部分提交的问题,这种情况需要记录日志,并报警待人工介入。TPCTransaction框架还未实现事务日志,有待继续完善。

Java设计模式之代理模式怎么实现Python Numpy常用函数总结

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。