二阶段提交协议的由来
X/Open 组织提出了分布式事务处理的规范 DTP 模型 (Distributed Transaction Processing),该模型中主要定义了三个基本组件,分别是
应用程序 (Application Program,简称 AP):用于定义事务边界 (即定义事务的开始和结束),并且在事务边界内对资源进行操作。
资源管理器 (Resource Manager,简称 RM):如数据库、文件系统等,并提供访问资源的方式。
事务管理器 (Transaction Manager,简称 TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。
一般,我们称 TM 为事务的协调者,而称 RM 为事务的参与者。TM 与 RM 之间的通信接口,则由 XA 规范来约定。
在 DTP 模型的基础上,才引出了二阶段提交协议来处理分布式事务。
二阶段提交基本算法
前提
二阶段提交协议能够正确运转,需要具备以下前提条件:
存在一个协调者,与多个参与者,且协调者与参与者之间可以进行网络通信
参与者节点采用预写式日志,日志保存在可靠的存储设备上,即使参与者损坏,不会导致日志数据的消失
参与者节点不会永久性损坏,即使后仍然可以恢复
实际上,条件 2 和 3 所要求的,现今绝大多数关系型数据库都能满足。
基本算法
第一阶段
协调者节点向所有参与者节点询问是否可以执行提交操作,并开始等待各参与者节点的响应。
参与者节点执行询问发起为止的所有事务操作,并将 Undo 信息和 Redo 信息写入日志。
各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个 ” 同意 ” 消息;如果参与者节点的事务操作实际执行失败,则它返回一个 ” 中止 ” 消息。
第二阶段
当协调者节点从所有参与者节点获得的相应消息都为 ” 同意 ” 时:
协调者节点向所有参与者节点发出 ” 正式提交 ” 的请求。
参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
参与者节点向协调者节点发送 ” 完成 ” 消息。
协调者节点收到所有参与者节点反馈的 ” 完成 ” 消息后,完成事务。
如下图所示:
如果任一参与者节点在第一阶段返回的响应消息为 ” 终止 ”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
协调者节点向所有参与者节点发出 ” 回滚操作 ” 的请求。
参与者节点利用之前写入的 Undo 信息执行回滚,并释放在整个事务期间内占用的资源。
参与者节点向协调者节点发送 ” 回滚完成 ” 消息。
协调者节点收到所有参与者节点反馈的 ” 回滚完成 ” 消息后,取消事务。
如下图所示:
缺陷分析
二阶段提交协议除了协议本身具有的局限性之外,如果我们把以下情况也考虑在内:
协调者宕机
参与者宕机
网络闪断(脑裂)
那么二阶段提交协议实际上是存在很多问题的
协议本身的缺陷
协议本身的缺陷是指,在协议正常运行的情况下,无论全局事务最终是被提交还是被回滚,依然存在的问题,而暂不考虑参与者或者协调者宕机,或者脑裂的情况。
性能问题
参与者的本地事务开启后,直到它接收到协调者的 commit 或 rollback 命令后,它才会提交或回滚本地事务,并且释放由于事务的存在而锁定的资源。不幸的是,一个参与者收到协调者的 commit 或者 rollback 的前提是:协调者收到了所有参与者在一阶段的回复。如果说,协调者一阶段询问多个参与者采用的是顺序询问的方式,那么一个参与者最快也要等到协调者询问完所有其它的参与者后才会被通知提交或回滚,在协调者未询问完成之前,这个参与者将保持占用相关的事务资源。即使,协调者一阶段询问多个参与者采用的是并发询问的方式,那么一个参与者等待收到协调者的提交或者回滚通知的时间,将取决于在一阶段过程中,响应协调者最慢的那个参与者的响应时间。无论是哪一种情况,参与者都将在整个一阶段持续的时间里,占用住相关的资源,参与者事务的处理时间增加。若此时在参与者身上有其它事务正在进行,那么其它事务有可能因为与这个延迟的事务有冲突,而被阻塞,这些被阻塞的事务,进而会引起其它事务的阻塞。总而言之,整体事务的平均耗时增加了,整体事务的吞吐量也降低了。这会使得整个应用系统的延迟变高,吞吐量降低,可扩展性降低 (当参与者变多的时候,延迟可能更严重)。总的来说,二阶段提交协议,不是一个高效的协议,会带来性能上的损失。
全局事务隔离性的问题
全局事务的隔离性与单机事务的隔离性是不同的。当我们在单机事务中提到不允许脏读时,那么意味着在事务未提交之前,它对数据造成的影响不应该对其它事务可见。当我们在全局事务中提到不允许脏读时,意味着,在全局事务未提交之前,它对数据造成的影响不应该对其它事务可见。在二阶段提交协议中,当在第二阶段所有的参与者都成功执行 commit 或者 rollback 之后,全局事务才算结束。但第二阶段存在这样的中间状态:即部分参与者已执行 commit 或者 rollback,而其它参与者还未执行 commit 或者 rollback。此刻,已经执行 commit 或者 rollback 的参与者,它对它本地数据的影响,对其它全局事务是可见的,即存在脏读的风险。对于这种情况,二阶段协议并没有任何机制来保证全局事务的隔离性,无法做到“读已提交”这样的隔离级别。
协调者宕机
如果在第一阶段,协调者发生了宕机,那么因为所有参与者无法再接收到协调者第二阶段的 commit 或者 rollback 命令,所以他们会阻塞下去,本地事务无法结束,如果协调者在第二阶段发生了宕机,那么可能存在部分参与者接收到了 commit/rollback 命令,而部分没有,因此这部分没有接收到命令的参与者也会一直阻塞下去。协调者宕机属于单点问题,可以通过另选一个协调者的方式来解决,但这只能保证后续的全局事务正常运行。而因为之前协调者宕机而造成的参与者阻塞则无法避免。如果这个新选择的协调者也宕机了,那么一样会带来阻塞的问题。
参与者宕机
如果在第一阶段,某个参与者发生了宕机,那么会导致协调者一直等待这个参与者的响应,进而导致其它参与者也进入阻塞状态,全局事务无法结束。如果在第二阶段,协调者发起 commit 操作时,某个参与者发生了宕机,那么全局事务已经执行了 commit 的参与者的数据已经落盘,而宕机的参与者可能还没落盘,当参与者恢复过来的时候,就会产生全局数据不一致的问题。
网络问题 - 脑裂
当网络闪断发生在第一阶段时,可能会有部分参与者进入阻塞状态,全局事务无法结束。当发生在第二阶段时,可能发生部分参与者执行了 commit 而部分参与者未执行 commit,从而导致全局数据不一致的问题。
三阶段提交
在二阶段提交中,当协调者宕机的时候,无论是在第一阶段还是在第二阶段发生宕机,参与者都会因为等待协调者的命令而进入阻塞状态,从而导致全局事务无法继续进行。因此,如果在参与者中引入超时机制,即,当指定时间过去之后,参与者自行提交或者回滚。但是,参与者应该进行提交还是回滚呢?悲观的做法是,统一都回滚。但事情往往没那么简单。当第一阶段,协调者宕机时,那么所有被阻塞的参与者选择超时后回滚事务是最明智的做法,因为还未进入第二阶段,所以参与者都不会接收到提交或者回滚的请求,当前这个事务是无法继续进行提交的,因为参与者不知道其它参与者的执行情况,所以统一回滚,结束分布式事务。在二阶段提交协议中的第二阶段,当协调者宕机后,由于参与者无法知道协调者在宕机前给其他参与者发了什么命令,进入了第二阶段,全局事务要么提交要么回滚,参与者如果引入超时机制,那么它应该在超时之后提交还是回滚呢,似乎怎么样都不是正确的做法。执行回滚,太保守,执行提交,太激进。如果在二阶段提交协议中,在第一阶段和第二阶段中间再引入一个阶段,如果全局事务度过了中间这个阶段,那么在第三阶段,参与者就可以认为此刻进行提交的成功率会更大。但这难道不是治标不治本吗,当进入第三阶段,全局事务需要进行回滚时候,如果协调者宕机,那么参与者超时之后自行进行提交事务,就会造成全局事务的数据不一致。再考虑参与者宕机的情况下,协调者应该在超时之后,对全局事务进行回滚。总结起来,三阶段提交主要在二阶段提交的基础上,为了解决参与者和协调者宕机的问题,而引入了超时机制,并因为超时机制,附带引入中间这一层。并且,三阶段提交并没有解决二阶段提交的存在的脑裂的问题。总而言之,二阶段和三阶段提交都无法完美地解决分布式事务的问题。关于三阶段提交更详细的算法和步骤,可以参考我的另外一篇文章《分布式事务概览》
fescar 二阶段提交
fescar 是阿里最近开源的一个关于分布式事务的处理组件,它的商业版是阿里云上的 GTS。在其官方 wiki 上,我们可以看到,它对 XA 二阶段提交思考与改进。在我们上面提到的参与者中,这个参与者往往是数据库本身,在 DTP 模型中,往往称之为 RM,即资源管理器。fescar 的二阶段提交模型,也是在 DTP 模型的基础上构建。
RM 逻辑不与数据库绑定
fescar 2PC 与 XA 2PC 的第一个不同是,fescar 把 RM 这一层的逻辑放在了 SDK 层面,而传统的 XA 2PC,RM 的逻辑其实就在数据库本身。fescar 这样做的好处是,把提交与回滚的逻辑放在了 SDK 层,从而不必要求底层的数据库必须对 XA 协议进行支持。对于业务来说,业务层也不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。基于我们先前对 XA 2PC 讨论,XA 2PC 存在参与者宕机的情况,而 fescar 的 2PC 模型中,参与者实际上是 SDK。参与者宕机这个问题之所以在 XA 2PC 中是个大问题,主要是因为 XA 中,分支事务是有状态的,即它是跟会话绑定在一起的,无法跨连接跨会话对一个分支事务进行操作,因此在 XA 2PC 中参与者一旦宕机,分支事务就已经无法再进行恢复。fescar 2PC 中,参与者实际上是 SDK,而 SDK 是可以做高可用设计的。并且,在其第一阶段,分支事务实际上已经是被提交了的,后续的全局上的提交和回滚,实际上是操作数据的镜像,全局事务的提交会异步清理 undo_log,回滚则会利用保存好的数据镜像,进行恢复。fescar 的 2PC 中,实际上是利用了 TCC 规范的无状态的理念。因为全局事务的执行、提交和回滚这几个操作间不依赖于公共状态,比如说数据库连接。所以参与者实际上是可以成为无状态的集群的。也就是说,在 fescar 2PC 中,协调者如果发现参与者宕机或者超时,那么它可以委托其他的参与者去做。
第二阶段非阻塞化
fescar 2PC 的第一阶段中,利用 SDK 中的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。这就意味着在阻塞在第一阶段过后就会结束,减少了对数据库数据和资源的锁定时间,明显效率会变更高。根据 fescar 官方的说法,一个正常运行的业务,大概率是 90% 以上的事务最终应该是成功提交的,因此可以在第一阶段就将本地事务提交呢,这样 90% 以上的情况下,可以省去第二阶段持锁的时间,整体提高效率。fescar 的这个设计,直接优化了 XA 2PC 协议本身的性能缺陷。
协调者的高可用
XA 2PC 中存在协调者宕机的情况,而 fescar 的整体组织上,是分为 server 层和 SDK 层的,server 层作为事务的协调者,fescar 的话术中称协调者为 TC(Transaction Coordinator),称 SDK 为 TM(Transaction Manager)。截止至这篇文章发表前,fescar 的 server 层高可用还未实现,依据其官方的蓝图,它可能会采用集群内自行协商的方案,也可能直接借鉴高可用 KV 系统。自行实现集群内高可用方案,可能需要引进一套分布式一致性协议,例如 raft,我认为这是最理想的方式。而直接利用高可用 KV 系统,例如 redis cluster,则会显得系统太臃肿,但实现成本低。
事务的隔离性
XA 2PC 是没有机制去支持全局事务隔离级别的,fescar 是提供全局事务的隔离性的,它把全局锁保存在了 server 层。全局事务隔离级别如果太高,性能会有很大的损耗。目前的隔离界别默认是读未提交,如果需要读已提交或者更高的级别,就会涉及到全局锁,则意味着事务的并发性会受影响。应用层业务层应该选择合适的事务隔离级别。
脑裂的问题仍然没有完美解决
无论是 XA 还是 fescar,都未解决上述提到的脑裂的问题。脑裂的问题主要影响了全局事务的最后的提交和回滚阶段。没有完美的分布式事务解决方案,即使是 fescar 或者 GTS,它们也必然需要人工介入。但脑裂问题是小概率事件,并且不是致命性错误,可以先通过重试的方法来解决,如果不行,可以收集必要的事务信息,由运维介入,以自动或者非自动的方式,恢复全局事务。
参考资料
维基百科:二阶段提交《2PC 之踵?是时候升级二阶段提交协议了》by Tim Yang《分布式事务概览》by beanlam fescar wiki