微服务兴起的这几年涌现出不少分布式事务框架,比如ByteTCC、TCC-transaction、EasyTransaction以及最近很火爆的Seata。最近刚看了Seata的源码(v0.5.2),借机记录一下自己对分布式事务的一些理解。(3年前这类框架还没成熟,因项目需要自己也写过一个柔性事务框架)。
限于篇幅一些网上可搜索的细节本文不展开阐述(例如XA、Saga、TCC、Seata等原理的的详细介绍)。
一、分布式事务的泛化
wiki对分布式事务的定义:A distributed transaction is a database transaction in which two or more network hosts are involved.
不过事务一词含义随着SOA架构逐渐扩大,根据上下文不同,可分为两类:
-
System transaction; -
Business transaction。
与此同时,分布式事务的含义也在泛化,尤其SOA、微服务概念流行起来后,多指的是一个业务场景,需要编排很多独立部署的服务时,如何保证交易整体的原子性与一致性问题。这类分布式事务也称作长事务(long-lived transaction),例如一个定行程的交易,它由购买航班、租车以及预订酒店构成,而航班预订可能需要一两天才能确认。为了统一对概念的理解,本文默认指的都是这类长事务。
二、为什么XA大家都不用?
-
性能(阻塞性协议,增加响应时间、锁时间、死锁); -
数据库支持完善度(MySQL 5.7之前都有缺陷); -
协调者依赖独立的J2EE中间件(早期重量级Weblogic、Jboss、后期轻量级Atomikos、Narayana和Bitronix); -
运维复杂,DBA缺少这方面经验; -
并不是所有资源都支持XA协议; -
大厂懂所以不使用,小公司不懂所以不敢用。
准确讲XA是一个规范、协议,它只是定义了一系列的接口,只是目前大多数实现XA的都是数据库或者MQ,所以提起XA往往多指基于资源层的底层分布式事务解决方案。其实现在也有些数据分片框架或者中间件也支持XA协议,毕竟它的兼容性、普遍性更好。
三、两阶段提交的“提升”
-
基于应用共享事务记录执行轨迹; -
然后通过异步重试确保交易最终一致(这也使得这种方式不适用那些业务上允许补偿回滚的场景)。
这类分布式事务场景并不是微服务才出现的,在SOA时代其实就有了,常见的Saga、TCC、可靠消息最终一致等模型也都是很多年前就有了,只是最近几年随着微服务兴起,这些方案又重新被人关注了起来。
「Saga」参考链接:https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf
-
Saga的核心就是补偿,一阶段就是服务的正常顺序调用(数据库事务正常提交),如果都执行成功,则第二阶段则什么都不做;但如果其中有执行发生异常,则依次调用其补偿服务(一般多逆序调用未已执行服务的反交易)来保证整个交易的一致性。应用实施成本一般。 -
TCC的特点在于业务资源检查与加锁,一阶段进行校验,资源锁定,如果第一阶段都成功,二阶段对锁定资源进行交易逻辑,否则,对锁定资源进行释放。应用实施成本较高。 -
基于可靠消息最终一致,一阶段服务正常调用,同时同事务记录消息表,二阶段则进行消息的投递,消费。应用实施成本较低。
DTP(Distributed Transaction Processing)参考链接:http://pubs.opengroup.org/onlinepubs/009680699/toc.pdf
▲ DTP模型
-
RM负责本地事务的提交,同时完成分支事务的注册、锁的判定,扮演事务参与者角色。 -
TM负责整体事务的提交与回滚的指令的触发,扮演事务的总体协调者角色。
四、Seata架构得与失
Seata的使用方式以及原理在其github wiki上已经阐述的很清晰,网上也已有很多源代码剖析的文章。接下来我们通过分析Seata AT模式原理,来看看它的亮点与问题。
「Seata的使用方式以及原理」参考链接:https://github.com/seata/seata/wiki
Seata对MT以及TCC的支持亮点有限,这两种模式更多是为了兼容已有应用生态。
Seata团队画了一个的详细调用流程图,对照此图阅读其源码会轻松很多。

▲ Seata执行流程图
-
应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性; -
将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚; -
通过全局锁实现了写隔离与读隔离。
这些特性的具体实现机制其官网以及github上都有详细介绍,这里不展开介绍。
另外undo log写入时blob字段的插入性能也是不高的。每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间(二阶段虽然是异步的,但其实也会占用系统资源,网络、线程、数据库)。
前后镜像如何生成?
通过druid解析SQL,然后复用业务SQL中的where条件,然后生成Select SQL执行。
业界还有种思路,通过数据库binlog恢复SQL执行前后镜像,这样省去了同步undo log生成记录,减少了性能损耗,同时对业务零侵入,个人感觉是一种更好的方式。
「Seata的引入全局锁会额外增加死锁的风险」参考链接:https://github.com/seata/awesome-seata/blob/master/wiki/en-us/Fescar-AT.md
Seata提供了一个@GlobalLock的注解,可以提供轻量级全局锁判定的功能(不生成undo log),但还是需要集成使用Seata。
五、分布式事务的取舍
基于XA的分布式事务如果要严格保证ACID,实际需要事务隔离级别为SERLALIZABLE。
-
业务改造成本低; -
性能损耗低; -
隔离性保证完整。
-
业务侵入性(基于注解、XML,补偿逻辑); -
隔离性(写隔离/读隔离/读未提交,业务隔离/技术隔离); -
TM/TC部署形态(单独部署、与应用部署一起); -
错误恢复(自动恢复、手动恢复); -
性能(回滚的概率、付出的代价,响应时间、吞吐); -
高可用(注册中心、数据库); -
持久化(数据库、文件、多副本一致算法); -
同步/异步(2PC执行方式); -
日志清理(自动、手动); -
……
六、结语
其实由于网络的不确定性,分布式下很多问题都是难题,最好的方案是避免分布式事务:)
未经允许不得转载:大自然的搬运工 » 如何做分布式事务选型的取舍?