原创

分布式系统学习-分布式事务、两阶段提交协议、三阶段提交协议

何为事务

事务机制可以将一个活动所涉及的全部操作当做一个不可分割的执行单元,只有这个执行单元的所有操作均能正常执行的情况下才提交事物;否则,只要其中任一一个操作执行失败,都将导致整个执行单元回滚。现在的关系型数据库、部分消息中间件都具备这样的事务处理能力。

事务的ACID

  1. Atomicity——原子性,是说事务中的所有操作的结果,要么全部成功,要么全部失败,不会存在中间状态。事务在执行过程中如果发生了错误,那么事务会恢复到开始前的状态,就像这个事务从来没有发生过一样。

  2. Consistency——一致性,是指在一个事务执行前后数据都必须处于一致的状态。如果事务成功执行,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中执行出错,那么系统中的所有变化将自动地回滚,系统恢复到原始状态。

  3. Isolation——隔离性,是指在多个请求并发操作相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

  4. Durability——持久性,是指当事务成功结束,它对数据库所做的更新就必须永久保留。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

  ACID特性是事务所具备基本特性, 目前关系型数据库大多实现了事务功能。传统单体应用中,对数据库的访问控制在一个数据库内,一个数据库事务操作不存在跨数据库访问情形。通过数据库本身提供的事务能力,很好的解决了一致性问题。

一致性问题的来源

  本地事务可以很好的解决一致性问题,然而事情却有了些变化。随着分布式及微服务架构越来越盛行的今天,越来越多的企业都在进行微服务改造,希望将自身的大而杂的几个大应用分离细化成数量众多的微小服务,尽量让每个微服务只做自己相关的业务,只操作跟自己相关的资源。

分布式服务

20190628150859671877.png

如图所示,一个完整的业务被拆分成了ABCDEF六个服务。每个服务有独立的进程空间,独立的数据库,并单独开发部署。各服务之间基于RPC调用,每个服务依赖数据库的本地事务能力,控制业务本身的数据一致性。

但是,假设在这个调用过程中服务C调用F异常,导致服务ABC的业务数据回滚(因调用异常导致本地事务回滚),F的业务数据可能已提交或者未执行,服务E本地事务已提交,而服务D还未调用,那么这个时候,这几个服务的数据状态将存在不一致的情况,即部分成功部分失败。从全局来看,使用者肯定不希望这样的发生。使用者希望的是如果成功那所有服务的数据都提交;如果某个服务失败,那已经提交的数据也必须回滚。

分布式事务一致性解决方案

目前分布式事务解决方案有很多种,基于两阶段提交的XA分布式事务和满足Base特性的最终一致性方案。

两阶段提交

两节阶段提交可以保证数据的强一致性。他是协调所有分布式原子事务的参与者,并决定提交或者取消提交的算法,同时也是解决一致性问题的一致性算法。所有关于分布式事务的介绍中都必然会讲到两阶段提交,因为它是实现XA分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)。所谓的两个阶段是指:第一阶段:准备阶段和第二阶段:提交阶段。

20190702172350353.png

图2.两阶段提交示意图(摘自info发布的《java事务设计策略》一文)

1.准备阶段:事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。(关于每一个参与者在准备阶段具体做了什么目前我还没有参考到确切的资料,但是有一点非常确定:参与者在准备阶段完成了几乎所有正式提交的动作,有的材料上说是进行了“试探性的提交”,只保留了最后一步耗时非常短暂的正式提交操作给第二阶段执行。)

2.提交阶段:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作,这样,最后的提交阶段将是一个耗时极短的微小操作,这种操作在一个分布式系统中失败的概率是非常小的,也就是所谓的“网络通讯危险期”非常的短暂,这是两阶段提交确保分布式事务原子性的关键所在。(唯一理论上两阶段提交出现问题的情况是当协调者发出提交指令后当机并出现磁盘故障等永久性错误,导致事务不可追踪和恢复)

从两阶段提交的工作方式来看,很显然,在提交事务的过程中需要在多个节点之间进行协调,而各节点对锁资源的释放必须等到事务最终提交时,这样,比起一阶段提交,两阶段提交在执行同样的事务时会消耗更多时间。事务执行时间的延长意味着锁资源发生冲突的概率增加,当事务的并发量达到一定数量的时候,就会出现大量事务积压甚至出现死锁,系统性能就会严重下滑。这就是使用XA事务

XA事务

XA事务是基于两阶段提交协议的一个分布式事务实现,XA事务主要定义了事务管理器和资源管理器之间的接口。XA接口是双向的系统接口,在事务管理器以及一个或多个资源管理器之间形成通信桥梁。

在XA事务中,由事务管理器协调多个资源管理器。

  • 第一阶段,事务管理器给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败,要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
  • 第二阶段,如果事务管理器收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。

下图说明了事务管理器、资源管理器,与应用程序之间的关系:

20190702172350600.gif
图1.XA规范下的分布式事务各类参与者之间的关系

XA事务优点

  1. 实现了事务的隔离,确保了强一致性,即本地事务未提交的数据对其它事务不可见,事务要么都提交成功要么都失败;

  2. 业务编程简单,由于事务管理是由事务管理器及本地事务资源管理器实现,开发者不必介入太多事务相关的工作;

XA事务缺点

  1. 同步阻塞调用,在事务执行过程中,所有参与者同步锁定资源以实现隔离,被锁定的资源长时间不能被其他事务访问;

  2. 需要本地事务支持,即本地数据库需要支持XA协议;

  3. 需要有事务管理器统一协调资源管理器,事务管理器本身存在单点问题;

  4. 事务会出现无法确认的状态。如当事务管理器发出commit请求后,事务管理器宕机,而参与者可能只有一个已提交,其它参与者尚未提交。此时如果唯一的参与者也宕机,整个事务状态将无法确认。

三阶段提交

三阶段提交(三阶段提交协议)是根据二阶段提交协议的缺点改进而来,相较于二阶段改进了两个点:

  • 在协调者和参与者中都引入了超时机制,
  • 在第一阶段中插入了一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

 1. 事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

 2. 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。

  • 假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

    1. 发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。

    2. 事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

    3. 响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

  • 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

    1. 发送中断请求 协调者向所有参与者发送abort请求。

    2. 中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

  • 执行提交

    1. 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。

      1. 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    2. 响应反馈 事务提交完之后,向协调者发送Ack响应。

    3. 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

  • 中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

    1. 发送中断请求 协调者向所有参与者发送abort请求

    2. 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

    3. 反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息

    4. 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。)

2PC与3PC的区别

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。后面的文章会介绍这个公认为难于理解但是行之有效的Paxos算法。

  1. 百度百科
  2. http://en.wikipedia.org/wiki/Java_Transaction_API
  3. http://www.nosqlnotes.net/archives/62#more-62
  4. http://hi.baidu.com/javaopensource/blog/item/0a2b764ec501b10cb3de05ba.html
  5. http://www.mamicode.com/info-detail-2713917.html
  6. https://www.hollischuang.com/archives/681
sev7e0
Write by sev7e0
end
本文目录