阿里巴巴中台战略--事务与柔性事务

阿里巴巴中台战略

不管是业务流程异步化还是数据库事务异步化,都面临一个如何保证业务事务一致性的问题。面对这个问题目前还没有完美的解决方案。

关于数据库事务,核心是提现数据库**ACID(原子性,一致性,隔离性和持久性)**属性。

传统数据库的事务确实非常好地保证了业务的一致性,但在互联网场景下,比如上面的订单流程和还款场景,就暴露出数据库性能和处理能力上的瓶颈。所以在分布式领域,基于CAP理论和在其基础上延伸出的BASE理论,有人提出了“柔性事务”的概念。

CAP理论

一个分布式系统最多只能同时满足一致性(Consistency)、**可用性(Availability)分区容错性(Partition tolerance)**这三项中的两项。

  • 一致性是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。

在一个系统中不论数据存在何处,作为一个整体应是完成的和一致的。在分布式系统中,数据通常不会只有一份,用户对数据进行一定的修改操作后,为了保证数据的一致性,那么应该对所有数据进行相同的操作并且这些操作应该是同时成功或者同时失败的。

如果一个存储系统可以保证一致性,那么客户读写的数据完全可以保证是最新的。不会发生两个不同的客户端在不同的存储节点中读取到不同副本的情况。

具体来说,系统中对一个数据的读和写虽然包含多个子步骤并且会持续一段时间才能执行完,但是在调用者看来,读操作和写操作都必须是单个即时完成的操作,感知不到其他调用者对这些数据的访问。对一个写操作,如果系统返回了成功,那么之后到达的读请求都必须读到这个新的数据。如果系统返回失败,那么所有的读,无论是之后发起的,还是和写同时发起的,都不能读到这个数据。

  • 可用性指用户在访问数据时可以得到及时的响应。

可用性是关于一个系统能够持续不间断使用的问题,严格的定义是高性能可用性。这意味着一个系统从设计到实施都应该能够提供可持续的操作(如读写操作),无论是操作冲突,还是软硬件部分因为升级而导致失效。但是可用性并不意味着数据的一致性,比如读取到的数据是过期数据或脏数据,但对于用户仍有返回数据的情况下,仍然可以被认为是可用的。

同时可用性的要求包含时效性,对于大多数应用而言,超过一定响应时间的服务是没有价值的或者价值量低的。

分区容错性指分布式系统在遇到某节点或者网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。Gilbert和Lynch是这样定义分区容忍性的:除了整个网络的故障外,其他的故障(集)都不能导致整个系统无法正确响应。

一旦针对同一服务的存储系统分布到了多个节点后,整个存储系统就存在分区的可能性。分区不是简单的理解为物理存储节点的分布,而且是节点间网络通信中断或者报文丢失等。比如,两个存储节点之间联通的网络断开(无论长时间或者短暂的),就形成了分区,一旦开始将数据和逻辑分布在不同的节点上,就有形成分区的风险。假定网线被切断,分区就行程了,节点A无法和节点B通信,正如数据库的数据在进行分库分表后,就是典型的分区状态。

举例说明

为了能更直观地了解CAP定理,我们举一个简单的例子。某用户在电商平台上搜一件衣服,但是用户并没有立即购买,她可能先浏览了其他类似商品。同时,另一个用户也看上了这件衣服并且直接下单并创建了订单,而恰巧这件衣服的库存仅剩下最后一件,那么当第一个用户再决定购买这件衣服时,由于衣服的库存已经为零,理论上她是不能再购买该衣服的。假设网站的数据是以分布式系统的方式存储在多个机房或者多个数据库中,那么,一个商品的库存信息就被存储在不同地方,那必然存在数据的同步和一致性问题。如果第二个用户在进行衣服订单的创建时,该请求所访问的数据库没有得到及时更新(在该数据库中,该商品的库存本应该是零),那么第一个用户可能也为该衣服进行了成功的订单创建,就出现了商品超卖的情况。这里所讲的就是分布式系统中数据一致性问题。

在这个例子中,如何解决数据一致性的问题?一个简易的方案就是建立类似操作系统中锁的机制,要求确保所有数据节点的数据均同步之后,才能进行数据的访问操作,也就是在第二个用户对该衣服创建订单时,商品的库存修改为零,接着等到该库存信息在所有数据库上都同步后,再返回给客户告知订单创建成功。在此过程中,系统不接受对该衣服库存数据的修改操作,所以等到第一个用户希望创建订单时,因为所有数据库中该商品的库存信息都已经同步为零,所以系统会给第一个用户做出库存不足的提示,而无法进行订单的创建。

但这引入了一个新问题,就是可用性问题。由于不同数据节点间的数据同步是需要时间的,而且大量采用锁机制会给数据层带来严重的性能瓶颈,从而可能导致平台在业务繁忙时的服务瘫痪或者槽糕的用户体验。一个客户无法访问的服务对任何人都没有价值,这就是分布式系统中的可用性问题。

另一个做法就是商品的库存数据只保存一份,不做复制,这样就不会存在数量一致性问题。而因为网站数据量太大,一个数据节点无法容纳如此大容量的数据,所以把整体数据分割成若干部分,每一部分存储在不同节点上,这也就是典型的数据进行分库分表的操作,这样就能解决可用性的问题。但这样也会有个很明显的问题,假如某一时刻数据节点的网络阻塞或者切断了,那么会导致网站可能获取不到完整的数据。这就是分区容忍性的问题。

所以,三个核心需求之间无法同时得到完全的保证。

CAP之间的取舍

根据前面的介绍,CAP理论的核心是:一个分布式系统不可能同时很好地满足一致性、可用性和分区容错性这三个需求,最多只能同时较好地满足两个。

CAP定理并不意味着所有系统的设计都必须抛弃三个要素之中的一个。CAP三者可以在一定程度上衡量,并不是非黑即白的,例如可用性从0%到100%有不同等级。

BASE理论

BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用合适的方式达到最终一致性(Eventual Consitency)

BASE是指基本可用(Basically Available)、柔性状态(Soft State)、最终一致性(Eventual Consistency)

  • 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

  • 柔性状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是柔性状态的体现。MySql Replication的异步复制也是一种柔性状态的体现。

  • 最终一致性是指系统中所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

传统分布式事务

上面提到数据在按照业务领域(用户中心,交易中心)的不同被拆分到不同的数据库后,在某些业务场景(比如订单创建)下,就必然会出现同一个事务上下文中,需要协调多个资源(数据库)以保证业务的事务一致性,对于这样的场景,业界早就有基于两阶段提交方式实现的分布式事务(如下图),两阶段提交协议包含两个阶段:第一阶段(也称准备阶段)和第二阶段(也称提价阶段)。

一个描述两阶段提交很好的类比是典型的结婚仪式,每个参与者(结婚典礼中的新郎和新娘)都必须服从安排,在正式步入婚姻生活之前说“我愿意”。一旦其中一位“参与者”在做出承诺前的最后一刻反悔,则这场婚礼就演变为一个悲剧。
两阶段提交之于此的结果也成立,虽然不具备如婚礼上那么大的破坏性。
阿里巴巴中台战略--事务与柔性事务

当commit请求从客户端向事务管理器发出,事务管理器开始两阶段提交过程。在第一阶段,所有的资源被轮询到,问它们是否准备好了提交作业。每个参与者可能回答“就绪(READY)”、**只读(READ_ONLY)**或者 “未准备好”(NOT_READY)
如果有任意一个参与者在第一阶段响应“未准备好”则整个事务回滚。如果所有参与者都回答“就绪”,那这些资源就在第二阶段提交。回答“只读”的资源,则在协议的第二阶段处理中被排除掉。
两阶段提交协议要求分布式事务参与者实现一个特别的“准备”操作,无论在资源管理器(如数据库)还是在业务服务中实现该操作都存在效率与复杂性挑战。
因此,两阶段提交协议有一个重要的优化,称为“最末参与者优化”(Last Participant Optimization, LPO),允许两阶段提交协议中有一个参与者不实现“准备”操作(称为单阶段参与者)。最末参与者优化的原理如下图。
阿里巴巴中台战略--事务与柔性事务
从6-5中可见,LPO中单阶段参与者不需要实现准备操作,只需要提供标准的提交操作即可。分布式事务协调者必须等其余两阶段参与者都准备好之后,再请求单阶段参与者提交,单阶段参与者的提交结果将决定整个分布式事务的结果。本质上,LPO是将最后一个参与者的准备操作与提交/放弃操作合并成一个提交操作。
X/Open组织为基于两阶段协议的分布式事务处理系统提出了标准的系统参考模型(X/OPEN事务模型)以及不同组件间与事务协调相关的接口,使不同厂商的产品能够互操作。
阿里巴巴中台战略--事务与柔性事务
分布式事务协调者在第一阶段通过对所有的分布式事务参与者请求“预备”操作,达成关于分布式事务一致性的共识。
分布式事务参与者在预备阶段必须完成所有的约束检查,并且确保后续提交或放弃时所需要的数据已持久化。
在第二阶段,分布式事务协调者根据之前达到的提交或者放弃的共识,请求所有的分布式事务参与者完成相应的操作。
很显然,在提交事务的过程中需要在多个资源节点之间协调,而各节点对锁资源的释放必须等到事务最终提交时,这样,比起一阶段提交,两阶段提交在执行同样的事务时会消耗更多的时间。
阿里巴巴中台战略--事务与柔性事务
事务执行时间的延长意味着锁资源发生冲突的概率增加,当事务的并发量达到一定数量的时候,就会出现大量事务积压甚至出现死锁。系统性能和处理吞吐率就会严重下滑,也就是系统处理的吞吐率和资源上的时间消耗成反比。这就是为什么今天在互联网应用场景中很少有人会选择传统的分布式事务方式,而选择柔性事务业务事务的主要原因。

柔性事务如何解决分布式事务问题

  1. 引入日志和补偿机制

类似传统数据库,柔性事务的原子性主要由日志保证。事务日志记录事务的开始、结束状态,可能还包括事务参与者信息。参与者节点也需要根据重做或者回滚需求记录REDO/UNDO日志。当事务重试、回滚时,可以根据这些日志最终将数据恢复到一致状态。

为避免单点,事务日志是记录在分布式节点上的,数据REDO/UNDO日志一般记录在业务数据库上,可以保证日志与业务操作同时成功/失败。通常柔性事务能通过日志记录找回事务的当前执行状态,并根据状态决定重试异常步骤(正向补偿),还是回滚前序步骤(反向补偿)。

  1. 可靠信息传递

在分布式环境下,由于“网络通信危险期”的存在,节点间的消息传递也会有“成功”、“失败”、“不知道成功还是失败”三种状态。这也给进行分布式事务处理时提供了更多的考虑点和要求。可靠消息投递就是为了解决这类问题产生的服务平台。

根据“不知道成功还是失败”状态的处理,消息投递只有两种模式:1)消息仅投递一次,但是可能会没有收到;2)消息至少投递一次,但可能会投递多次。在业务一致性的高优先级下,第一种投递方式肯定是无法接受的,因此只能选择第二种投递方式。
由于消息可能会重复投递,这就要求消息处理程序必须实现幂等(同一操作反复执行多次结果不变)。
每种业务场景不同,实现幂等的方法也会有所不同,最简单的幂等实现方式是根据业务流水号写日志,阿里内部一般把这种日志叫做排重表。

  1. 实现无锁

现在大家都知道造成数据库性能和吞吐率瓶颈往往是因为强事务带来的资源锁。如何很好地解决数据库锁问题是实现高性能的关键所在。所以选择放弃锁是一种解决问题的思路,但是放弃锁并不意味着放弃隔离性,如果隔离性没有保障,则必然带来大量的数据脏读、幻读等问题,最终导致业务不可控地不一致。
实现事务隔离的方法有很多,在实际的业务场景中可灵活选择以下几种典型的实现方式:

  • 避免事务进入回滚。如果事务在出现异常时,可以不回滚也能满足事务的要求,也就是要求业务不管出现任何情况,只能继续朝事务处理流程的顺像继续处理,这样中间状态即使对外可见,由于事务不会回滚,也不会导致脏读。
  • 辅助业务变化明细表。比如对资金或商品库存进行增减处理时,可采用记录这些增减变化的明细表的方式,避免所有事务均对同一数据表进行更新操作,造成数据访问热点,同时使得不同事务中处理的数据互不干扰,实现对资金或库存信息处理的隔离。
    比如在用户进行订单操作时,需要对商品的库存进行扣减,如果是在秒杀和大促场景下,大量订单都是对同一商品进行下单操作,如果所有订单创建事务中都是修改商品表中商品数据的库存信息,则必然会出现该条商品记录访问热点,而且很容易出现锁抢占的情况,避免锁的方式就是在订单创建事务中只是在“库存预减明细表”中添加一条对应商品的库存预减记录。而无需对原商品数据表进行库存修改的操作,一旦用户成功付款,则真正的将商品数据表中的库存减除。在付款之前当应用要获取该商品的库存信息时,则是通过以下公式获得:
    阿里巴巴中台战略--事务与柔性事务
  • 乐观锁
    乐观锁大多是基于数据版本(version)记录机制实现。例如通过在商品表中增加记录版本号的字段,在事务开始前获取到该商品记录的版本号,在事务处理最后对该商品数据进行数据更新时,可通过在执行最后的修改update语句时进行之前获取版本号的比对,如果版本号一致,则update更新数据成功,修改到新的版本号;如果版本号不一致,则表示数据已经被其他事务修改了,则重试或者放弃当前事务。
    阿里巴巴中台战略--事务与柔性事务
    淘宝消息分布事务
    在淘宝平台中,被广泛用来解决分布式事务场景的方案就是消息分布式事务,通过MQ事务消息功能特性达到分布式事务的最终一致。
    实现原理及流程。图6-10是基于MQ提供的事务消息功能实现分别对两个数据库进行事务处理流程示意。
    阿里巴巴中台战略--事务与柔性事务
    阿里巴巴中台战略--事务与柔性事务
    阿里巴巴中台战略--事务与柔性事务