Distributed transaction, avoid the use of XA

Table of Contents

1 Avoid XA (Introducing BASE)

XA 规范中的两次提交协议(Two-Phase Commit protocol)处理分布式事务的最大问题其性能问题,系统的并发度很高时无法胜任。

Eric Brewer于1998年提出了 CAP公理(CAP theorem) :分布式系统的三项重要指标——一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)—— 在任意时刻,只有两项能同时成立。

分区容错性是指系统的部分故障不能影响系统的其它功能。对于高流量的网站来说,我们必须选择分区容错性,因为它是实现可伸缩的根本。对于24x7运行的网站,选择可用性也是理所当然的。于是只好放弃一致性(consistency),这里的放弃一致性是指放弃“实时的一致性”,系统可能在某个时间点处于不一致的状态,但我们会想办法让系统(在某个时间内,如5s或1天)最终达到一致。

下面链接(本节内容基本摘自于它)是Ebay的架构师在2008年发表给ACM的文章,是一篇解释BASE原则,或者说最终一致性的经典文章:
Base: An Acid Alternative
Base: An Acid Alternative (中文版)

注:XA选择了CAP中的C(一致性)和P(分区容错性),而放弃了A(可用性)。

1.1 BASE(Basically available, Sofe state, Eventually consistent)

BASE是Basically available, Sofe state, Eventually consistent的编写,这个缩写多少有些拼凑的感觉。可能是作者认为它的含义与ACID恰好相反。在英文中,Acid代表酸,而Base代表碱。就像这两个单词在化学中的含义一样——ACID与BASE位于CAP理论的两端,代表了分布式系统的两种选择。

下面用例子来说明 BASE方案中是如何不用分布式事务(二阶段提交,2PC),而最终也能实现一致性的。
假设一个买卖系统中“买家”和“卖家”之间产生了一笔交易。系统的数据库如图 1 所示。

transaction_base_ex1.jpg

Figure 1: 数据库例子

产生交易后,我们需要更新transaction表和user表,看起来像图 2 所示。

transaction_base_ex1_fig3.jpg

Figure 2: 分布式事务解决办法(往往是“性能瓶颈”)

为了保证分区容错性,user表和transaction表往往不在一个主机上。所以,上面的事务是一个分布式事务。为了使系统提供更好的可用性,我们应该避免分布式事务。比如把上面过程拆为两个本地事务(一个事务仅访问一个主机上的数据表),如图 3 所示。

transaction_base_ex1_fig4.jpg

Figure 3: 拆为两个本地事务,一致性无法保障

把分布式事务拆为两个本地事务后,一致性无法得到保障(因为在这两个事务之间如果系统出现故障,那么系统就不一致了)。

怎么办?我们可以引入一个“消息队列”,如图 4 所示。

transaction_base_ex1_fig5.jpg

Figure 4: 引入“消息队列”,但第二个事务中还是有2PC问题

为了避免第一个事务成为分布式事务,“消息队列”和transaction表应该在同一个主机上,但这时“消息队列”没有和user表在同一个主机上,从而第二个事务就成为了分布式事务。

一种可能的解决办法是:什么都不做,就让第二个事务是分布式事务,但把它放到后端去处理。 通过把对user表的更新操作(第二个事务)解耦到一个单独的后端组件,我们可以保留面向客户的组件的可用性。对业务需求来说,消息处理器较低的可用性可能是可接受。

其实,我们还可以让第二个事务从分布式事务变为本地事务。请看下文。

1.1.1 使用“消息队列”和“消息应用状态表”去除分布式事务

在前面的介绍中,引入了“消息队列”,使得一个分布式事务拆为两个事务:一个本地事务和另一个分布式事务(这个分布式事务可以做为一个后端组件,而不影响面向客户的组件的可用性)。

为了避免前面提到的“第二个事务”成为分布式事务,我们在user表所在主机中增加另一个表——“消息应用状态表updates_applied”,如图 5 所示。

transaction_base_ex1_fig6.jpg

Figure 5: “消息应用状态表”(已经处理过的消息会加到这个表中)

有了上面的表后,我们可以把第二个事务中对消息队列的操作移到事务外部(即不在“Begin transaction”和“End transaction”中间)来做(这样第二个事务就不是分布式事务了),如图 6 所示。

transaction_base_ex1_fig7.jpg

Figure 6: 最终方案——用“消息应用状态表”

说明1:在上面的实现中,第二个事务开始之前只是“Peek message”,当第二个事务成功后才“Remove message from queue”。
说明2:在第二个事务结束后,“Remove message from queue”之前如果系统出故障,系统重新从消息队列中取出这一消息后,通过updates_applied表可以检查出来这一消息已经被应用过(此时代码中条件“If processed == 0”不会满足),所以并不会再次应用这个消息。
说明3:显然,如果消息已经被从消息队列中删除,那么就可以把它从“消息应用状态表”中也删除(当然不删除也没有关系)。
说明4: 增加“消息应用状态表”的目的是保证第二个事务是“幂等操作”(即:重复调用多次产生的业务结果与调用一次产生的业务结果相同。这很有用,因为在部分失败时,会再调用它,而反复地调用它不会改变系统的最终状态)。

至此,我们没有使用分布式事务,实现了系统的“最终一致性”(系统在某个时间点上可能是不一致的,但最终会达到一个一致的状态)。

总结:
采用BASE方案(增加了“消息队列”和“消息应用状态表”),解除了两个数据库之间的紧密耦合,系统性能和可伸缩性大大增强,但使得应用程序的开发变得相对复杂 (采用分布式事务时,应用程序的逻辑是非常简单的,如 2 所示)。

2 TCC (Try-Cancel/Confirm)模式

TCC是另外一种不采用分布式事务的解决方案。TCC模式需要每个业务服务实现自己的Try/Cancel/Confirm三个接口,如图 7 所示。其中Cancel/Confirm操作要满足“幂等性”(即重复调用多次产生的业务结果与调用一次产生的业务结果相同),如在前面介绍的BASE方案中用“消息应用状态表”实现了“幂等性”。TCC的工作流程如图 8 所示。

transaction_tcc1.jpg

Figure 7: TCC模式中每个业务需要自己实现三个接口

transaction_tcc2.jpg

Figure 8: TCC模式工作流程

摘自:大规模SOA系统中的分布事务处事,支付宝首席架构师程立


Author: cig01

Created: <2013-07-21 Sun 00:00>

Last updated: <2017-12-13 Wed 22:09>

Creator: Emacs 25.3.1 (Org mode 9.1.4)