分布式事务

906 阅读13分钟

什么是事务和分布式事务

事务(transaction)是我们学习数据库时经常提到的概念,它是指数据库执⾏过程中的⼀个逻辑单位,把我们的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作要么全部完成(commit),要么什么都不做(rollback)。

事务有四大特性(ACID):

  • 原⼦性(Atomicity):事务作为⼀个整体被执⾏,包含在其中的对数据库的操作要么全部被执⾏,要么都不执⾏。
  • ⼀致性(Consistency): 事务应确保数据库的状态从⼀个⼀致状态转变为另⼀个⼀致状态。⼀致状态是指数据库中的数据应满⾜完整性约束。
  • 隔离性(Isolation): 多个事务并发执⾏时,⼀个事务的执⾏不应影响其他事务的执⾏,如同只有这⼀个操作在被数据库所执⾏⼀样。
  • 持久性(Durability): 已被提交的事务对数据库的修改应该永久保存在数据库中。在事务结束时,此操作将不可逆转。

分布式事务(Distributed Transaction)指事务的参与者、⽀持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上,且属于不同的应⽤,分布式事务需要保证这些操作要么全部成功,要么全部失败。

换句话说,在目前的互联网架构中,一个操作的成功,不仅取决于对本地db的操作成功,还取决于上下游服务等一系列服务的成功,简单的说,分布式事务就是为了保证不同服务不同数据库的数据一致性。

分布式事务的理论基础

分布式系统有三大指标:

  • 一致性(Consistency):对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
  • 可用性(Availability):顾名思义,就是系统可不可用的指标,非故障的节点应该在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。
  • 分区容错性(Partition tolerance):当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

但在CAP理论中,这三项指标不能同时完成,一般而言,网络不是100%可用的,所以分布式系统通常采用CP或者AP。当网络出现问题时,如果需要保证数据的一致性,则需要暂时放弃系统的可用性。如果要保证系统的可用性,则势必要放弃一定的数据一致性。

BASE理论 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。

  • 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
  • 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。
  • 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务的实现

分布式事务的实现也分为两种不同的级别。一种是偏底层的实现,由数据库自己来实现分布式事务,比较著名的有两阶段提交(2PC)和三阶段提交(3PC)。另一种是偏上层实现,业务系统自己来实现分布式事务,比较常见的是TCC。

学习2PC和3PC之前可以先了解一下XA Transactions--2PC/3PC是具体的协议和方案,XA是规范。

2PC

2PC(Two-phaseCommit) 是两阶段提交的过程

在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有其他节点(称作参与者)的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。

协调的过程分为两个阶段:

image-20220903171912780.png

  • 准备阶段:又叫voting phase 投票阶段

    事务协调者给每个参与者发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

image-20220903172100723.png

  • 提交阶段:commit phase 提交阶段

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

总结下来,正常情况下两阶段提交有这么两种情况

image-20220903172309428.png

image-20220903172328856.png

两阶段提交尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现。 但是两阶段提交仍然存在一系列问题,性能并不高(网络开销,锁开销等成本高)

  • 同步阻塞:参与者在等待消息的时候处于阻塞状态,会锁资源,其他事务需要等待释放资源。
  • 单点问题:如果协调者在第二阶段挂掉,那么所有参与者都会阻塞,无法继续完成事务。即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
  • 数据一致性问题:如果在commit阶段由于网络问题,部分参与者没有收到commit消息,会一直阻塞,导致整个系统数据不一致

3PC

3PC(Three-phase Commit Protocol),三阶段提交协议是2PC的改进版,主要的改动点在于:

1、引入超时机制。同时在协调者和参与者中都引入超时机制。

2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

3PC的三个阶段分别为:

  • CanCommit阶段:类似于2PC的prepare阶段

    • 事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
    • 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
  • PreCommit阶段:协调者根据canCommit阶段参与者的反应情况来决定是否可以继续事务的PreCommit操作。这个阶段和canCommit阶段合起来完成了prepare完成的事情

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

      • 发送预提交请求: 协调者向参与者发送PreCommit请求
      • 事务预提交: 参与者接收到PreCommit请求后,会执行事务操作,并将undoredo信息记录到事务日志中。
      • 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
    • 假如canCommit阶段有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

      • 发送中断请求: 协调者向所有参与者发送abort请求
      • 中断事务: 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。不进行doCommit了
  • doCommit阶段:该阶段进行真正的事务提交

    • 执行提交:

      • 发送提交请求:协调接在preCommit阶段收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
      • 事务提交: 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
      • 响应反馈: 事务提交完之后,向协调者发送Ack响应。
      • 完成事务: 协调者接收到所有参与者的ack响应之后,完成事务。
    • 中断事务:协调者在preCommit阶段没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

      • 发送中断请求:协调者向所有参与者发送abort请求
      • 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
      • 反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息
      • 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

image.png

当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求(如果不能执行会收到abort,不会进入第三阶段),那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)

所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大,所以超时后可以自动commit。

相比于2PC,3PC所有参与者也都有超时机制,主要解决单点故障问题(一定程度也解决了阻塞问题),可以防止因单点故障带来的资源无法释放的问题,在doCommit阶段,一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。

另外,2PC 还有个问题无法解决。那就是协调者再发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交(所有人都在prepare状态,协调者也不知道发出commit没)。

这种情况在 3PC 中是有办法解决的,因为在 3PC 中,选出新的协调者之后,他可以咨询所有参与者的状态,如果有某一个处于 commit 状态或者 prepare-commit 状态,那么他就可以通知所有参与者执行 commit,否则就通知大家 rollback。因为 3PC 的第三阶段一旦有机器执行了 commit(即收到了prepare-commit信息并处于这个状态),那必然第一阶段大家都是同意 commit 的,所以可以放心执行 commit。

但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况(几率比较小,canCommit阶段已经询问了能否执行,如果不能执行到不了第三阶段)。

同时,二阶段提交与三阶段提交可以对比zookeeper的ZAB协议(选举、发现、同步阶段) 投票机制,前者是要求所有结点返回的状态必须一致,而后者是只要超过半数就可以

TCC(Try-Confirm-Cancel)

TCC 也可以看成两阶段提交的过程,但不太一样。TCC是偏上层实现,业务系统自己来实现分布式事务。

TCC每个阶段都是完整的数据库事务,锁资源的时间短,两阶段的提交事务只在第二阶段。

try阶段:在 TCC 的第一个阶段,协调者要求所有数据库尝试(Try)进行所有本地事务。本地尝试之后将尝试的结果返回给协调者。在两阶段提交的第一阶段,事务并没有提交,而是到达了“准备成功”的状态,而在 TCC 的情况下,事务会真正提交。

Comfirm阶段:TCC 第一阶段结束之后,协调者知道了所有节点的状态。如果所有节点的本地事务提交都成功,那么协调者会给所有节点发送确认(Confirm)消息。节点在收到确认消息之后进行确认操作。

另外,如果有任何一个节点在第一阶段(Try)出了问题,协调者就会给所有节点发送取消(Cancel)的消息。节点在收到Cancel消息之后,会对第一阶段的事务做逆向操作,取消掉第一阶段的影响。 Ps.TCC 的取消操作不是事务的回滚,而是业务的回滚。因为第一阶段已经提交了事务,所以不能对已经提交的事务进行回滚操作。这时候用到的是事务补偿,也就是说用一个反向业务来对冲正向业务的效果。因此你如果想要实现 TCC 的话,需要把每个业务实现两遍。一遍是正向的业务,另一遍是反向的业务。

因此,基于 TCC 实现分布式事务,会将原来只需要一个接口就可以实现的逻辑拆分为 Try、Confirm、Cancel 三个接口,所以代码实现复杂度相对较高。可以参考这篇文章理解参考文章

为了防止网络问题带来的乱序和丢失问题,TCC 还需要支持回滚空事务(回滚一个不存在的事物--还没收到try)和防悬挂(第一阶段的尝试提交如果发现有空回滚标识的话,尝试提交需要失败。这个过程也叫作防悬挂。)

image.png

对于TCC的实际实现,可以了解一下seata框架