分布式事务面面观

〇、当我们谈论事务的时候,我们在谈论什么

提起事务,大部分人想到的是ACID,事务隔离级别(read uncommitted、read committed、repeatable read、serializable),事务传播策略(required、support、mandatory、requires_new)等,然后想到2PC(2-phase-commitment)、CAP(Consistency-Availability-Partition)、BASE(Basically Available,Soft state,Eventually consistent)等名词。但这一切往往掺杂在一起,实际上头脑并不见得清晰。这里,我们首先对事务分一下类,然后看看各种理论都针对哪些类型的事务。

第一类为本地事务,它是其它一切事务的基础。比如你在一个传统数据库中更新一条记录,如果autocommit=true,那么这条更新就是一个本地事务。如果把autocommit设成false,然后执行几条更新命令,最后手动commit,那么这几个动作也构成一个本地事务。

第二类为分布式事务。这一类又大致分为两种。

第一种为分布式系统的提交协议。如redis、zookeeper等分布式系统,每次更新都会涉及到复制副本,那么,复制成功还是失败就关系到用户的这次更新成功还是失败,因此需要一个提交协议。比如简单多数原则:多数副本复制成功,就成功返回,还没复制完成的那些让它们慢慢复制去。zookeeper的原子广播就是简单多数协议。再比如最小委员会原则:成立一个最少人数(服务器数)委员会,只要它们复制成功就返回,剩下的慢慢复制去(这种原则对水平伸缩有利,缺点是其它副本可能落后委员会副本较多)。

因此,这种类型的分布式事务是“分布式系统(复制型)”自己的实现机制。

第二种分布式事务为支持业务流程的全局事务。典型的假想例子是跨行转账:从A银行的数据库取出100元钱,存入B银行的数据库;这两个动作要么都完成,要么一个也不能完成,就是说要保证原子性。这是传统中间件如Tuxedo、WebLogic/WebSphere等支持的XA/2PC强一致性事务。它不关心自己是否作用于分布式系统,它只关心多个系统协调一件工作,这件工作要么在各系统中都成功完成,要么都如没有发生过一般。

这两种事务的思想本质是相同的,但对一致性的要求不同,侧重点也不同,一个侧重于分布式系统自身的实现,另一个侧重于对其它业务的支撑。我们为了叙述方便把它们分开。

分布式事务,无论是第一种还是第二种,基本都依赖本地事务。因为说到底,任何更新最终都落实到在某台机器上写数据,要么写内存,要么写磁盘,经常是都要写;这都要落实到存储引擎。下面分别讨论上述三种事务(重点在后两种分布式事务),以及其它事务机制。

一、本地事务

本地事务的典型例子是一个单例关系型数据库。通常来讲,本地事务要保证ACID。其实现往往依赖于操作系统和硬件提供的一些锁机制,以及自身开发的一些锁机制。ACID的概念适用于一切事务。

经典数据库一般都支持隔离级别。但它不必考虑传播策略,因为发生在本地。CAP、BASE等词汇都是描述分布式系统的,与它无关。

一个本地事务也不涉及2PC——虽然数据库本身为了支持XA协议会实现2PC机制。但业界也有一些搞笑的事情,比如MySQL Server有bin log,InnoDB又有redo log,当这两种日志都打开时,写日志本身成了一个问题,不得不用2PC协议;也就是说,要实现一个本地事务,却要用全局事务的机制去做。

二、复制事务(分布式系统的提交协议)

我们经常谈的CAP理论和“最终一致性”主要就是指这种事务。

为了简单起见,我们用一个2实例的Oracle RAC数据库做例子。Oracle RAC是active-active的分布式关系数据库,实例间有专门的网络进行数据实时同步。假设2个实例一个在北京一个在上海。

C是指一致性,指的是用户操作过程中看到的数据具有一致性,即实例间要同步更新。

A是指高可用性,即多个实例都是可用的。通常分布式系统的实例之间都有failover的能力。

P是指的是容忍断网,即对两个实例间网络不通的耐受性。(经常被翻译成“分区容错性”)

如果你要保证CA,即,两个实例都可用(A),且看到的数据是一致的(C),那么就无法接受断网。因为北京与上海断了网的话数据就不能实时同步,不能实时同步就无一致性可言。

如果保证CP,即用户看到的数据要一致(C),且可以容忍断网(P),那么就保证不了高可用性,此时只能用其中的一个实例。如果用两个的话,断了网,还是无法实时同步,还是做不到一致。

如果保证AP,即用户既要求两台机器可用(A),又允许北京与上海之间断网(P),那么就要忍受因断网产生的数据不一致。

什么是“最终一致性”?就是说,在这样的分布式系统中,虽然有时候断网,有时候实例失败,而且复制也需要时间,但是只要其中一部分实例成功的话,那么其它实例都有机会“赶上来”,最终达成一致。如zookeeper的follower们慢慢catch up它们的leader。

上面的例子中,我们只用了两个实例,现实中实例可能更多,比如北京有三个实例,上海有两个,而且分布在不同地理位置,这样就把风险分摊了,各实例之间都断网的几率更小,CAP更有保证。由于断网或实例失败导致的failover对用户是透明的,用户的业务可以不间断,而系统最终也能一致。

事实上,副本复制的提交协议可以使用2PC或3PC进行强一致同步,如中国和印度铁路系统使用的GemFire。但更多的系统选择最终一致同步,如zookeeper原子广播或Paxos(Raft)。zookeeper在自身实现上用弱一致性,但在节点失败需要failover用户session时,却总是选择那些时间线不落后于失败节点的节点,给用户一致性的视图。这说明它在面向自身和面向用户时选择了不同策略,在弱一致性与强一致性之间转圜。

三、业务事务(传统中间件支持的全局事务)

传统中间件支持的全局事务是建立在2PC和XA协议上的,具有强一致性。如Tuxedo的ATMI和JavaEE的JTA协议。

从技术上看,业务事务处于复制事务的下层;从应用上看,业务事务处于复制事务的上层。

从技术上看,2PC和XA本身并不对使用者进行假设,它们只是“协调多个资源进行同一件工作”的协议,复制也是一种“业务”。

从应用上看,基于2PC和XA的全局事务管理器(transaction manager)常被用来完成类似于银行转账的业务。这类业务对一致性具有极强的要求。当然,真实的转账业务远比这个例子复杂,我们只是拿它代指此类业务。

这类事务不关注分布式系统的副本一致性,因而CAP和最终一致性理论不适用于转账类事务。硬要说的话,转账事务是绝对服从于C的,而且它不涉及P,P关心的是分布式系统节点之间通信断开,JTA事务中间件作为独立coordinator,不是参与方(resource)的一员,也不需要resource之间互相通信。因此它对P的支持度是很高的,它牺牲的是A,而A可以通过coordinator集群化来解决。

事务的传播策略主要是针对这种事务说的。比如本文一开始说的几种传播策略都表现在EJB或Spring中。隔离级别也可以在这种事务中指定,默认使用Resource Manager(如DB)的当前隔离级别;但如果在组件上(Bean的方法)指定隔离级别,则会在session(connection)上设置指定的级别——前提是Resource Manager支持你指定的级别。

四、基于补偿的事务

互联网时代到来以后,由于微服务的大量采用,传统的JTA事务受到了挑战。以前,一个单体(monolithic)应用可以访问好几个后端数据库,而现在由于服务分开、数据库分开,每个服务的权责变小了,一项工作要两个或多个微服务(应用)协作才能完成。而且在访问量巨大的情况下,如果用2PC/XA保证强一致性,效率也太低。微服务、松耦合的普及,不仅带来了RPC/Message风暴,也带来了系统设计难题,全局事务管理就是其中之一。本节中我们将讨论两种试图解决此问题的事务机制。显然,这里所说的事务,是指业务事务。

常见的基于补偿的协议有TCC(Try-Cancel/Confirm)和Saga。

TCC出自Guy Pardon发表于2011年的论文《Towards Distributed Atomic Transactions over RESTful Services》,他是Atomikos的CTO。他考虑在REST风格的WS服务上进行无侵入的事务支持。TCC的commit与rollback不依赖于既有的中间件和DB的实现,而是立足于APP本身。它把一个操作分为两个阶段,先try,把数据更新到一个中间态,再confirm做实际修改,如果出了问题要回滚,则执行cancel动作。这意味着业务开发人员要为一个业务操作定义三个动作(方法),而try类似于锁实现。

此前也有其他人提出REST的事务,通常要扩展HTTP协议,以实现某种机制的事务上下文传播(transaction context propagation)。但Guy不想扩展HTTP协议,他不要transaction context,各个resource manager(改叫TCC resource)甚至不知道有事务在进行。没有事务上下文的传播,意味着xid不能被各个TCC resource感知,即这些resource不记录transaction状态。因此它的coordinator在confirm前除了要记录所有的branch信息外可能还需要记录各个branch的before image和after image,以便coordinator失败重启后执行recover(还面临ABA问题)。TCC理论上能保证ACID,但实现起来几乎要考虑所有2PC/XA所考虑的问题。

Saga协议则是旧瓶装新酒。一个事务(saga)由多个“子事务”组成,一般每个服务是一个“子事务”,后一个由前一个完成后触发,第一个由客户触发。回滚就是自定义undo动作。这是一个简单直接的基于补偿的模型。(其实“子事务”不是一个好词,它破坏“事务”这个词的基本语义,因为事务是原子的、不可再分的,故而一般我们宁可说“事务分支”。)

最初Saga提出时(1987年)可能是基于串行系统的,它明确说不能保证隔离性。即Saga只有ACID中的ACD。这样的系统在Recover时无法回滚,只能疯狂地向前重试(否则就需要将所有的分支用一个全局锁保护起来;由于是串行,这个锁显然比2PC的还重)。

五、BASE与“面向道歉的计算”

TCC,Saga都是基于补偿的,都将补偿逻辑提到了业务层,而业务层缺乏锁机制,缺乏统一的undo/redo,要自行解决。对此有一些常用策略,如业务层可以设置所谓的“语义锁”:pending状态的订单、冻结状态的账号等。还有应对复杂网络问题的各种理论,如正向动作与反向动作的可交换性(commutative),使它们互为补偿,以解决反向动作先于正向动作到达的情况;比如要能够取消一个不存在的订单,当正向任务后到达时必须保证确实能被取消,事实上这很难做。这些措施显然也使业务逻辑与事务逻辑混在一起了。

今天,试图在微服务世界实现全局事务的人,面临复杂局面:undo/redo上移到服务层,业务代码和事务代码混在一起,搞不清自己是在写业务还是在写中间件;想要实现分布式锁,但锁的粒度无法控制,加锁解锁都是网络通信,失败概率高;微服务数量多、系统压力大与分布式锁的特点构成巨大矛盾等等。这一切都限制了新事务基础件的出现和推广。有的人转向Message Broker等不那么严格的替代品。

前面我们讲了CAP是面向分布式系统事务的理论,并举了双机热备的例子说明。但这个理论提出时用的是减库存的例子:亚马逊的系统是分布式的,一本书在全世界贩售,如果每卖出一本都要锁全部相关节点以使所有人都看到一致、正确的库存变化,那么系统就太慢了,何况还有一些人锁库存(放入购物车)但最后不会买。

BASE提出时也是用这个例子,它觉得CAP很有道理,对大规模的分布式系统确实难以要求那么强的一致性,可以放松一下。因此认为BASE可以作为ACID的一种替代。这种基本可用、软状态的库存逻辑带来的问题就是超售,因此有人戏称为“面向道歉的计算”。这里,BASE的“最终一致性”和我们上面说的最终一致性就有区别了,它是指通过人工的方式解决不一致。可见,当最终一致性发生在分布式基础件自身领域时,它问题不大;当发生在业务领域时,就要面向道歉计算一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值