6.3 事务与柔性事务
在上面两节中不管是业务流程异步化,还是数据库事务异步化,其实都面临一个如何保证业务事务一致性的问题。面对这个问题目前并没有完美的解决方案,本节会介绍淘宝是如何对订单创建场景实现业务一致的实践,以及近一两年来我们在分布式事务上所作出的创新尝试,供各技术同行在解决此类问题时借鉴和参考。
关于数据库事务,相关的文档资料已经非常多,这里不做赘述,核心是体现数据库ACID(原子性、一致性、隔离性和持久性)属性,即作为一个事务中包含的所有逻辑处理操作在作用到数据库上时,只有这个事务中所有的操作都成功,对数据库的修改才会永久更新到数据库中,任何一个操作失败,对于数据库之前的修改都会失效。
传统数据库的事务确实非常好地保证了业务的一致性,但在互联网场景下,比如上文提到淘宝订单和互联网金融P2P场景下,就暴露出数据库性能和处理能力上的瓶颈。所以在分布式领域,基于CAP理论和在其基础上延伸出的BASE理论,有人提出了“柔性事务”的概念。
在结合具体实例对柔性事务进行介绍前,有必要对CAP和BASE理论做一个简单的介绍,这样才能让大家更加清晰地理解互联网场景下解决事务问题的思路和理论出发点。
1.CAP理论
2000年7月,加州大学伯克利分校的Eric Brewer教授在分布式计算原则研讨会议上提出CAP猜想。直到2002年,由麻省理工学院的Seth Gilbert和Nancy Lynch从理论上证明了CAP理论,从而让CAP理论正式成为分布式计算领域的公认定理。
CAP理论:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
“一致性”指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。一致性指在一个系统中不论数据存放在何处,作为一个整体应是完整和一致的。在分布式系统中,数据通常不会只有一份,用户对数据进行一定的修改操作(增/删/改)之后,为了保证数据的一致性,那么应该对所有数据进行相同的操作并且这些操作应该是同时成功或者同时失败的。
如果一个存储系统可以保证一致性,那么客户读写的数据完全可以保证是最新的。不会发生两个不同的客户端在不同的存储节点中读取到不同副本的情况。
具体来说,系统中对一个数据的读和写虽然包含多个子步骤并且会持续一段时间才能执行完,但是在调用者看来,读操作和写操作都必须是单个即时完成的操作,感知不到其他调用者对这些数据的访问。对一个写操作,如果系统返回了成功,那么之后到达的读请求都必须读到这个新的数据;如果系统返回失败,那么所有的读,无论是之后发起的,还是和写同时发起的,都不能读到这个数据。
“可用性”指用户在访问数据时可以得到及时的响应。可用性是关于一个系统能够持续不间断使用的问题,严格的定义是高性能可用性。这意味着一个系统从设计到实施都应该能够提供可持续的操作(如读写操作),无论是操作冲突,还是软硬件部分因为升级而导致失效。但是可用性并不意味着数据的一致性,比如读取到的数据是过期数据或脏数据,但对于用户仍有返回数据的情况下,仍然可以被认为是可用的。
同时可用性的要求包含时效性,对于大多数应用而言,超过一定响应时间的服务是没有价值的或者价值量低的。例如,阿里巴巴和Google这样的公司很细小的响应延迟都会对公司的盈利造成损失。
“分区容错性”指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。Gilbert和Lynch是这样定义分区容忍性的:除了整个网络的故障外,其他的故障(集)都不能导致整个系统无法正确响应。
一旦针对同一服务的存储系统分布到了多个节点后,整个存储系统就存在分区的可能性。分区不是简单的理解为物理存储节点的分布,而且是节点间网络通信中断或报文丢失等。比如,两个存储节点之间联通的网络断开(无论长时间或者短暂的),就形成了分区,一旦开始将数据和逻辑分布在不同的节点上,就有形成分区的风险。假定网线被切断,分区就形成了,节点A无法和节点B通信。正如数据库的数据在进行了分库分表后,就是典型的分区状态。
举例说明
为了能更直观地了解CAP定理,我们举一个简单的例子。某用户在电商平台上搜索到一件喜欢的衣服,但是用户并没有立即购买,她选择先浏览其他类似商品。与此同时,另一用户也看上了这件衣服并直接下单创建了订单,而恰巧这件衣服的库存仅剩下最后一件,那么当第一个用户在返回页面决定购买该衣服时,由于这件衣服的库存已经为零,理论上她是不能再购买该衣服的。假设网站的数据是以分布式系统的方式存储在多个机房或多个数据库中,那么,一个商品的库存信息就被存储在不同地方,那必然存在数据的同步和一致性问题。如果第一个用户在进行衣服订单的创建时,该请求所访问的数据库没有得到及时更新(在该数据库中,该商品的库存本应该是零),那么第一用户可能也为该衣服进行了成功的订单创建,就出现了商品超卖的情况。这里所讲的就是分布式系统中的数据一致性问题。
在这个例子中,如何解决数据一致性的问题?一个简易的方案就是建立类似操作系统中锁的机制,要求确保所有数据节点的数据均同步之后,才能进行数据的访问操作,也就是在第二个客户对该衣服创建订单时,商品的库存修改为零,接着等到该库存信息在所有数据库上都同步后,再返回给客户告知订单成功创建。在此过程中,系统不接受对该衣服库存数据的修改操作,所以等到第一个客户希望创建订单时,因为所有数据库中该商品的库存信息都已经被同步为零,所以系统会给第一个客户做出库存不足的提示,而无法进行订单的创建。但这引入了一个新问题,就是可用性问题。由于不同数据节点间的数据同步是需要时间的,而且大量采用锁机制会给数据层带来严重的性能瓶颈,从而可能导致平台在业务繁忙时的服务瘫痪或糟糕的用户体验(点击平台上一个业务请求所需要等待的时间过长)。一个客户无法访问的服务对任何人都没有价值,这就是分布式系统中的可用性问题。
另一个做法就是商品的库存数据只保存一份,不做复制,这样就不会存在数据一致性的问题。而因为网站的数据量太大,一个数据节点无法容纳如此大容量的数据,所以把整体数据分割成若干部分,每一部分存储在不同节点上,这也就是典型的数据进行分库分表的操作,这样就能解决可用性的问题。但这样也会有个很明显的问题,假如某一时刻数据节点间的网络阻塞或者切断了,那么会导致网站可能获取不到完整的数据。这就是分区容忍性的问题。所以,三个核心需求之间无法同时得到完全的保证。
CAP之间的取舍
根据前面的介绍,CAP理论的核心是:一个分布式系统不可能同时很好地满足一致性、可用性和分区容错性这三个需求,最多只能同时较好地满足两个。
CAP定理并不意味着所有系统的设计都必须抛弃三个要素之中的一个。CAP三者可以在一定程度上衡量,并不是非黑即白的,例如可用性从0%到100%有不同等级。显然我们有几种组合选择问题。
1)放弃分区容忍性。为了避免分区问题发生,一种做法是将所有与事务相关的东西都放到一台机器上,但这并不能100%地保证,因为在一台机器上还是有可能部分失败的情况发生,虽然这种情况下由分区问题带来的负面效果不易被察觉到。但是,系统从分布式系统退化为单机系统,从根本上失去了可扩展性,这个选择会严重影响系统规模。实践中,大部分人认为位于单一地点的数据中心内部是没有分区的,因此在单一数据中心之内可以选择CA(一致性,可用性);CAP理论出现之前,系统都默认这样的设计思路,包括传统数据库在内。然而在今天互联网时代,各种业务的数据量都出现了爆发式增长的态势,单一数据中心越来越多的出现分区的情况,这就动摇了以CA为取向的设计基础。
2)放弃可用性。相对于放弃分区容忍性来说,其反面就是放弃可用性。一旦遇到分区事件,受影响的服务需要等待数据一致,因此在等待期间就无法对外提供服务。在多个节点上控制这一点会相当复杂,而且恢复的节点需要处理逻辑,以便平滑地返回服务状态。
3)放弃一致性。其实,在互联网应用没有盛行之前,传统应用中出现分区的情况并不多见,系统在大多数情况下是允许完美的C(一致性)和A(可用性)。但当分区存在或者可感知其影响的情况下,就要预备一种策略去探知分区并显式处理其影响。这样的策略一般分为三个步骤:探知分区发生,进入显式的分区模式以限制某些操作,启动恢复过程以恢复数据一致性并补偿分区期间发生的错误。分区期间,独立且能自我保证一致性的节点子集合可以继续执行操作,只是无法保证全局范围的不变性约束不受破坏。数据分片就是这样的例子,架构师预先将数据划分到不同的分区节点,分区期间单个数据分片多半可以继续操作。相反,如果被分区的是内在关系密切的状态,或者有某些全局性的不变性约束非保持不可,那么最好的情况是只有分区一侧可以进行操作,最坏情况是操作完全不能进行。
2.BASE理论
eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出了BASE理论。BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。
BASE是指基本可用(Basically Available)、柔性状态(Soft State)、最终一致性(Eventual Consistency)。
·“基本可用”是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
·“柔性状态”是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是柔性状态的体现。MySQL Replication的异步复制也是一种柔性状态体现。
·“最终一致性”是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
ACID和BASE的区别与联系
ACID和BASE代表了两种截然相反的设计哲学。ACID是传统数据库常用的设计理念,追求强一致性模型;BASE支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。
ACID(英文中是酸)和BASE(英文中有碱性的意思)在化学的世界中两者(酸和碱)是完全对立的,具有独特而相反的性质。在实际的系统设计中,不同业务场景对一致性要求是不同的,因此ACID和BASE一般在系统中又会结合使用。那为什么在今天互联网应用中会更广泛采用柔性事务而放弃传统数据库事务来解决业务事务性问题?
刚刚提到,柔性事务是由于互联网应用的需求产生的,那么需要看互联网应用的核心诉求是什么。互联网应用的最核心需求是高可用(也就是BASE的BA)。对于这一点可能很多非互联网企业的读者会产生怀疑,或许是遇到过或看到过很多的互联网平台出现系统崩塌和不可访问的情况,就让人感觉互联网应用出现不可用是一件经常发生的事情,而且看起来对业务的影响并没有多大。而作者所面对的金融、政府职能单位的技术同仁,都会强调自身业务对高可用性的要求要远超互联网企业,我想说这是一个不小的误解。
在今天互联网时代有着上百万的互联网的应用和平台,我认为其中很大一部分的平台所依靠的技术能力和架构都是有明显缺陷和隐患的,这也直接导致有些平台所提供的服务并不是稳定可靠的,所以才给人有了这样的印象:互联网平台出现问题很正常,对业务不会有太大影响。但从专业的角度来说,一个专业的互联网平台一定首先考虑的是系统服务能力的高可用,因为服务不可用意味着就是商业损失。淘宝交易高峰10秒钟不可用,可能损失价值千万的交易订单,如果在双11这一天,10秒钟可能影响的是上亿的订单交易。
服务的不可用除了造成金钱上的损失外,更为严重的是造成对平台的伤害。平台的服务不可用一定会给客户带来商业损失或者糟糕的用户体验,同时也会让这些客户失去对平台的信心,如果造成平台客户流失将会给这个互联网平台带来更为可怕的后果。
对于如何实现高可用,我们认为:
高可用=系统构建在多机=分布式系统
高性能=分布式系统的副产品
今天大家都知道淘宝去IOE的故事,但当年去Oracle排名第一位的需求不是性能,而是因为Oracle是系统单点。一旦Oracle不可用,整个系统100%不可用,而Oracle每年总是会出1次故障。既然为了高可用采用了分布式系统,那么分布式系统特性决定了柔性事务的第二个特性:最终一致。
分布式系统内通信和单机内通信最大的区别是:单机系统总线不会丢消息,而网络会。
一台向另一台机器通信的结果可能是收到、未收到、不知道收到没收到。消息不可靠带来的副作用是:数据或者状态在多机之间同步的成本很高。
大家都知道Paxos协议。在多机间通信不存在伪造或篡改的前提下,可以经由Paxos协议达成一致性。成本是发给Paxos系统的信息(数据)需要至少同步发送到一半以上多数(Quorum)的机器确认后,才能认为是成功。这样大幅增加了信息更新的延迟,因此分布式系统的首选不是这种强同步而是最终一致。
而采用最终一致,一定会产生柔性状态。
3.传统分布式事务
上面提到数据在按照业务领域(用户中心、交易中心)的不同被拆分到不同的数据库后,在某些业务场景(比如订单创建)下,就必然会出现同一个事务上下文中,需要协调多个资源(数据库)以保证业务的事务一致性,对于这样的场景,业界早就有基于两阶段提交方式实现的分布式事务(如图6-5所示),两阶段提交协议包含了两个阶段:第一阶段(也称准备阶段)和第二阶段(也称提交阶段)。一个描述两阶段提交很好的类比是典型的结婚仪式,每个参与者(结婚典礼中的新郎和新娘)都必须服从安排,在正式步入婚姻生活之前说“我愿意”。一旦其中一位“参与者”在做出承诺前的最后一刻反悔,则这场婚礼就演变为一个悲剧。两阶段提交之于此的结果也成立,虽然不具备如婚礼上那么大的破坏性。
图6-5 传统两阶段提交分布式事务
当commit请求从客户端向事务管理器发出,事务管理器开始两阶段提交过程。在第一阶段,所有的资源被轮询到,问它们是否准备好了提交作业。每个参与者可能回答“就绪”(READY)、“只读”(READ_ONLY)或“未准备好”(NOT_READY)。如果有任意一个参与者在第一阶段响应“未准备好”(NOT_READY),则整个事务回滚。如果所有参与者都回答“就绪”(READY),那这些资源就在第二阶段提交。回答“只读”(READ_ONLY)的资源,则在协议的第二阶段处理中被排除掉。
两阶段提交协议要求分布式事务参与者实现一个特别的“准备”操作,无论在资源管理器(如数据库)还是在业务服务中实现该操作都存在效率与复杂性的挑战。因此,两阶段提交协议有一个重要的优化,称为“最末参与者优化”(Last Participant Optimization,LPO),允许两阶段提交协议中有一个参与者不实现“准备”操作(称为单阶段参与者)。最末参与者优化的原理如图6-6所示。
图6-6 最末参与者优化(LPO)示例
从图6-5中可见,LPO中单阶段参与者不需要实现准备操作,只需要提供标准的提交操作即可。分布式事务协调者必须等其余两阶段参与者都准备好之后,再请求单阶段参与者提交,单阶段参与者的提交结果将决定整个分布式事务的结果。本质上,LPO是将最后一个参与者的准备操作与提交/放弃操作合并成一个提交操作。
X/Open组织为基于两阶段协议的分布式事务处理系统提出了标准的系统参考模型(X/Open事务模型)以及不同组件间与事务协调相关的接口,使不同厂商的产品能够互操作。X/Open事务模型如图6-7所示。
图6-7 X/Open事务模型
从图中可以看出,X/Open模型定义了两个标准接口:TX接口用于应用程序向事务管理器发起事务、提交事务和回滚事务(即确定事务的边界和结果);XA接口形成了事务管理器和资源管理器之间的通信桥梁,用于事务管理器将资源管理器(如数据库、消息队列等)加入事务、并控制两阶段提交。
事务管理器一般由专门的中间件提供,或者在应用服务器中作为一个重要的组件提供。资源管理器如数据库、消息队列等产品一般也会提供对XA接口的支持,通过使用符合X/Open标准的分布式事务处理,能够简化分布式事务类应用的开发。
从图6-5中可见,两阶段提交协议的关键在于“预备”操作。分布式事务协调者在第一阶段通过对所有的分布式事务参与者请求“预备”操作,达成关于分布式事务一致性的共识。分布式事务参与者在预备阶段必须完成所有的约束检查,并且确保后续提交或放弃时所需要的数据已持久化。在第二队段,分布式事务协调者根据之前达到的提交或放弃的共识,请求所有的分布式事务参与者完成相应的操作。很显然,在提交事务的过程中需要在多个资源节点之间进行协调,而各节点对锁资源的释放必须等到事务最终提交时,这样,比起一阶段提交,两阶段提交在执行同样的事务时会消耗更多时间:
·单机锁=时间消耗(微秒级)
·跨多机的锁=时间消耗(毫秒级)=1000倍单机时间消耗
事务执行时间的延长意味着锁资源发生冲突的概率增加,当事务的并发量达到一定数量的时候,就会出现大量事务积压甚至出现死锁,系统性能和处理吞吐率就会严重下滑,也就是系统处理的吞吐率与资源上的时间消耗成反比(参考阿姆达尔定理)。这就是为什么今天在互联网应用场景中鲜有人会选择这样传统的分布式事务方式,而选择柔性事务处理业务事务的主要原因。
4.柔性事务如何解决分布式事务问题
(1)引入日志和补偿机制
类似传统数据库,柔性事务的原子性主要由日志保证。事务日志记录事务的开始、结束状态,可能还包括事务参与者信息。参与者节点也需要根据重做或回滚需求记录REDO/UNDO日志。当事务重试、回滚时,可以根据这些日志最终将数据恢复到一致状态。
为避免单点,事务日志是记录在分布式节点上的,数据REDO/UNDO日志一般记录在业务数据库上,可以保证日志与业务操作同时成功/失败。通常柔性事务能通过日志记录找回事务的当前执行状态,并根据状态决定是重试异常步骤(正向补偿),还是回滚前序步骤(反向补偿)。
在互联网业界采用日志方式实现柔性事务的比例非常大,但因为这部分的技术实现并没有如XA这样的技术标准和规范,看到很多互联网应用对这部分的实现非常的粗糙,只是简单的采用数据库进行了分布式事务过程中的状态记录,对于事务中异常处理和补偿回滚支持是明显不够的,并不能完全意义上的满足业务的最终一致性,而且一旦出现问题,所投入的人力维护成本也非常高昂。
(2)可靠消息传递
在分布式环境下,由于“网络通信危险期”(见下面阅读框中内容)的存在,节点间的消息传递会有“成功”、“失败”、“不知道成功还是失败”三种状态。这也给进行分布式事务处理时提出了更多的考虑点和要求。可靠消息投递就是为了解决这类问题产生的服务平台。
根据“不知道成功还是失败”状态的处理,消息投递只有两种模式:1)消息仅投递一次,但是可能会没有收到;2)消息至少投递一次,但可能会投递多次。在业务一致性的高优先级下,第一种投递方式肯定是无法接受的,因此只能选择第二种投递方式。
由于消息可能会重复投递,这就要求消息处理程序必须实现幂等(幂等=同一操作反复执行多次结果不变),这一要求跟传统应用开发相比是非常具有互联网特征的一种模式,在很多的应用场景下,都会要求程序实现幂等。
每种业务场景不同,实现幂等的方法也会有所不同,最简单的幂等实现方式是根据业务流水号写日志,阿里内部一般把这种日志叫做排重表。在接下来的场景示例中会有更加详细的介绍。
关于网络通信的危险期
由于网络通信故障随时可能发生,任何发出请求后等待回应的程序都会有失去联系的危险。这种危险发生在发出请求之后,服务器返回应答之前,如果在这个期间网络通信发生故障,发出请求一方无法收到回应,于是无法判断服务器是否已经成功地处理请求,因为收不到回应可能是请求没有成功地发送到服务器,也可能是服务器处理完成后的回应无法传回请求方。这段时间称为网络通信的危险期(In-doubt Time)。很显然,网络通信的危险期是分布式系统除单点可靠性之外需要考虑的另一个问题。
(3)实现无锁
现在大家都知道造成数据库性能和吞吐率瓶颈往往是因为强事务带来的资源锁。如何很好地解决数据库锁问题是实现高性能的关键所在。所以选择放弃锁是一个解决问题的思路,但是放弃锁并不意味着放弃隔离性,如果隔离性没有保障,则必然带来大量的数据脏读、幻读等问题,最终导致业务不可控地不一致。
实现事务隔离的方法有很多,在实际的业务场景中可灵活选择以下几种典型的实现方式。
·避免事务进入回滚。如果事务在出现异常时,可以不回滚也能满足业务的要求,也就是要求业务不管出现任何情况,只能继续朝事务处理流程的顺向继续处理,这样中间状态即使对外可见,由于事务不会回滚,也不会导致脏读。
·辅助业务变化明细表。比如对资金或商品库存进行增减处理时,可采用记录这些增减变化的明细表的方式,避免所有事务均对同一数据表进行更新操作,造成数据访问热点,同时使得不同事务中处理的数据互不干扰,实现对资金或库存信息处理的隔离。比如在用户进行订单创建操作时,需要对商品的库存进行减扣,如果是在秒杀和大促场景下,大量订单都是对同一商品进行下单操作,如果所有订单创建事务中都是修改商品表中商品数据的库存的信息,则必然会出现该条商品记录访问热点,而且很容易出现锁抢占的情况,避免锁的方式就是在订单创建事务中只是在“库存预减明细表”中添加一条对应商品的库存预减记录(见图6-8),而无需对原商品数据表进行库存修改的操作,一旦用户成功付款,则真正地将商品数据表中的库存减除。在付款之前当应用要获取该商品的库存信息时,则是通过以下公式获得:
商品当前库存数量=商品表中的库存数量-预减明细表中该商品对应明细表中库存数量之和
图6-8 扣减库存场景下的业务变化明细表示意
·乐观锁。数据库的悲观锁对数据访问具有极强的排他性,也是产生数据库处理瓶颈的重要原因,采用乐观锁则在一定程度上解决了这个问题。乐观锁大多是基于数据版本(Version)记录机制实现。例如通过在商品表中增加记录版本号的字段,在事务开始前获取到该商品记录的版本号,在事务处理最后对该商品数据进行数据更新时,可通过在执行最后的修改update语句时进行之前获取版本号的比对,如果版本号一致,则update更新数据成功,修改该数据到新的版本号;如果版本号不一致,则表示数据已经被其他事务修改了,则重试或放弃当前事务。如图6-9所示,当两个事务同时要对商品数据表进行更新操作时,通过版本号的方式实现乐观锁机制的示意。
从以上示例可以看出乐观锁机制避免了长事务中的数据库加锁开销。大大提升了大并发量下的系统整体性能表现。需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在应用中实现的,如果有另一个应用也对商品数据进行更新操作时则不一定会遵循乐观锁机制,因此可能会造成脏数据被更新到数据库中。这也就是为什么在构建共享服务体系时,对于数据的操作都会要求统一到该数据库对应的服务中心层,而不允许其他应用再对该数据库进行单独访问和操作。
图6-9 通过版本实现乐观锁示意
5.柔性事务在阿里巴巴内部的几种实现
基于以上柔性事务实现分布式事务的思路以及从多年对互联网业务场景特性的深度剖析,从阿里巴巴内部共发展和演变出三套成熟的分布式事务解决方案,下面分别介绍。
(1)消息分布式事务
在淘宝平台中,被广泛用来解决分布式事务场景的方案就是基于消息分布式事务,通过MQ事务消息功能特性达到分布式事务的最终一致。
实现原理及流程。图6-10是基于MQ提供的事务消息功能实现分别对两个不同数据库进行事务处理的流程示意。
步骤的①②是在MQ发送方(即整个分布式事务的发起方)执行第一个本地事务前,会向MQ服务端发送一条消息,但这条消息不同于MQ上的普通消息,而是一条事务消息(事务消息功能是阿里巴巴MQ平台特有的一个功能特性),事务消息在MQ的服务端处于一个特殊的状态,此时该消息已经保存到MQ服务端,但MQ订阅方是无法感知到该消息,更无法对该消息进行消费,否则就可能出现第一个本地事务并没有执行成功,而后面一个本地事务执行的业务不一致问题。
图6-10 MQ实现分布式事务流程示意
完成了事务消息的发送后,步骤③才正在开始执行对第一个数据库进行单机事务的操作。此时本地事务执行的情况决定了后面几个步骤的各自目的:
a)当本地事务执行成功后,会执行步骤④将原本保存在MQ服务端的事务消息的状态更新为正常的消息状态。
b)当本地事务执行时,如果因为某些原因(网络或当前运行机器宕机)造成程序没有及时给MQ服务端相应的反馈,则之前发送到MQ服务端的事务消息会一直保存在MQ服务端,为了保证事务继续执行,MQ服务端提供了对服务器上保存在事务消息堆栈中的事务消息进行定时扫描,如果发现一段有事务消息在该堆栈的保存时间超过了一段时间(比如5分钟),此时MQ服务端会执行步骤⑤,发起一个请求发送到具有跟之前MQ发送发具有同样生产者ID的MQ发送方(与前一发送方具有同一应用代码)其中的一个实例上(因为有可能之前运行的那个实例已经宕机了),该请求的目的是让MQ发送发去检查之前执行的本地事务到底是否执行成功还是失败。
c)步骤⑥在对数据库进行了之前本地事务执行结果的确认后,如果发现本地事务根本没有执行,则给MQ服务端返回结果,告知MQ服务端可扔弃该事务消息;如果检查发现之前执行的本地事务实际上已经成功执行了,只是因为各种原因没能及时到MQ服务端更新事务消息的状态,此时只需更新服务端上事务消息的状态为正常状态即可。所以在前面这部分步骤中,核心是让第一个本地事务的执行和MQ服务端的消息能否被投递和消费同时成功或者同时失败,而不会出现本地事务并没有被成功执行,但消息已经被消息的消费者消息,进行了下一个本地事务的执行,在某种程度上是保证了本地事务和消息发送的事务性,所以我们称为该类消息为事务消息。
步骤⑧则是在消息订阅方获取到由事务消息置为正常状态的消息后,通过消息里的事务和业务信息执行第二个本地事务的执行。如果第二个本地事务执行成功,则最终实现了两个不同数据库上的事务同时成功;如果第二个本地事务执行失败,则还可通过消息的方式通知MQ发送发,对第一个本地事务进行业务的回滚操作。
总体来看,通过消息进行事务异步的方式,保证了前后两个数据库事务同时执行成功或失败,保持了事务的一致性,同时因为避免了传统两阶段提交事务方式对数据长时间的资源锁定,所以数据库整体的吞吐率和性能大大超过传统的分布式事务方式。
从本质上来说,对比柔性事务解决分布式事务的思路,消息服务在其中扮演了事务日志的职能,对全局事务有一个统一的记录和调度能力;事务的参与者通过对消息订阅关系建立了事务间的关联。在采用消息服务实现分布式事务的场景如果出现异常时,一般会采用正向补偿的方式,即不会像传统事务方式出现异常时依次进行回滚,会通过消息的不断重试或人工干预的方式让该事务链路继续朝前执行,而避免出现事务回滚。
正如以上对消息实现分布式事务的流程描述,在事务发起方要实现事务消息的发送,本地事务的执行,同时对当出现事务消息没有及时收到发起方更新反馈信息的时候,还要实现本地事务状态检查的代码实现,以及如果出现异常时,业务的回滚控制。也就是原本通过数据库的事务特性实现的事务执行检查和回滚都需要靠开发人员来实现,这对于开发人员提供了额外更高的要求,这种情况在淘宝提供了规范的技术培训和开发人员不错的开发素养情况下,还能较好的应付,但对于阿里巴巴外部的开发人员来说,要完整的考虑业务回滚和事务检查等开发习惯,其实是有一定的难度,所以为了给开发人员提供更好的开发体验,降低因为开发人员的原因导致业务不一致现象的发生,才有了后面要介绍的新一代分布式事务平台TXC的诞生。
典型应用场景。在淘宝平台中,使用消息服务实现分布式事务的场景众多,其中淘宝的订单交易则是对这一分布式事务方式体现最为典型的一个场景。
图6-11分别示意了在淘宝下单和付款两个操作时几个主要业务步骤的执行示意,其中在下单这个分布式事务操作中包含了库存预减、创建交易订单、创建支付订单几个主要操作,核心就是通过MQ消息服务的方式实现了这几个操作的事务最终一致性。在付款事务中,对扣款、创建扣款流水、实减库存、修改订单状态等进行了事务操作,其中也都是有MQ服务实现了整个分布式事务的事务一致性。
图6-11 淘宝交易流程采用MQ实现分布式事务
在订单创建或付款出现异常,比如实减库存失败、付款超时时,同样也会通过消息服务的方式,通知相关的服务进行订单状态的修改、支付宝中支付订单状态的更新及退款操作、预减库存回撤等相关操作,所有的这些操作可能由不同的服务完成相应操作,但整体保持事务性。整个过程如图6-12所示。
除了淘宝的订单场景之外,比如上文提到的异构索引数据的同步等类似跨库数据更新的场景均是采用消息的方式实现的。
总之,采用消息服务实现的分布式事务很好地实现了应用服务化后业务处理流程的异步化,大大提升了整个业务处理的吞吐率和响应时间。但你会发现采用消息事务的方式,在两个事务间实现分布式事务时,可以很好地满足事务最终一致性以及事务的回滚,但如果一个事务上下文中超过两个事务操作后,因为事务的回滚逻辑变得非常复杂而不可控,所以在这样的场景下只能进行正向的事务补偿,在某些业务场景下会给带给客户不同的体验。
图6-12 淘宝交易出现异常时的业务回滚实现
(2)支付宝XTS框架
支付宝的XTS分布式事务框架是基于BASE的思想实现的一套类似两阶段提交的分布式事务方案,用来保障在分布式环境下高可用性、高可靠性的同时兼顾数据一致性的要求。与上面介绍的基于消息实现的分布式事务仅支持正向补偿,XTS可同时支持正向和反向补偿。
XTS是TCC(Try/Confirm/Cancel)型事务(如图6-13所示),属于典型的补偿型事务。
·Try阶段主要是对业务系统做检测及资源预留。
·Confirm阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
·Cancel阶段主要是在业务执行错误需要回滚的状态下,执行业务取消,预留资源释放。
典型业务处理模式。支付宝的主体业务基本都会在一次业务处理中进行一次或多次账务处理。典型的业务处理模式如图6-14所示。
图6-13 TCC型事务模型
图6-14 TCC处理账务的处理流程
这种模式可以概括如下:
1)支付宝的主体业务服务在执行过程中一般都会涉及一次或者多次的账务处理。
2)业务服务与账务服务对业务处理的最终结果有同等的决定权,两者都能够使业务处理失败。
3)当一次业务处理中涉及超过两个参与者时,附加的参与者一般对业务处理的最终结果没有决定权,但它们会根据业务处理的最终结果完成自己的处理。例如,很多业务在完成之后都涉及收费的处理,但一般收费不成功不会影响业务处理本身的结果。
根据两参与者的特点,以及账务服务的中心地位,我们可以根据“两阶段提交协议”以及“最末参与者优化”原理,设计支付宝分布式事务处理的基础模型。
如图6-15所示,XTS各个主要组件的职责如下:
·“业务服务”负责具体业务处理,如交易服务、红包服务等等。
·“账务前置”负责接收、检查并缓冲从业务服务发起的账务请求。
·“账务核心”负责记账并更新分户余额。
·“主事务管理器”与“业务服务”位于同一个“本地事务域”,负责主事务的启动、提交与回滚。
图6-15 XTS分布式事务架构
·“分支事务管理器”与账务服务操作位于同一个本地事务域,负责分支事务的准备,确认与取消。
·“事务恢复daemon”定时运行,负责恢复处于已准备状态,但在指定时间阈值内尚未确认或者取消的事务。
下面我们介绍上述组件如何通过协作完成一次包含账务的业务处理。
准备阶段。图6-16显示在“准备”阶段各个组件之间的交互过程。
图6-16 XTS组件在准备阶段的交互流程
1)“业务服务(1)”首先向主事务管理器请求开始主事务,此时,主事务管理器启动本地事务,按照一定规则生成一个对本次处理唯一的txId,记录主事务日志,并在事务上下文中记录txId,这个txId在整个分布式事务的生命周期中用于建立主事务与分支事务之间的对应关系,并用于业务重复性检查。
2)“业务服务”向账务前置发送账务处理请求。主事务管理器能够拦截本次请求,并将主事务ID(txId)附加到账务处理请求的上下文中,一起发送给账务前置。
3)“账务前置”进行前置约束检查。前置约束检查至少要保证:a)事务Id有效;b)业务不重复。前置约束检查前,相关账户必须锁定(除特定账户外、如中间账户等)。
4)“账务前置”调用账务核心进行账务约束检查。账务约束检查至少要保证:a)账户状态正确;b)账户资金足够;c)其他账务约束满足。账务约束检查时必须考虑到在本事务中尚未到达的资金,因此这是检查中比较特殊的地方,需要恰当处理。
5)“账务前置”调用“账务核心”进行资金冻结。对于完成本次账务处理需要的资金,需要一种特殊的方式冻结起来,但这种冻结没有业务含义,因此,不应该记录资金冻结日志,只是在freeze_amount中增加这笔冻结资金,确保账务确认阶段能够使用这笔资金。如果本次账务处理所需要的资金尚未到达,则不需要冻结。
6)“账务前置”调用“分支事务管理器”记录分支事务日志。分支事务日志中记录了本次账务处理的内容以及冻结的金额,在确认阶段,“分支事务管理器”会根据分支事务日志中记录的内容驱动账务系统完成预冻结金额的解冻与实际的账务处理。
7)“账务前置”向业务服务返回账务处理的结果。
8)“业务服务”根据账务处理的结果继续进行业务处理。
确认阶段。图6-17显示在“确认”阶段各个组件的交互过程。
1)“业务服务”请求“主事务管理器”提交事务。
2)“主事务管理器”首先完成本地事务的提交。
3)“主事务管理器”向“业务系统”返回事务提交的结果。
4)“主事务管理器”向“分支事务管理器”确认分支事务结果。
5)“分支事务管理器”顺序处理对应于本次分布式事务的每一条分支事务日志,对每一条分支事务日志,调用账务前置确认该次处理。
6)“账务前置”首先请求账务核心解冻预冻结的资金。
7)“账务前置”请求账务核心进行账务处理。
8)“账务核心”对本次账务处理进行约束检查。对于特定的检查(比如账户状态是否有效等)是否需要做,视业务而定。
图6-17 XTS组件在确认阶段的交互流程
9)“账务核心”进行账务处理,包含记录账务日志并更新账户余额等。其他正常账务处理中需要执行的工作也同样需要做。
10)“账务核心”向账务前置返回账务处理的结果。
11)“账务前置”向“分支事务管理器”返回账务确认的结果,分支事务管理器提交本地事务。
12)“分支事务管理器”请求主事务管理器勾对主事务。勾对的方式可以是删除主事务记录,也可以是为主事务记录打上标志。
回滚阶段。图6-18是回滚阶段的步骤。
1)“业务服务”请求“主事务管理器”回滚事务。
2)“主事务管理器”回滚本地事务。
3)“主事务管理器”向业务系统返回回滚结果。
4)“主事务管理器”向“分支事务管理器”请求取消分支事务。
5)“分支事务管理器”针对每一条分支事务明细,向“账务前置”请求取消账务处理。
图6-18 XTS组件在回滚阶段的交互流程
6)“账务前置”向“账务核心”请求解冻预冻结资金。
7)“分支事务管理器”清除分支事务日志。
总结。本质上支付宝的XTS给开发人员提供了一个实现分布式事务的事务框架,主要负责事务日志的记录,事务的参与者需要实现XTS提供的接口,以实现XTS框架对事务参与者的事务协调和控制。通过Try实现业务的软隔离,避免了耗时的真正数据锁,从而在整体上相比于传统的分布式事务有更好的性能和处理吞吐率。但总体来说,因为需要开发人员实现事务的补偿机制,对于开发人员的心智负担过于沉重。所以只能依赖TCC服务器的失败重试机制,如果失败重试机制不能处理,只能人肉去处理(建议对重试次数需要限定,因为同时进行失败重试和人肉的话,如果失败重试和人肉操作都在操作同一条数据,还需要考虑这种竞争的场景)。
为了真正将开发人员从自己实现事务补偿的重心智的负担中解脱出来,才有了接下来给大家介绍的阿里巴巴新一代分布式事务平台TXC。
6.阿里巴巴AliWare TXC事务服务
产品研发背景。因为阿里巴巴使用的TDDL或DRDS分布式数据层平台均不支持分布式事务,而在整个阿里巴巴的服务化体系中,分布式事务几乎成为每个业务实现的必然需求。那不管是淘宝平台广泛使用的基于消息分布式事务还是支付宝体系中的XTS分布式事务框架,都要求开发人员在业务出现异常时,自行实现事务的补偿或回滚,这对于开发人员的心智确实很有挑战。在现实中的多个项目中均出现了开发人员因为对事务回滚的逻辑考虑不够全面,导致了比较严重的数据不一致问题,也给后期的人工处理带来了更多的复杂度。
在通过对业界现有分布式事务平台的深入研究,围绕着开发人员使用这些平台的各种优点和弊端,研发设计出了新一代分布式事务平台TXC,其功能如下:
·TXC完全能满足之前分布式事务平台所提供的对于事务服务高可用和事务最终一致性的基本业务要求。
·标准模式下无需开发人员自行进行事务回滚或补偿的代码,平台支持自动按事务中事务操作的顺序依次回滚和补偿。
·易用性是TXC的主要目标,在保证事务完整性的前提下,标准模式可不修改应用的代码,同时也提供之前平台中所提供的事务重试以及自定义事务模式。
TXC架构概览。TXC同样也是基于两阶段提交的理论实现的分布式事务框架,全面支持分布式数据库事务、多库事务、消息事务、服务链路调用事务及其各种组合场景下的事务,架构图如图6-19所示。
图6-19 TXC架构示意图
架构图中,Client是与Server进行交互的客户端,其中某一类客户端称为事务发起者,事务的创建提交必须由事务发起者发起,在代码层界定事务边界,即整个事务上下文中对于各事务操作按业务需求进行组装,另一类客户端则是在事务中调用的服务提供者。在出现业务异常时,会由TXC的客户端发起事务的回滚。
TXC Server扮演了事务协调者的角色,负责对整个事务上下文的日志的记录以及在事务处理过程中全局的协调和控制。具体包含对事务以及分支事务的注册,对事务的提交和回滚进行统一的管控。同时平台对异常情况进行捕捉并发起对应的事务回滚或提交操作,参见表6-1。
表6-1 异常的捕捉

架构图中的RM(Resource Manager)为资源管理器,一般管理多个TXC数据源,负责在TXC客户端进行数据源访问时,与TXC服务器进行事务的注册和状态更新。TXC数据源是在原有的数据源基础上做了一层较薄的封装,因为TXC需要拦截和捕捉到所有TXC客户端对于数据库进行的数据修改,从而为事务的自动回滚提供数据的原始值。
相比于传统的两阶段提交方式,最大的区别在于XA在准备阶段是没有提交本地事务的,而TXC则是立即执行并可见,在隔离性级别上实现的是读未提交(read uncommitted),所以避免了在分布式事务中对于数据的长时间锁占用。也就是说,TXC在允许数据脏读的业务场景中,能充分发挥性能上的优势。比如商品在大促秒杀场景下,允许商品的库存在事务没有提交前给前端应用提供查询,只不过在最后订单扣减库存时进行控制,避免商品超卖现场的发生。如果业务场景不允许数据的脏读,TXC平台也支持select for update以及提供@hint的功能临时提升事务的隔离级别。
同时,TXC服务器端会记录当前处理事务对数据库中进行了修改数据的信息(行信息),当有其他事务也要对这些数据进行修改操作时,TXC服务端会协调两个事务间的执行,避免在第一个事务没有提交前,同样的数据会被另一个事务对该数据进行修改。从本质来说,将原来传统事务场景下,由数据库提供的锁机制提升到了TXC服务端进行了实现,这样相比于数据库锁的实现成本更加轻量,加上TXC本身服务能力的扩展能力,最终在同样实现事务隔离性的前提下,大大提升了整体的数据库处理吞吐率。
TXC两阶段提交实现。TXC也是基于两阶段提交理论实现,由TXC服务器负责整体的事务协调和管理,由部署在TXC客户端上的资源管理器组件实现各客户端与TXC服务器的事务注册、状态更新、提交等操作,一个标准的两阶段提交事务处理时序图如图6-20所示。
图6-20 TXC实现两阶段事务的流程示意图
1)在图6-20中,实现了两个事务通过TXC实现整体事务性的流程,第一个事务是对某个数据库的数据修改操作;第二个事务是调用远程服务的RPC调用,在该RPC服务中也会实现对另外一个数据库进行数据的修改。
2)步骤1)是事务发起者在执行这两个事务前,首先会在TXC服务器上对该事务进行注册。
3)步骤2)是事务发起者首先发送一条SQL请求进行数据修改操作,该SQL请求被TXC感知到后,会向TXC服务器对该分支事务进行注册(步骤3))。
4)步骤4)则是对在实际的数据库上执行SQL操作,同时进行undo和Redo日志的生成,在执行了这些一系列的数据库本地事务操作并提交后,会向TXC服务器更新该分支事务的状态(步骤5)。
5)步骤6是从事务发起者在完成了第一个数据库本地事务的操作后,进行远程的RPC调用,同样,在RPC服务调用过程中,一旦出现对数据库修改的操作,则会再次注册该分支事务到TXC服务器上(步骤7)。
6)在步骤8中完成了如步骤4同样的数据库本地事务操作并提交后,更新该分支事务状态到TXC服务器上。
7)最后当两个事务都完成并没有异常出现的情况下,事务发起者会发起整体事务更新的请求,此时会依次对之前的两个分支事务进行状态的提交;当在整个事务过程中出现代码异常或网络异常时,则会依次对两个事务中进行的数据修改进行自动的回滚操作,将数据恢复到该事务执行前的状态,从而实现了整个事务操作的业务一致性。
TXC如何实现事务自动回滚。TXC平台提供了对事务的自动回滚,使得开发人员不会像前两种方案中要实现业务的正向补偿或回滚,大大降低了开发人员对业务逻辑深度理解和额外开发方面的要求。这也是TXC相比于今天业界其他分布式事务框架中最大的一个特点,接下来通过TXC实现事务自动回滚的流程介绍一下这一关键功能的实现原理,如图6-21所示。
1)用户在向TXC服务器发起事务请求后,进入到数据库的操作时,会对该分支事务在TXC服务器上进行注册。当资源管理器捕捉到SQL的请求后,会对SQL语句进行SQL解析,如果是执行Insert/Delete/Update的SQL操作,则会针对该SQL语句构造出对应的SQL查询语句,将当前SQL请求要修改的数据先从数据库中获取,以Undo日志的方式保存起来,用于将来回滚。
2)进行实际的SQL语句的执行,在SQL执行完毕以后,会再次通过查询方式获取到修改后的数据,并保存为Redo日志,用于业务回滚前脏数据的校验。
图6-21 TXC实现事务回滚的工作原理图
3)当SQL的执行和Undo/Redo日志作为一个本地事务提交给数据库的同时,也会更新分支事务状态。当整个事务成功提交后,则会删除Undo/Redo日志。
当出现事务回滚时,会按以下顺序进行数据的恢复和操作。首先对比当前数据库中数据值与之前保存的Redo日志中被修改的值是否一致,如果一致则根据Undo日志生成回滚用的Undo SQL并执行,恢复数据到执行事务前的状态;如果当前数据库中的数据与Redo日志中的值不一致,则说明是该分布式事务在第一阶段修改了数据后,又被其他线程(可能是通过非TXC事务控制的数据访问渠道)修改了该数据,这样就不能再继续进行数据的自动回滚,否则会出现业务不一致的情况,回滚会抛出异常,由TXC Server发出告警,引入人工干预。
总结。目前TXC平台已经在菜鸟网络、1688、村淘、阿里影业等业务场景中全面上线,同时该产品已经在多个外部客户的生产环境中成功上线。以下是在一个金融客户互联网金融场景中对于TXC平台的稳定性和性能的测试数据一览,让大家对于TXC的特性有更加准确的认识。
·性能测试。在不带事务运行的情况下,测试场景包含了两个查询+一个update+一个insert,数据库qps峰值为23000。采用TXC实现分布式事务后,整体的QPS峰值接近20000,吞吐率仅下降13%左右,这个数值相比传统XA分布式事务方案有巨大优势,通常传统分布式事务总体会下降一个量级,也就是从23000降到两三千左右。
·稳定性测试。在100并发测试下,测试平台运行10个小时,完成了接近1亿次分布式事务,全部成功,没有业务异常。
·数据一致性测试。金融客户对数据一致性的要求非常之高,需要保证异常情况下、数据冲突下数据的严格一致,测试结果符合用户预期。
因为TXC是近两年阿里巴巴中间件团队针对分布式事务场景研发出的新一代产品,在本书中不便做更多详细的介绍,相信在不久的将来会有关于TXC更多的文献资料,甚至专门的书籍对该平台做更加详细的介绍。
7.关于柔性事务的总结
从电商领域到电信、金融领域,今天在我们面对的众多业务平台建设时,发现其中绝大部分场景下,我们都不需要用两阶段提交这样低效的方式来解决分布式事务问题。上面描述的几个最终一致性方案,都很好地在保证业务一致性的前提下,展现出极高的系统吞吐能力。为了充分发挥柔性事务框架性能的优势并实现业务的最终一致,需要采纳以下配合方案:
·应用程序一定要做幂等实现,特别是对数据库进行数据修改操作时。
·远程模块之间用异步消息来驱动,异步消息还可以起到检查点的作用。
两阶段提交的方案可以保证最强的ACID要求,开发者因此不需要仔细考虑自己的应用到底可以接受什么级别的ACID;同时,两阶段提交的方案开发简单,开发者只需要指定事务的边界即可。而最终一致性方案往往意味着更高的事务处理性能及处理吞吐率,但有些实现方案需要开发人员更全面地了解前端业务以实现事务的正向补偿或反向回滚,也会付出有损事务隔离性的代价。所以一定要在业务上精确分析自己的ACID需求,寻找性能与ACID的折中点,采取最合适的方案。
