事务的原子性:
- 事务,可理解为包含一系列操作的序列
- 原子,代表不可分割的最小粒度
事务的原子性就是让包含若干操作的事务表现得像一个最小粒度操作。这操作一旦被执行,只有“成功”或“失败”两种结果。好比特(bit),只能代表0或1,没其它选择。
为啥要让事务表现出原子性?
1 没错,又是ATM取款案例
走到ATM前,从自己50,000元的账户上取1,000元现金。当你输入密码和取款金额后, ATM会吐出1,000块钱,同时你的账户余额会扣减1,000元;虽然有时,ATM故障,无法吐钞,系统会提示取款失败,但你的余额会保持在50,000元。
总之,要么既吐钞又扣减余额,要么既不吐钞又不扣减余额,你拿到手的现金和账户余额总计始终是50,000元,这就是一个具有原子性的事务。
吐钞和扣减余额是两个不同操作,且分别作用在ATM和银行的存款系统。当事务整合了两个独立节点上的操作时,称为分布式事务,其达成的原子性也就是分布式事务的原子性。
对事务的原子性,图灵奖得主、事务处理大师詹姆斯·格雷(Jim Gray)的更权威定义:
Atomicity:Either all the changes from the transaction occur (writes, and messages sent), or none occur.
原子性就是要求事务只有两个状态:
- 成功,所有操作全部成功
- 失败,任何操作都没有被执行,即使过程中已经执行了部分操作,也要保证回滚这些操作
多数情况下事务是由多个操作构成的序列。而分布式事务原子性的外在表现与事务原子性一致,但前者涉及多个物理节点,且增加了网络这个不确定性因素,使问题更复杂。
2 实现事务原子性的两种协议
如何协调内部的多项操作,对外表现出统一的成功或失败状态?需要一系列算法或协议保证。
原子性提交协议按作用范围分为:
- 面向应用层
比较典型的协议,TCC协议。 - 面向资源层
2.1 面向应用层的TCC(Try-Confirm-Cancel)
事务过程中的三个操作。
① 适用场景
单元架构 + 单体数据库的这类方案需在应用层实现事务原子性,经常用TCC。
② TCC转账
小明和小红都是XX银行客户,小明想给小红转账2,000元,在XX银行存款系统中咋实现?
系统架构示意图:

XX银行的存款系统是单元化架构。即系统由多个单元构成,每个单元包含一个存款系统的部署实例和对应数据库,专门为某一个地区的用户服务。如单元A为北京用户服务,单元B为上海用户服务。
单元化架构:
- 好处:每个单元只包含部分用户,运行负载较小,一旦异常也只影响少部分客户,提升了整个存款系统可靠性
- 局限性:虽然单元内的客户转账容易,但跨单元的转账需引入额外处理机制,TCC就是常见选择
TCC的参与角色:
- 事务管理器,只能有一个
- 事务参与者,具体的业务服务,可多个,每个服务都要提供Try、Confirm和Cancel三个操作。
③ TCC具体执行
小明的银行卡在北京的网点开户,而小红的银行卡是在上海出差时办理的,所以两人的账户分别在单元A和单元B上。现在小明的账户余额是4,900元,要给小红转账2,000元,一个正常流程是这样的。

第一阶段,事务管理器Try操作,要求进行资源的检查和预留。即:
- 单元A要检查小明账户余额&&冻结其中2,000
- 单元B要确保小红的账户合法,可接收转账
该阶段,两者账户余额始终不会发生变化。
第二阶段,因参与者都已做好准备,所以事务管理器Confirm操作,执行真正业务,完成2,000元的划转。
不幸,小红账户是无法接收转账的非法账户,处理过程变成:

第一阶段,事务管理器发出Try指令,单元B对小红账户的检查没有通过,回复No。而单元A检查小明账户余额正常,并冻结了2,000元,回复Yes。
第二阶段,因为前面有参与者回复No,所以事务管理器向所有参与者发出Cancel指令,让已经成功执行Try操作的单元A执行Cancel操作,撤销在Try阶段的操作,也就是单元A解除2,000元的资金冻结。
可见,TCC仅是应用层的分布式事务框架,具体操作完全依赖业务编码,可做针对性设计,但这业务侵入较深。
考虑到网络的不可靠,操作指令须能重复执行,就要求Try、Confirm、Cancel须幂等性操作,加大开发难度。
有其他选择吗?当然,数据库领域最常用的两阶段提交协议(Two-Phase Commit,2PC),面向资源层的典型协议。
2.2 数据库领域最常用的2PC
2PC首次提出在Jim Gray 1977年发表的“Notes on Data Base Operating Systems”,对当时数据库系统研究成果和实践进行了总结,而2PC在工程中的应用还再早几年。
2PC的处理过程也分为准备和提交两个阶段,每个阶段都由事务管理器与资源管理器共同完成:
- 事务管理器作为事务的协调者只有一个
- 资源管理器作为参与者执行具体操作允许有多个
2PC具体是如何运行的呢?我们还是说回小明转账的例子。
小明给小红转账没有成功,两人又到木瓜银行来尝试。
木瓜银行的存款系统采用了分库分表方案,系统架构大致是这样的:

在木瓜银行的存款系统中,所有客户的数据被分散存储在多个数据库实例中,这些数据库实例具有完全相同的表结构。业务逻辑部署在应用服务器上,通过数据库中间件访问底层的数据库实例。数据库中间件作为事务管理器,资源管理器就是指底层的数据库实例。
假设,小明和小红的数据分别被保存在数据库D1和D2上。
我们还是先讲正常的处理流程。

第一阶段是准备阶段,事务管理器首先向所有参与者发送待执行的SQL,并询问是否做好提交事务的准备(Prepare);参与者记录日志、分别锁定了小明和小红的账户,并做出应答,协调者接收到反馈Yes,准备阶段结束。
第二阶段是提交阶段,如果所有数据库的反馈都是Yes,则事务管理器会发出提交(Commit)指令。这些数据库接受指令后,会进行本地操作,正式提交更新余额,给小明的账户扣减2,000元,给小红的账户增加2,000元,然后向协调者返回Yes,事务结束。
那如果小明的账户出了问题,导致转账失败,处理过程会是怎样呢?

第一阶段,事务管理器向所有数据库发送待执行的SQL,并询问是否做好提交事务的准备。
由于小明之前在木瓜银行购买了基金定投产品,按照约定,每月银行会自动扣款购买基金,刚好这个自动扣款操作正在执行,先一步锁定了账户。数据库D1发现无法锁定小明的账户,只能向事务管理器返回失败。
第二阶段,因为事务管理器发现数据库D1不具备执行事务的条件,只能向所有数据库发出“回滚”(Rollback)指令。所有数据库接收到指令后撤销第一阶段的操作,释放资源,并向协调者返回Yes,事务结束。小明和小红的账户余额均保持不变。
2PC的三大问题
学完了TCC和2PC的流程,我们来对比下这两个协议。
相比于TCC,2PC的优点是借助了数据库的提交和回滚操作,不侵入业务逻辑。但是,它也存在一些明显的问题:
- 同步阻塞
执行过程中,数据库要锁定对应的数据行。如果其他事务刚好也要操作这些数据行,那它们就只能等待。其实同步阻塞只是设计方式,真正的问题在于这种设计会导致分布式事务出现高延迟和性能的显著下降。
- 单点故障
事务管理器非常重要,一旦发生故障,数据库会一直阻塞下去。尤其是在第二阶段发生故障的话,所有数据库还都处于锁定事务资源的状态中,从而无法继续完成事务操作。
- 数据不一致
在第二阶段,当事务管理器向参与者发送Commit请求之后,发生了局部网络异常,导致只有部分数据库接收到请求,但是其他数据库未接到请求所以无法提交事务,整个系统就会出现数据不一致性的现象。比如,小明的余额已经能够扣减,但是小红的余额没有增加,这样就不符合原子性的要求了。
你可能会问:这些问题非常致命呀,2PC到底还能不能用?
所以,网上很多文章会建议你避免使用2PC,替换为 TCC或者其他事务框架。
但我要告诉你的是,别轻易放弃,2PC都提出40多年了,学者和工程师们也没闲着,已经有很多对2PC的改进都在不同程度上解决了上述问题。
事实上,多数分布式数据库都是在2PC协议基础上改进,来保证分布式事务的原子性。这里我挑选了两个有代表性的2PC改进模型和你展开介绍,它们分别来自分布式数据库的两大阵营,NewSQL和PGXC。
3 分布式数据库的两个2PC改进模型
3.1 NewSQL阵营:Percolator
Percolator来自Google论文“Large-scale Incremental Processing Using Distributed Transactions and Notifications”,基于分布式存储系统BigTable建立的模型,所以可和NewSQL无缝链接。
Percolator模型同时涉及隔离性和原子性的处理。
使用Percolator模型的前提是事务的参与者,即数据库,要支持MVCC。主流单体数据库和分布式数据库都支持MVCC。
转账事务开始前,小明和小红的账户分别存储在分片P1和P2。

分片的账户表中各有两条记录,第一行记录的指针(write)指向第二行记录,实际的账户余额存储在第二行记录的Bal. data字段中。
Bal.data分为两个部分,冒号前面的是时间戳,代表记录的先后次序;后面的是真正的账户余额。我们可以看到,现在小明的账户上有4,900元,小红的账户上有300元。
Percolator的流程

准备阶段
事务管理器向分片发Prepare请求,包含具体的数据操作要求。
分片接到请求后要做:
- 写日志
- 添加私有版本
在lock字段写入了标识信息的记录就是私有版本,只有当前事务能够操作,通常其他事务不能读写这条记录。
两个分片上的lock内容并不一样。
主锁的选择是随机的,参与事务的记录都可能拥有主锁,但一个事务只能有一条记录拥有主锁,其他参与事务的记录在lock字段记录了指针信息“primary@Ming.bal”,指向主锁记录。
准备阶段结束的时候,两个分片都增加了私有版本记录,余额正好是转账顺利执行后的数字。

提交阶段
事务管理器只需要和拥有主锁的分片通讯,发送Commit指令,且不用附带其他信息。
分片P1增加了一条新记录时间戳为8,指向时间戳为7的记录,后者在准备阶段写入的主锁也被抹去。这时候7、8两条记录不再是私有版本,所有事务都可以看到小明的余额变为2,700元,事务结束。
为啥在提交阶段不更新小红的记录?
Percolator最有趣就这里,因为分片P2的最后一条记录,保存了指向主锁的指针。其他事务读取到Hong7这条记录时,会根据指针去查找Ming.bal,发现记录已经提交,所以小红的记录虽然是私有版本格式,但仍然可视为已经生效了。
当然,这种通过指针查找的方式,会给读操作增加额外的工作。如果每个事务都照做,性能损耗就太大了。所以,还会有其他异步线程来更新小红的余额记录,最终变成下面的样子。

现在,让我们对比2PC的问题,来看看Percolator模型有哪些改进。
- 数据不一致
2PC的一致性问题主要缘自第二阶段,不能确保事务管理器与多个参与者的通讯始终正常。
但在Percolator的第二阶段,事务管理器只需要与一个分片通讯,这个Commit操作本身就是原子的。所以,事务的状态自然也是原子的,一致性问题被完美解决了。
- 单点故障
Percolator通过日志和异步线程的方式弱化了这个问题。
一是,Percolator引入的异步线程可以在事务管理器宕机后,回滚各个分片上的事务,提供了善后手段,不会让分片上被占用的资源无法释放。
二是,事务管理器可以用记录日志的方式使自身无状态化,日志通过共识算法同时保存在系统的多个节点上。这样,事务管理器宕机后,可以在其他节点启动新的事务管理器,基于日志恢复事务操作。
Percolator模型在分布式数据库的工程实践中被广泛借鉴。比如,分布式数据库TiDB,完全按照该模型实现了事务处理;CockroachDB也从Percolator模型获得灵感,设计了自己的2PC协议。
CockroachDB的变化在于没有随机选择主锁,而是引入了一张全局事务表,所有分片记录的指针指向了这个事务表中对应的事务记录。单就原子性处理来说,这种设计似乎差异不大,但在相关设计上会更有优势,具体是什么优势呢,下一讲我来揭晓答案。
3.2 PGXC阵营:GoldenDB的一阶段提交
那么,分布式数据库的另一大阵营,PGXC,又如何解决2PC的问题呢?
GoldenDB展现了另外一种改良思路,称之为“一阶段提交”。
GoldenDB遵循PGXC架构,包含了四种角色:协调节点、数据节点、全局事务器和管理节点,其中协调节点和数据节点均有多个。GoldenDB的数据节点由MySQL担任,后者是独立的单体数据库。

虽然名字叫“一阶段提交”,但GoldenDB的流程依然可以分为两个阶段。

第一阶段,GoldenDB的协调节点接到事务后,在全局事务管理器(GTM)的全局事务列表中将事务标记成活跃的状态。这个标记过程是GoldenDB的主要改进点,实质是通过全局事务列表来申请资源,规避可能存在的事务竞争。
这样的好处是避免了与所有参与者的通讯,也减少了很多无效的资源锁定动作。

第二阶段,协调节点把一个全局事务分拆成若干子事务,分配给对应的MySQL去执行。如果所有操作成功,协调者节点会将全局事务列表中的事务标记为结束,整个事务处理完成。如果失败,子事务在单机上自动回滚,而后反馈给协调者节点,后者向所有数据节点下发回滚指令。
由于GoldenDB属于商业软件,公开披露信息有限,我们也就不再深入细节了,你只要能够理解上面我讲的两个阶段就够了。
GoldenDB的“一阶段提交”,本质上是改变了资源的申请方式,更准确的说法是,并发控制手段从锁调度变为时间戳排序(Timestamp Ordering)。这样,在正常情况下协调节点与数据节点只通讯一次,降低了网络不确定性的影响,数据库的整体性能有明显提升。因为第一阶段不涉及数据节点的操作,也就弱化了数据一致性和单点故障的问题。
4 总结
- 事务的原子性就是让包含若干操作的事务表现得像一个最小粒度的操作,而这个操作一旦被执行只有两种结果,成功或者失败。
- 相比于单机事务,分布式事务原子性的复杂之处在于增加了多物理设备和网络的不确定性,需要通过一定的算法和协议来实现。这类协议也有不少,我重点介绍了TCC和2PC这两个常用协议。
- TCC提供了一个应用层的分布式事务框架,它对参与者没有特定要求,但有较强的业务侵入;2PC是专为数据库这样的资源层设计的,不侵入业务,也是今天分布式数据库主流产品的选择。
- 考虑到2PC的重要性和人们对其实用价值的误解,我又展开说明2PC的两种改良模型,分别是Percolator和GoldenDB的“一阶段提交”。Percolator将2PC第二阶段工作简化到极致,减少了与参与者的通讯,完美解决了一致性问题,同时通过日志和异步线程弱化了单点故障问题。GoldenDB则改良了2PC第一阶段的资源协调过程,将协调者与多个参与者的交互转换为协调者与全局事务管理器的交互,同样达到了减少通讯的效果,弱化了一致性和单点故障的问题。
为啥不学三阶段提交协议(Three-Phase Commit,3PC)呢?
3PC虽然试图解决2PC问题,但通讯开销更大,网络分区时也无法很好工作,少在工程实践使用,所以没介绍,知道有这协议就行。
2PC V.S 两阶段封锁协议(Two-Phase Locking,2PL)。
从整个分布式事务看,原子性协议之外还有一层隔离性协议,由后者保证事务能够成功申请到资源。在相当长的一段时间里,2PC与2PL的搭配都是一种主流实现方式,可能让人误以为它们是可以替换的术语。实际上,两者是截然不同的,2PC是原子性协议,而2PL是一种事务的隔离性协议,也是一种并发控制算法。
两种改良模型都一定程度上化解了2PC的单点故障和数据一致性问题,但同步阻塞导致的性能问题还没有根本改善,而这也是2PC最被诟病的地方,可能也是很多人放弃分布数据库的理由。
2PC注定就是延时较长、性能差吗?分布式数据库中的分布式事务,延时一定很长吗?
不少优秀分布式数据库产品已经大幅缩短了2PC的延时,无论是理论模型还是工程实践都已经验证。

参考
- Daniel Peng and Frank Dabek: Large-scale Incremental Processing Using Distributed Transactions and Notifications
- Jim Gray: Notes on Data Base Operating Systems
5 FAQ
今天内容主要围绕着2PC展开,而它的第一阶段“准备阶段”也被称为“投票阶段”,“投票”这个词是不是让你想到Paxos协议呢?
2PC和Paxos协议有没有关系?
TCC 第二阶段
- 如果向单元 A 发出 confirm 操作成功且收到成功应答,但向单元 B 发出 confirm 操作失败,这时是否需要通过其它手段来回滚(或者补偿) A 的变更呢?
- 如果向单元 A 发出 confirm 操作成功且收到成功应答,向单元 B 发出 confirm 操作成功,但没有收到成功应答,是否应该先确认 B 的状态,然后再决定是否需要回滚(或者补偿)A 的变更呢?
对于上面2中情况,通常是怎么做的呢?通过分析日志发现异常然后处理吗?
问题1,如果单元A成功,单元B失败,则面临一个选择。如果以努力成功为目标,则可以再次向单元B发送指令,有可能成功,这也是为什么要求操作具有幂等性。当然也可以直接撤销A操作,是A与B一样都是失败状态,这种就是失败回滚。 问题2,没有收到应答,其实也和问题1类似,重试争取成功,或者直接回滚。但这里面有个问题是,如果无论发送任何指令,单元B始终也没有返回,怎么办。这个时候基本上就是要人工干预了。当然,对于这种网络通讯故障也不是没有解决办法,有种协议叫做Paxos Commmit,可以极大缓解这个问题,我会在第15讲答疑篇详细介绍,你可以关注下。
老师不是很理解为什么TCC就不用像2PC那样加锁和记日志了呢?TCC如何保证事务隔离性呢?如果有其他代码修改同一行数据怎么办?
TCC可使用的手段更灵活,不限于数据库锁了。比如,可以增加一个“未冻结余额”字段,初始值和余额一样,一阶段时直接在这个字段上扣减金额,这样后发生的事务如果发现剩下的“未冻结余额”不够,就会返回失败,这样多事务就可以协同了。
感觉这里可以对Percolator的设计思想再阐述得更详细一些:
- 为什么需要MVCC来实现percolator?或者说为什么要保存多个版本的key才能实现percolator?
- 为什么要有个write指向实际数据存储的行?而不是直接存储对应数据?
第一个问题,如果只实现“用主锁来控制事务的原子性,从锁保留指向主锁的指针”这个设计,那确实不一定要用MVCC。Percolator是一个工业实现,它要解决自己的业务需求,除了原子性还有隔离性要处理,使用了MVCC还可以解决读写冲突嘛。 第二个问题,我觉得是设计细节,采用其他形式应该也是可以的。
这个思考题我延伸到Raft思考一下: Raft里面基于Leader的复制是否就是一种2PC呢?有写请求时,Leader先发给大多数节点,成功再写入。但是又好像不完全一样。面试的时候面试官也说这不是2PC。
paxos是共识算法,是对同一份数据的达成共识。2pc更多是为了达成的多份不同数据修改的原子性。不知道这样理解对不?
是的,赞。还有延伸内容,同样建议关注第15讲。
个人的理解是paxos可以用于解决2PC的单点故障和数据不一致问题,协调者和参与者利用paxos实现多副本一致,在节点宕机后可切换到副本节点继续完成2PC流程。 今天讲的内容,Percolator很好理解,很好的解决了传统2PC存在的问题。 但是对于PGXC的解决方案还是不明白,几个问题请教一下老师,或者请老师帮忙推荐一下相关学习资料。 1、PGXC中分布式事务的实现是不是也是基于单体数据库的XA事务来实现的?像MYSQL的XA要避免脏读是需要工作在可串行级别下的,若是使用XA如何解决XA的性能问题呢? 2、关于利用GTM实现资源分配这点不是很理解,是指由GTM负责从SQL中解析出事务要读写的数据,然后判断读写冲突吗? 3、在MYSQL中事务在提交前是不会写binlog的,是不是意味着MYSQL实现分布式数据库就没办法利用binlog进行主备同步了,因为若2PC的提交阶段,某个节点宕机后不恢复,没办法利用备库继续执行。 4、PGXC类型的分布式数据库,是不是需要实现在并发执行多个事务时,保证所有节点按照相同的顺序执行SQL?这个问题看起来也很复杂,没想明白。
paxos提供可靠性,2pc提供原子性,所以3pc没优势?
tcc和goden的方式隔离性有问题吧? 都可能出现读中间状态的情况
理论上是不会的。GoldenDB是通过全局事务列表统一控制,数据节点(也就是单体数据库)基于日志实现回滚。TCC只要三个操作都正确实现,理论上也不会出现中间状态。
自己认为2pc更像是一种思想,多个节点对某件事达成共识,其实paxos完成的也是类似的行为,就单个操作在多副本上达成一致或共识,因此借鉴了2pc思想
GlodenDB如何避免全局管理器成为瓶颈呢?
从公开资料看,也没有太特别的设计,就是把这个进程独立部署,这样不会与其他任务竞争资源。
感觉2PC和Basic-Paxos的过程好像啊,第一阶段的区别是2PC需要全部的回复,而Paxos只需要一半以上的Acceptor回复;第二阶段就几乎一模一样了。至于为什么第一阶段有这样的区别,大概是2PC的每一个节点职能(包括数据)都不相同,要满足一致性约束必须全部的节点的同意;而Paxos抛去每个节点角色不同,它们存储的数据都一样(理想中一致,实际不一致,Paxos会出现日志空缺),为了全局一致,一次同意一半以上就可以了,因为两次一半以上一定是有交集的,这保证了paxos需要的一致性。
至于它们的关系,我觉得它们都是共识算法(consensus),适用前提区别在于节点职能是否相同。
我觉得2PC还不是共识算法,因为参与者并不是对一件事情(一个状态)投票,而是要让多个独立的状态要保持一致,且参与者并不了解这种一致性。可以参照15讲的Paxos Commit协议再体会下。
关于Percolator,文中提到“在 lock 字段上写入了标识信息的记录就是私有版本,只有当前事务能够操作”,而在例子中又有其他事务读到了私有版本的数据,这是为什么呢?
如果加了secondary锁,其他事务确实会读取到,是为了追溯事务状态,再决定读取哪个版本。而在异步线程或第一个读操作维护好状态后,就不再是私有版本了,所以我们说“通常其他事务不能读写”
某业务涉及两个系统,业务要求两个系统须都成功或都失败,通过报文交互,这个场景比较适合TCC。
我的理解是2PC中的任何一个节点都是逻辑的,可以有多个物理节点组成,PAXOS可以让这个逻辑节点内部的值达成一致,就是各种一致性模型。
而2PC是解决分布式的事物,参与的各个节点的值是不一样的,比如小明节点的余额是2900,而小红节点的余额是2300
2PC解决的: 1)小明账户 - 100; 2)小红账户 + 100; 3)小明和小红账户信息存储在不同的数据库实例中;
Basic Paxos解决的: 1)客户端不停的有 a=xxx 这样的操作 2)Basic Paxos 就是让多个节点就 x 的值达成一致 3)说白了就是数据在多副本之间的复制
二者也可联系起来看。
















