You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
23 KiB
Markdown

2 years ago
# 09原子性2PC还是原子性协议的王者吗
你好我是王磊你也可以叫我Ivan。今天我要和你讲一讲分布式事务的原子性。
在限定“分布式”范围之前,我们先认识一下“事务的原子性”是啥。
如果分开来看的话,事务可以理解为包含一系列操作的序列,原子则代表不可分割的最小粒度。
而合起来看的话事务的原子性就是让包含若干操作的事务表现得像一个最小粒度的操作。这个操作一旦被执行只有“成功”或者“失败”这两种结果。这就好像比特bit只能代表0或者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.
这句话说得很精炼,我再和你解释下。
原子性就是要求事务只有两个状态:
* 一是成功,也就是所有操作全部成功;
* 二是失败,任何操作都没有被执行,即使过程中已经执行了部分操作,也要保证回滚这些操作。
要做到事务原子性并不容易,因为多数情况下事务是由多个操作构成的序列。而分布式事务原子性的外在表现与事务原子性一致,但前者要涉及多个物理节点,而且增加了网络这个不确定性因素,使得问题更加复杂。
## 实现事务原子性的两种协议
那么,如何协调内部的多项操作,对外表现出统一的成功或失败状态呢?这需要一系列的算法或协议来保证。
### 面向应用层的TCC
原子性提交协议有不少按照其作用范围可以分为面向应用层和面向资源层。我想先给你介绍一种“面向应用层”中比较典型的协议TCC协议。
TCC是Try、Confirm和Cancel三个单词的缩写它们是事务过程中的三个操作。关于TCC的适用场景嘛还记得我在[第1讲](https://time.geekbang.org/column/article/271373)中介绍的“单元架构 + 单体数据库”吗? 这类方案需要在应用层实现事务的原子性经常会用到TCC协议。
下面我用一个转账的例子向你解释TCC处理流程。
小明和小红都是番茄银行的客户现在小明打算给小红转账2,000元这件事在番茄银行存款系统中是如何实现的呢
我们先来看下系统的架构示意图:
![9.1](https://static001.geekbang.org/resource/image/2a/5c/2a34990e1c95645f3942cd7d358f4c5c.jpg?wh=2700*1724)
显然番茄银行的存款系统是单元化架构的。也就是说系统由多个单元构成每个单元包含了一个存款系统的部署实例和对应的数据库专门为某一个地区的用户服务。比如单元A为北京用户服务单元B为上海用户服务。
单元化架构的好处是每个单元只包含了部分用户,这样运行负载比较小,而且一旦出现问题,也只影响到少部分客户,可以提升整个存款系统的可靠性。
不过这种架构也有局限性。那就是虽然单元内的客户转账非常容易但是跨单元的转账需要引入额外的处理机制而TCC就是一种常见的选择。
TCC的整个过程由两类角色参与一类是事务管理器只能有一个另一类是事务参与者也就是具体的业务服务可以是多个每个服务都要提供Try、Confirm和Cancel三个操作。
下面是TCC的具体执行过程。
小明的银行卡在北京的网点开户而小红的银行卡是在上海出差时办理的所以两人的账户分别在单元A和单元B上。现在小明的账户余额是4,900元要给小红转账2,000元一个正常流程是这样的。
![9.2](https://static001.geekbang.org/resource/image/61/cd/613371c67df3e1910a77785320586acd.jpg?wh=2700*1510)
第一阶段事务管理器会发出Try 操作要求进行资源的检查和预留。也就是说单元A要检查小明账户余额并冻结其中的2,000元而单元B要确保小红的账户合法可以接收转账。在这个阶段两者账户余额始终不会发生变化。
第二阶段因为参与者都已经做好准备所以事务管理器会发出Confirm操作执行真正的业务完成2,000元的划转。
但是很不幸,小红账户是无法接收转账的非法账户,处理过程就变成下面的样子。
![9.3](https://static001.geekbang.org/resource/image/01/f8/01fdc99cb9ba3233yyaa6a9cbee731f8.jpg?wh=2700*1514)
第一阶段事务管理器发出Try指令单元B对小红账户的检查没有通过回复No。而单元A检查小明账户余额正常并冻结了2,000元回复Yes。
第二阶段因为前面有参与者回复No所以事务管理器向所有参与者发出Cancel指令让已经成功执行Try操作的单元A执行Cancel操作撤销在Try阶段的操作也就是单元A解除2,000元的资金冻结。
从上述流程可以发现,**TCC仅是应用层的分布式事务框架**,具体操作完全依赖于业务编码实现,可以做针对性的设计,但是这也意味着业务侵入会比较深。
此外考虑到网络的不可靠操作指令必须能够被重复执行这就要求Try、Confirm、Cancel必须是幂等性操作也就是说要确保执行多次与执行一次得到相同的结果。显然这又增加了开发难度。
那还有其他的选择吗?
当然有我们来看看数据库领域最常用的两阶段提交协议Two-Phase Commit2PC这也是面向资源层的典型协议。
### 数据库领域最常用的2PC
2PC的首次正式提出是在Jim Gray 1977年发表的一份文稿中文稿的题目是“[Notes on Data Base Operating Systems](https://cs.nyu.edu/courses/fall18/CSCI-GA.3033-002/papers/Gray1978.pdf)”对当时数据库系统研究成果和实践进行了总结而2PC在工程中的应用还要再早上几年。
2PC的处理过程也分为准备和提交两个阶段每个阶段都由事务管理器与资源管理器共同完成。其中事务管理器作为事务的协调者只有一个而资源管理器作为参与者执行具体操作允许有多个。
2PC具体是如何运行的呢我们还是说回小明转账的例子。
小明给小红转账没有成功,两人又到木瓜银行来尝试。
木瓜银行的存款系统采用了分库分表方案,系统架构大致是这样的:
![9.4](https://static001.geekbang.org/resource/image/4f/24/4f417c89459f5f2f30bf7148ea747824.jpg?wh=2700*1377)
在木瓜银行的存款系统中,所有客户的数据被分散存储在多个数据库实例中,这些数据库实例具有完全相同的表结构。业务逻辑部署在应用服务器上,通过数据库中间件访问底层的数据库实例。数据库中间件作为事务管理器,资源管理器就是指底层的数据库实例。
假设小明和小红的数据分别被保存在数据库D1和D2上。
我们还是先讲正常的处理流程。
![9.5](https://static001.geekbang.org/resource/image/c1/1f/c1e92da6dbf1d6e92628383089a8ab1f.jpg?wh=2700*1518)
第一阶段是准备阶段事务管理器首先向所有参与者发送待执行的SQL并询问是否做好提交事务的准备Prepare参与者记录日志、分别锁定了小明和小红的账户并做出应答协调者接收到反馈Yes准备阶段结束。
第二阶段是提交阶段如果所有数据库的反馈都是Yes则事务管理器会发出提交Commit指令。这些数据库接受指令后会进行本地操作正式提交更新余额给小明的账户扣减2,000元给小红的账户增加2,000元然后向协调者返回Yes事务结束。
那如果小明的账户出了问题,导致转账失败,处理过程会是怎样呢?
![9.6](https://static001.geekbang.org/resource/image/8c/c8/8c1cd6763c88b1fbf9be14402f3bfbc8.jpg?wh=2700*1494)
第一阶段事务管理器向所有数据库发送待执行的SQL并询问是否做好提交事务的准备。
由于小明之前在木瓜银行购买了基金定投产品按照约定每月银行会自动扣款购买基金刚好这个自动扣款操作正在执行先一步锁定了账户。数据库D1发现无法锁定小明的账户只能向事务管理器返回失败。
第二阶段因为事务管理器发现数据库D1不具备执行事务的条件只能向所有数据库发出“回滚”Rollback指令。所有数据库接收到指令后撤销第一阶段的操作释放资源并向协调者返回Yes事务结束。小明和小红的账户余额均保持不变。
### 2PC的三大问题
学完了TCC和2PC的流程我们来对比下这两个协议。
相比于TCC2PC的优点是借助了数据库的提交和回滚操作不侵入业务逻辑。但是它也存在一些明显的问题
1. **同步阻塞**
执行过程中,数据库要锁定对应的数据行。如果其他事务刚好也要操作这些数据行,那它们就只能等待。其实同步阻塞只是设计方式,真正的问题在于这种设计会导致分布式事务出现高延迟和性能的显著下降。
2. **单点故障**
事务管理器非常重要,一旦发生故障,数据库会一直阻塞下去。尤其是在第二阶段发生故障的话,所有数据库还都处于锁定事务资源的状态中,从而无法继续完成事务操作。
3. **数据不一致**
在第二阶段当事务管理器向参与者发送Commit请求之后发生了局部网络异常导致只有部分数据库接收到请求但是其他数据库未接到请求所以无法提交事务整个系统就会出现数据不一致性的现象。比如小明的余额已经能够扣减但是小红的余额没有增加这样就不符合原子性的要求了。
你可能会问:**这些问题非常致命呀2PC到底还能不能用**
所以网上很多文章会建议你避免使用2PC替换为 TCC或者其他事务框架。
但我要告诉你的是别轻易放弃2PC都提出40多年了学者和工程师们也没闲着已经有很多对2PC的改进都在不同程度上解决了上述问题。
事实上多数分布式数据库都是在2PC协议基础上改进来保证分布式事务的原子性。这里我挑选了两个有代表性的2PC改进模型和你展开介绍它们分别来自分布式数据库的两大阵营NewSQL和PGXC。
## 分布式数据库的两个2PC改进模型
### NewSQL阵营Percolator
首先我们要学习的是NewSQL阵营的Percolator。
Percolator来自Google的论文“[Large-scale Incremental Processing Using Distributed Transactions and Notifications](https://www.cs.princeton.edu/courses/archive/fall10/cos597B/papers/percolator-osdi10.pdf)”因为它是基于分布式存储系统BigTable建立的模型所以可以和NewSQL无缝链接。
Percolator模型同时涉及了隔离性和原子性的处理。今天我们主要关注原子性的部分在讲并发控制时我再展开隔离性的部分。
使用Percolator模型的前提是事务的参与者即数据库要**支持多版本并发控制MVCC**。不过你不用担心现在主流的单体数据库和分布式数据库都是支持的MVCC。
在转账事务开始前小明和小红的账户分别存储在分片P1和P2上。如果你不了解分片的含义可以回到[第6讲](https://time.geekbang.org/column/article/275696)学习。当然,你也可以先用单体数据库来替换分片的概念,这并不会妨碍对流程的理解。
![9.7](https://static001.geekbang.org/resource/image/55/67/55141bef63a89718517cda63512af967.jpg?wh=2700*1256)
上图中的Ming代表小明Hong代表小红。在分片的账户表中各有两条记录第一行记录的指针write指向第二行记录实际的账户余额存储在第二行记录的Bal. data字段中。
Bal.data分为两个部分冒号前面的是时间戳代表记录的先后次序后面的是真正的账户余额。我们可以看到现在小明的账户上有4,900元小红的账户上有300元。
我们来看下Percolator的流程。
![9.8](https://static001.geekbang.org/resource/image/e6/25/e610bcb9d4fa5b53cf9e7f293b4da425.jpg?wh=2700*1441)
**第一,准备阶段**事务管理器向分片发送Prepare请求包含了具体的数据操作要求。
分片接到请求后要做两件事写日志和添加私有版本。关于私有版本你可以简单理解为在lock字段上写入了标识信息的记录就是私有版本只有当前事务能够操作通常其他事务不能读写这条记录。
你可能注意到了两个分片上的lock内容并不一样。
主锁的选择是随机的参与事务的记录都可能拥有主锁但一个事务只能有一条记录拥有主锁其他参与事务的记录在lock字段记录了指针信息“primary@Ming.bal”指向主锁记录。
准备阶段结束的时候,两个分片都增加了私有版本记录,余额正好是转账顺利执行后的数字。
![9.9](https://static001.geekbang.org/resource/image/f2/60/f2a39536e65c8e0f4c282a0e05274160.jpg?wh=2700*1522)
**第二,提交阶段**事务管理器只需要和拥有主锁的分片通讯发送Commit指令且不用附带其他信息。
分片P1增加了一条新记录时间戳为8指向时间戳为7的记录后者在准备阶段写入的主锁也被抹去。这时候7、8两条记录不再是私有版本所有事务都可以看到小明的余额变为2,700元事务结束。
你或许要问,为什么在提交阶段不用更新小红的记录?
Percolator最有趣的设计就是这里因为分片P2的最后一条记录保存了指向主锁的指针。其他事务读取到Hong7这条记录时会根据指针去查找Ming.bal发现记录已经提交所以小红的记录虽然是私有版本格式但仍然可视为已经生效了。
当然,这种通过指针查找的方式,会给读操作增加额外的工作。如果每个事务都照做,性能损耗就太大了。所以,还会有其他异步线程来更新小红的余额记录,最终变成下面的样子。
![9.10](https://static001.geekbang.org/resource/image/c8/d2/c8dc734cd33a8149eeb1ffb2f435e6d2.jpg?wh=2700*1516)
现在让我们对比2PC的问题来看看Percolator模型有哪些改进。
1. **数据不一致**
2PC的一致性问题主要缘自第二阶段不能确保事务管理器与多个参与者的通讯始终正常。
但在Percolator的第二阶段事务管理器只需要与一个分片通讯这个Commit操作本身就是原子的。所以事务的状态自然也是原子的一致性问题被完美解决了。
2. **单点故障**
Percolator通过日志和异步线程的方式弱化了这个问题。
一是Percolator引入的异步线程可以在事务管理器宕机后回滚各个分片上的事务提供了善后手段不会让分片上被占用的资源无法释放。
二是,事务管理器可以用记录日志的方式使自身无状态化,日志通过共识算法同时保存在系统的多个节点上。这样,事务管理器宕机后,可以在其他节点启动新的事务管理器,基于日志恢复事务操作。
Percolator模型在分布式数据库的工程实践中被广泛借鉴。比如分布式数据库TiDB完全按照该模型实现了事务处理CockroachDB也从Percolator模型获得灵感设计了自己的2PC协议。
CockroachDB的变化在于没有随机选择主锁而是引入了一张全局事务表所有分片记录的指针指向了这个事务表中对应的事务记录。单就原子性处理来说这种设计似乎差异不大但在相关设计上会更有优势具体是什么优势呢下一讲我来揭晓答案。
### PGXC阵营GoldenDB的一阶段提交
那么分布式数据库的另一大阵营PGXC又如何解决2PC的问题呢
GoldenDB展现了另外一种改良思路称之为“一阶段提交”。
GoldenDB遵循PGXC架构包含了四种角色协调节点、数据节点、全局事务器和管理节点其中协调节点和数据节点均有多个。GoldenDB的数据节点由MySQL担任后者是独立的单体数据库。
![9.11](https://static001.geekbang.org/resource/image/0f/70/0fa31de65b7c81yydda0d319ebe06070.jpg?wh=2700*1122)
虽然名字叫“一阶段提交”但GoldenDB的流程依然可以分为两个阶段。
![9.12](https://static001.geekbang.org/resource/image/14/e2/142b33b069b60562f89acdb83c3346e2.jpg?wh=2700*641)
第一阶段GoldenDB的协调节点接到事务后在全局事务管理器GTM的全局事务列表中将事务标记成活跃的状态。这个标记过程是GoldenDB的主要改进点实质是通过全局事务列表来申请资源规避可能存在的事务竞争。
这样的好处是避免了与所有参与者的通讯,也减少了很多无效的资源锁定动作。
![](https://static001.geekbang.org/resource/image/f6/f3/f65854eceef3335edc3b8930879115f3.jpg?wh=2700*1202)
第二阶段协调节点把一个全局事务分拆成若干子事务分配给对应的MySQL去执行。如果所有操作成功协调者节点会将全局事务列表中的事务标记为结束整个事务处理完成。如果失败子事务在单机上自动回滚而后反馈给协调者节点后者向所有数据节点下发回滚指令。
**由于GoldenDB属于商业软件公开披露信息有限我们也就不再深入细节了你只要能够理解上面我讲的两个阶段就够了。**
GoldenDB的“一阶段提交”本质上是改变了资源的申请方式更准确的说法是并发控制手段从锁调度变为时间戳排序Timestamp Ordering。这样在正常情况下协调节点与数据节点只通讯一次降低了网络不确定性的影响数据库的整体性能有明显提升。因为第一阶段不涉及数据节点的操作也就弱化了数据一致性和单点故障的问题。
## 小结
好了,以上就是今天的主要内容了,我希望你能记住以下几点:
1. 事务的原子性就是让包含若干操作的事务表现得像一个最小粒度的操作,而这个操作一旦被执行只有两种结果,成功或者失败。
2. 相比于单机事务分布式事务原子性的复杂之处在于增加了多物理设备和网络的不确定性需要通过一定的算法和协议来实现。这类协议也有不少我重点介绍了TCC和2PC这两个常用协议。
3. TCC提供了一个应用层的分布式事务框架它对参与者没有特定要求但有较强的业务侵入2PC是专为数据库这样的资源层设计的不侵入业务也是今天分布式数据库主流产品的选择。
4. 考虑到2PC的重要性和人们对其实用价值的误解我又展开说明2PC的两种改良模型分别是Percolator和GoldenDB的“一阶段提交”。Percolator将2PC第二阶段工作简化到极致减少了与参与者的通讯完美解决了一致性问题同时通过日志和异步线程弱化了单点故障问题。GoldenDB则改良了2PC第一阶段的资源协调过程将协调者与多个参与者的交互转换为协调者与全局事务管理器的交互同样达到了减少通讯的效果弱化了一致性和单点故障的问题。
这节课马上就要结束了你可能要问为什么咱们没学三阶段提交协议Three-Phase Commit3PC
原因也很简单因为3PC虽然试图解决2PC的问题但它的通讯开销更大在网络分区时也无法很好地工作很少在工程实践中使用所以我就没有介绍你只要知道有这么个协议就好。
另外我还要提示一个容易与2PC协议混淆的概念也就是两阶段封锁协议Two-Phase Locking2PL
我认为这种混淆并不只是因为名字相似。从整个分布式事务看原子性协议之外还有一层隔离性协议由后者保证事务能够成功申请到资源。在相当长的一段时间里2PC与2PL的搭配都是一种主流实现方式可能让人误以为它们是可以替换的术语。实际上两者是截然不同的2PC是原子性协议而2PL是一种事务的隔离性协议也是一种并发控制算法。
在这一节中其实我们多次提到了并发控制算法但都没有展开介绍原因是这部分内容确实比较复杂没办法用三言两语说清我会在后面第13讲和第14讲中详细解释。
两种改良模型都一定程度上化解了2PC的单点故障和数据一致性问题但同步阻塞导致的性能问题还没有根本改善而这也是2PC最被诟病的地方可能也是很多人放弃分布数据库的理由。
可是2PC注定就是延时较长、性能差吗或者说分布式数据库中的分布式事务延时一定很长吗
我想告诉你的是其实不少优秀的分布式数据库产品已经大幅缩短了2PC的延时无论是理论模型还是工程实践都已经过验证。
那么,它们又有哪些精巧构思呢?我将在下一讲为你介绍这些黑科技。
![](https://static001.geekbang.org/resource/image/0a/91/0a676d16295d91870a30caa1fccd4c91.jpg?wh=2700*2715)
## 思考题
最后我给你留下一个思考题。今天内容主要围绕着2PC展开而它的第一阶段“准备阶段”也被称为“投票阶段”“投票”这个词是不是让你想到Paxos协议呢
那么你觉得2PC和Paxos协议有没有关系如果有又是什么关系呢
如果你想到了答案,又或者是触发了你对相关问题的思考,都可以在评论区和我聊聊,我会在下一讲和你一起探讨。最后,谢谢你的收听,希望这节课能带给你一些收获,欢迎你把它分享给周围的朋友,一起进步。
## 学习资料
Daniel Peng and Frank Dabek: [_Large-scale Incremental Processing Using Distributed Transactions and Notifications_](https://www.cs.princeton.edu/courses/archive/fall10/cos597B/papers/percolator-osdi10.pdf)
Jim Gray: [_Notes on Data Base Operating Systems_](https://cs.nyu.edu/courses/fall18/CSCI-GA.3033-002/papers/Gray1978.pdf)