分布式事务选型的取舍

标签: | 发表时间:2019-08-06 15:04 | 作者:
出处:https://mp.weixin.qq.com

作者介绍

温卫斌,就职于中国民生银行信息科技部,目前负责分布式技术平台设计与研发,主要关注分布式数据相关领域。


微服务兴起的这几年涌现出不少分布式事务框架,比如ByteTCC、TCC-transaction、EasyTransaction以及最近很火爆的Seata。最近刚看了Seata的源码(v0.5.2),借机记录一下自己对分布式事务的一些理解。(3年前这类框架还没成熟,因项目需要自己也写过一个柔性事务框架)。


本文分五部分,首先明确分布式事务概念的演变,然后简单说下为什么大家不用XA,第三部分阐述两阶段提交的“提升”,第四部分介绍Seata的架构的亮点与问题,第五部分谈下分布式事务的取舍。


限于篇幅一些网上可搜索的细节本文不展开阐述(例如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),例如一个定行程的交易,它由购买航班、租车以及预订酒店构成,而航班预订可能需要一两天才能确认。为了统一对概念的理解,本文默认指的都是这类长事务。


分布式事务概念泛化的同时,也带来了一个技术问题,微服务下这类分布式事务的ACID该如何保证?是否仍然可以用传统两阶段提交/XA去解决?很可惜,基于数据库的XA有点像扶不起的阿斗,中看不中用。


二、为什么XA大家都不用?


其实也并非不用,例如在IBM大型机上基于CICS很多跨资源是基于XA协议实现的分布式事务,事实上XA也算分布式事务处理的规范了,但在为什么互联网中很少使用,究其原因我觉得有以下几个:


  • 性能(阻塞性协议,增加响应时间、锁时间、死锁);

  • 数据库支持完善度(MySQL 5.7之前都有缺陷);

  • 协调者依赖独立的J2EE中间件(早期重量级Weblogic、Jboss、后期轻量级Atomikos、Narayana和Bitronix);

  • 运维复杂,DBA缺少这方面经验;

  • 并不是所有资源都支持XA协议;

  • 大厂懂所以不使用,小公司不懂所以不敢用。


准确讲XA是一个规范、协议,它只是定义了一系列的接口,只是目前大多数实现XA的都是数据库或者MQ,所以提起XA往往多指基于资源层的底层分布式事务解决方案。其实现在也有些数据分片框架或者中间件也支持XA协议,毕竟它的兼容性、普遍性更好。


三、两阶段提交的“提升”


基于数据库的XA协议本质上就是两阶段提交,但由于性能原因在互联网高并发场景下并不适用。如果数据库只能保证本地ACID时,那么其中出现交易异常后,如何实现整个交易原子性A,从而保证一致性C呢?另外在处理过程中如何保证隔离性呢?


最直接的方法就是按照逻辑依次调用服务,但出现异常怎么办?那就对那些已经成功的进行补偿,补偿成功就一致了,这种朴素的模型就是Saga。但Saga这种方式并不能保证隔离性,于是出现了TCC。在实际交易逻辑前先做业务检查、对涉及到的业务资源进行“预留”,或者说是一种“中间状态”,如果都预留成功则完成这些预留资源的真正业务处理,典型的如票务座位等场景。


当然还有像Ebay提出的基于消息表,即可靠消息最终一致模型,但本质上这也属于Saga模式的一种特定实现,它的关键点有两个:


  • 基于应用共享事务记录执行轨迹;

  • 然后通过异步重试确保交易最终一致(这也使得这种方式不适用那些业务上允许补偿回滚的场景)。


这类分布式事务场景并不是微服务才出现的,在SOA时代其实就有了,常见的Saga、TCC、可靠消息最终一致等模型也都是很多年前就有了,只是最近几年随着微服务兴起,这些方案又重新被人关注了起来。


「Saga」参考链接:https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf


仔细对比这些方案与XA,会发现这些方案本质上都是将两阶段提交从资源层提升到了应用层。


  • Saga的核心就是补偿,一阶段就是服务的正常顺序调用(数据库事务正常提交),如果都执行成功,则第二阶段则什么都不做;但如果其中有执行发生异常,则依次调用其补偿服务(一般多逆序调用未已执行服务的反交易)来保证整个交易的一致性。应用实施成本一般。

  • TCC的特点在于业务资源检查与加锁,一阶段进行校验,资源锁定,如果第一阶段都成功,二阶段对锁定资源进行交易逻辑,否则,对锁定资源进行释放。应用实施成本较高。

  • 基于可靠消息最终一致,一阶段服务正常调用,同时同事务记录消息表,二阶段则进行消息的投递,消费。应用实施成本较低。


具体到基于这些模型实现的分布式事务框架,也多借鉴了DTP(Distributed Transaction Processing)模型。


DTP(Distributed Transaction Processing)参考链接:http://pubs.opengroup.org/onlinepubs/009680699/toc.pdf


▲ DTP模型


  • RM负责本地事务的提交,同时完成分支事务的注册、锁的判定,扮演事务参与者角色。

  • TM负责整体事务的提交与回滚的指令的触发,扮演事务的总体协调者角色。


不同框架在实现时,各组件角色的功能、部署形态会根据需求进行调整,例如TM有的是以jar包形式与应用部署在一起,有的则剥离出来需要单独部署(例如Seata中将TM的主要功能放到一个逻辑上集中的Server上,叫做TC( Transaction Coordinator ))


四、Seata架构得与失


今年初,阿里发布了开源分布式事务框架Fescar,后来跟蚂蚁TCC方案整合后改名为Seata,目前版本虽然只到0.6,但GitHub star已经过9k,一方面可见阿里在圈内推广能力,另外一方面也说明大家对阿里分布式事务框架的期待。


Seata的使用方式以及原理在其github wiki上已经阐述的很清晰,网上也已有很多源代码剖析的文章。接下来我们通过分析Seata AT模式原理,来看看它的亮点与问题。


「Seata的使用方式以及原理」参考链接:https://github.com/seata/seata/wiki


Seata对MT以及TCC的支持亮点有限,这两种模式更多是为了兼容已有应用生态。


Seata团队画了一个的详细调用流程图,对照此图阅读其源码会轻松很多。


▲ Seata执行流程图


1、亮点


相比与其它分布式事务框架,Seata架构的亮点主要有几个:


  • 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;

  • 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚;

  • 通过全局锁实现了写隔离与读隔离。


这些特性的具体实现机制其官网以及github上都有详细介绍,这里不展开介绍。


2、性能损耗


我们看看Seata增加了哪些开销(纯内存运算类的忽略不计):


一条Update的SQL,则需要全局事务xid获取(与TC通讯)、before image(解析SQL,查询一次数据库)、after image(查询一次数据库)、insert undo log(写一次数据库)、before commit(与TC通讯,判断锁冲突),这些操作都需要一次远程通讯RPC,而且是同步的。


另外undo log写入时blob字段的插入性能也是不高的。 每条写SQL都会增加这么多开销,粗略估计会增加5倍响应时间(二阶段虽然是异步的,但其实也会占用系统资源,网络、线程、数据库)。


前后镜像如何生成?

通过druid解析SQL,然后复用业务SQL中的where条件,然后生成Select SQL执行。


3、性价比


为了进行自动补偿,需要对所有交易生成前后镜像并持久化,可是在实际业务场景下,这个是成功率有多高,或者说分布式事务失败需要回滚的有多少比率?这个比例在不同场景下是不一样的,考虑到执行事务编排前,很多都会校验业务的正确性,所以发生回滚的概率其实相对较低。按照二八原则预估, 即为了20%的交易回滚,需要将80%的成功交易的响应时间增加5倍,这样的代价相比于让应用开发一个补偿交易是否是值得? 值得我们深思。


业界还有种思路,通过数据库binlog恢复SQL执行前后镜像,这样省去了同步undo log生成记录,减少了性能损耗,同时对业务零侵入,个人感觉是一种更好的方式。


4、全局锁


1)热点数据


Seata在每个分支事务中会携带对应的锁信息,在before commit阶段会依次获取锁(因为需要将所有SQL执行完才能拿到所有锁信息,所以放在commit前判断)。相比XA,Seata 虽然在一阶段成功后会释放数据库锁,但一阶段在commit前全局锁的判定也拉长了对数据锁的占有时间,这个开销比XA的prepare低多少需要根据实际业务场景进行测试。全局锁的引入实现了隔离性,但带来的问题就是阻塞,降低并发性,尤其是热点数据,这个问题会更加严重。


2)回滚锁释放时间


Seata在回滚时,需要先删除各节点的undo log,然后才能释放TC内存中的锁,所以如果第二阶段是回滚,释放锁的时间会更长。


3)死锁问题


Seata的引入全局锁会额外增加死锁的风险,但如果实现死锁,会不断进行重试,最后靠等待全局锁超时,这种方式并不优雅,也延长了对数据库锁的占有时间。


「Seata的引入全局锁会额外增加死锁的风险」参考链接:https://github.com/seata/awesome-seata/blob/master/wiki/en-us/Fescar-AT.md


5、其他问题


1)对于部分采用Seata的应用,如何保证数据不脏读、幻读?


Seata提供了一个@GlobalLock的注解,可以提供轻量级全局锁判定的功能(不生成undo log),但还是需要集成使用Seata。


2)TC在逻辑上是单点,如何做到高可用、高性能还是需要后续版本不断优化。


3)单机多数据源跨服务目前不支持。


五、分布式事务的取舍


严格的ACID事务对隔离性的要求很高,在事务执行中必须将所有的资源锁定, 对于长事务来说,整个事务期间对数据的独占,将严重影响系统并发性能。因此,在高并发场景中,对ACID的部分特性进行放松从而提高性能,这便产生了BASE柔性事务。柔性事务的理念则是通过业务逻辑将互斥锁操作从资源层面上移至业务层面。通过放宽对强一致性要求,来换取系统吞吐量的提升。另外提供自动的异常恢复机制,可以在发生异常后也能确保事务的最终一致。


基于XA的分布式事务如果要严格保证ACID,实际需要事务隔离级别为SERLALIZABLE。


由上可见柔性事务需要应用层进行参与,因此这类分布式事务框架一个首要的功能就是怎么最大程度降低业务改造成本,然后就是尽可能提高性能(响应时间、吞吐),最好是保证隔离性。


一个好的分布式事务框架应用尽可能满足以下特性:


  • 业务改造成本低;

  • 性能损耗低;

  • 隔离性保证完整。


但如同CAP,这三个特性是相互制衡的,往往只能满足其中两个,我们可以画一个三角约束:



基于业务补偿的Saga满足1.2;TCC满足2.3;Seata满足1.3。


当然如果我们要自己设计一个分布式事务框架,还需要考虑很多其它特性,在明确目标场景偏好后进行权衡取舍,这些特性包括但不限于以下:


  • 业务侵入性(基于注解、XML,补偿逻辑);

  • 隔离性(写隔离/读隔离/读未提交,业务隔离/技术隔离);

  • TM/TC部署形态(单独部署、与应用部署一起);

  • 错误恢复(自动恢复、手动恢复);

  • 性能(回滚的概率、付出的代价,响应时间、吞吐);

  • 高可用(注册中心、数据库);

  • 持久化(数据库、文件、多副本一致算法);

  • 同步/异步(2PC执行方式);

  • 日志清理(自动、手动);

  • ......


六、结语


分布式事务一直是业界难题,难在于CAP定理,在于分布式系统8大错误假设,在于FLP不可能原理,在于我们习惯于单机事务ACID做对比。无论是数据库领域XA、Google percolator或Calvin模型,还是微服务下Saga、TCC、可靠消息等方案,都没有完美解决分布式事务问题,它们不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。


「分布式系统8大错误假设」参考链接:http://%5Bhttps://en.wikipedia.org/wiki/Fallacies_of_distributed_computing%5D(https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing)


「FLP不可能原理」参考链接:html%5D(https://www.cnblogs.com/firstdream/p/6585923.html)


其实由于网络的不确定性,分布式下很多问题都是难题,最好的方案是避免分布式事务:)


最后回到主题,Seata解决了分布式事务难题了吗?看你最在意哪方面了。如果你希望业务尽量少感知,DB操作简单,那它会给你带来惊喜;但如果你更看重响应时间,DB写操作较多,调用链条较长,那它可能会让失望。最后希望Seata开源项目越做越好!

相关 [分布] 推荐:

分布式日志

- - Java - 编程语言 - ITeye博客
最近完成一个简单的日志管理系统,拿出来跟大家分享一下. 3、支持文件输出、habse输出、mongodb输出. 基于以上三点功能,我们下面详细说明. 说道支持这个功能,有个同事认为没有这个必要,他的观点是log4j的配置不需要经常变动,不需要支持这样的功能;本人的观点是“配置可以进行统一管理、而且正式机跟测试机的log4j的配置肯定会有一些差异的”,因此这个功能是必须的.

分布式事务简述

- If you are thinking one year ahead, you plant rice. If you are thinking twenty years ahead, you plant trees. If you are thinking a hundred years ahead, you educate people. - BlogJava-首页技术区
  随着系统越来越大,不断的模块化和SOA化,你的系统可能被分散于不同的机器上,这时候,你原先的单机本地事务可能已经无法满足你的需求,你可能要跨系统跨资源的去使用事务.   具体就不多介绍了,相信大家都能明白ACID特性的基本含义. 而一个具体的事务需要涉及到的模型(无论哪种模型)一般由下面几部分组成:.

Hadoop与分布式计算

- 透明 - 丕子
写本文由leftnoteasy发布于http://leftnoteasy.cnblogs.com 本文可以被全部或者部分的使用,但请注明出处,如果有问题,可以联系wheeleast (at) gmail.com, 也可以加作者的新浪微博:http://weibo.com/leftnoteasy. 很久没有写写博客了,之前主要是换工作,耽误了很多的时间,让人也变得懒散,不想花大时间来写东西.

分布式缓存-Memcached

- - 人月神话的BLOG
分布式缓存出于如下考虑,首先是缓存本身的水平线性扩展问题,其次是缓存大并发下的本身的性能问题,再次避免缓存的单点故障问题(多副本和副本一致性). 分布式缓存的核心技术包括首先是内存本身的管理问题,包括了内存的分配,管理和回收机制. 其次是分布式管理和分布式算法,其次是缓存键值管理和路由. 原文: http://wenku.baidu.com/view/8686d46c7e21af45b307a8c3.html.

再谈集中和分布

- - 人月神话的BLOG
上篇文章转载了关于mysql数据库的垂直和水平拆分的相关内容,本篇文章再谈下关于应用集中化后的集中和分布相关策略问题,以及最近关于完全去IOE思路的一些思考和回顾. 现在有一个问题我暂时还没得到比较明确的一些验证,如对于当前x86的pc server服务器好的配置完全可以达到200万TPMC,对于磁盘阵列可以挂接到20T甚至更高的存储容量.

hadoop分布式配置

- - CSDN博客云计算推荐文章
一、前面的部分见伪分布式配置. 二、实现SSH无密码登录远程主机(只在源主机上配置). 注意:以上scp命令表示把authoriezd_keys远程复制到对应主机的相应目录下. slave2是目的主机的名字,需要在源主机的/etc/hosts下配置slave2以及对应的IP地址 192.168.0.5.

浅谈分布式缓存

- - CSDN博客推荐文章
在前面的一些文章中,从实战的角度,讲解了有关 memcached的应用、容灾、监控等等. 但是缺乏对理论的讲解和原理性的剖析. 本文将从理论的角度去介绍,让大家从宏观上对“分布式缓存、nosql”等技术有所了解,以便进一步学习和使用. 在构建大规模的web应用时,缓存技术可以说是必备的,学习的必要性不言而喻.

关于分布式事务

- - Web前端 - ITeye博客
Mysql当前分布式事务只支持Innodb存储引擎. 1个分布式事务由多个行为在不同的数据库上执行,1个分布式事务的执行成功意味着相关数据库上的行为执行均成功. 使用分布式事务的应用程序设计1个或多个资源管理器和一个事务管理器. 资源管理器(RM):用户提供通向事务的途径. 数据库服务器是一个种资源管理器.

BDRP分布式redis集群

- - 百度运维团队技术博客
BDRP(baidu distributed redis platform)是包含 twemproxy, redis,redis-sentinel等多个模块开发的分布式redis平台. bdrp已经在github上进行了开源, bdrp的github项目点这里. 目前redis集群架构主要有以下几个组件: twemproxy:redis的代理系统,可以选择多种数据分片算法 redis:集群的redis存储节点 sentinel:redis官方的集群高可用组件,可以监控redis主节点故障,并进行主备切换.

分布式搜索算法

- - 杨尚川的个人页面
对于搜索引擎来说,索引存放在成千上万台机器上,如何进行分布式搜索呢. 假设搜索结果是以分页的方式显示,以PageNumber代表当前页,从1开始,以PageSize代表页面大小,默认为10,以N代表搜索服务器数量. 最简单的分布式搜索算法为:有一台 合并服务器负责接受用户的搜索请求,然后分别向N台机器获取前PageNumber*PageSize条结果,得到的结果数为N*PageNumber*PageSize,然后把这些数据重新进行排序,根据所要显示的页面PageNumber,获取从(PageNumber - 1) * PageSize + 1开始的PageSize条结果返回给用户.