掘金 后端 ( ) • 2024-04-29 18:52

  在传统单体应用中,所有数据集中存储在一个数据库,任何对数据的操作都有事务作为一致性的保证。但随着业务的增长,单体应用往往无法继续支撑,这时候就需要用到分布式系统。以微服务架构的应用为例,一个客户端请求往往需要多个后台服务协同操作以完成响应。这些后台服务通常拥有各自独立的数据库,所以无法通过常规的数据库事务来保证数据的一致性,此时就需要用到分布式事务。

  常用的分布式事务解决方案既有同步模式,如两阶段提交(2PC);也有异步模式,如 saga。它们各自有其优缺点以及适用的场景:同步模式可以保证强一致性,但整个事务需要全程对资源加锁,这会限制整体服务的并发以及吞吐,不适用于比较大的分布式系统(系统规模越大,服务越多,完成一个事务需要的时间就越长,资源被锁定的时间也就越长);异步模式只能保证最终一致性,但可以提高系统的并发和吞吐,同时方便系统的扩展,适合规模比较大的分布式系统。

⒈ 2PC

  两阶段提交保证了整个事务操作的原子性以及数据的强一致性。两阶段提交需要有一个协调者(coordinator)来协调各个参与者节点(participant)之间的操作,以保证各个参与者节点之间步调一致,要么全部提交,要么全部回滚。

  1. prepare 阶段

  在 prepare 阶段,协调者向各个参与者发送 prepare 消息,然后等待各个参与者的响应。如果所有参与者都返回 ready 消息,则表明所有参与者都已经准备好提交事务;如果有任何一个参与者返回了 abort 消息,则表明有参与者无法提交事务,当前事务需要终止。

  在协调者等待各个参与者响应 prepare 消息的过程中,可能会有部分参与者因为网络原因或自身故障导致无法及时响应协调者的消息。如果协调者在指定的超时时间内没有收到参与者的响应消息,则协调者默认当前参与者响应的是 abort 消息。

  1. commit 阶段

  如果协调者收到的所有参与者对 prepare 消息的响应均为 ready,则协调者会向所有参与者发送 commit 消息,各个参与者在收到消息后将当前事务提交;如果有参与者对 prepare 消息的响应为 abort,则协调者向所有参与者发送 abort 消息,各个参与者在收到消息后将当前事务回滚。

  两阶段提交的实现基于数据库系统本身对事务的支持,易于理解和实现,同时也保证了整个事务操作的原子性以及数据的一致性。但是,两阶段提交全程需要协调者与各个参与者之间频繁通信,这无疑增加了系统的性能开销,不利于系统扩展。另外,在整个过程中,协调者的角色至关重要,万一协调者出现故障,则可能导致各个参与者处于不确定状态、系统被阻塞、甚至可能出现数据不一致。再者,整个事务的过程需要对特定的数据记录加锁,如果在事务完成之前协调者出现故障,那么这些被锁定的记录将无法被其他事务访问。

如果协调者在事务执行过程中出现故障,各参与者由于无法收到后续的消息而处于不确定的状态(不知道应该提交还是回滚);事务过程中被锁定的记录由于无法被其他依赖这些数据的事务访问而导致系统被阻塞;

如果协调者在第二阶段出现故障,部分参与者可能已经收到了 commit 消息而提交了事务,部分参与者却没有收到 commit 消息,这将会导致最终数据的不一致。

⒉ 3PC

  三阶段提交相较于两阶段提交是在 preparecommit 之间增加了一个缓冲阶段 pre-commit。一旦事务进入 pre-commit 阶段,则表明各参与者已经准备好提交事务,并且协调者也已经决定提交事务。

3PC commit 流程

3PC abort 流程

  在 prepare 阶段,只有当所有的参与者都准备好提交事务以后,协调者才会向各个参与者节点发送 pre-commit 消息;如果有任何参与者在 prepare 阶段没有准备好提交事务(自身故障或网络等原因),则协调者直接向各个参与者发送 abort 消息回滚事务。

  如果协调者在事务执行过程中出现故障,所有参与者可以集体决定事务最终的状态。只要有任何一个参与者处于 pre-commit 状态,则表明所有参与者都已经准备好提交事务,所有参与者接下来只需要将事务提交即可。而如果所有的参与者当前都处于 prepare 状态,则将事务进行回滚。通过加入缓冲阶段 pre-commit,三阶段提交解决了两阶段提交中因为协调者故障导致的阻塞问题。

⒊ Saga

  Saga 是一种异步的分布式事务解决方案,只能保证最终一致性。其本质是一组本地事务序列,各个参与者只需要执行各自本地的事务,然后通过发布事件消息来触发下一个参与者执行本地事务,依此类推,直到整个事务完成。如果在此过程中有参与者的本地事务执行失败,则需要对该参与者之前的所有参与者的本地事务进行补偿,以保证数据的一致性。

  由于在 saga 中各个参与者只需要执行本地事务,所以即使是需要长时间执行的事务也不会对系统整体造成阻塞,但这也导致事务之间缺乏隔离性。另外,saga 的特性也导致任何一个参与者的失败引发的对之前参与者的补偿非常复杂,尤其是对于高并发、高吞吐的系统,在执行补偿之前,同一参与者的同一份数据可能已经被其他事务修改过。

⓵ 基于编排的 Saga(Orchestrator-based)

  基于编排的 saga 需要一个编排器(orchestator)与各个参与者进行通信,在这种模式下参与者之间不进行消息交互,所有的消息交互都在编排器与参与者之间进行。编排器通过消息告诉参与者需要需要执行的操作,参与者在执行完本地事务后将结果通知编排器,参与者根据收到的消息决定是继续执行事务还是终止事务并对之前已完成的本地事务进行补偿。

  由于各个参与者只需要与编排器进行通信,所以参与者之间没有依赖关系,降低了参与者之间的耦合。参与者只需要负责自身的功能,不需要关心系统当前的状态以及事务整体执行的情况,方便了系统的扩展。

  同样,由于所有的参与者都与编排器进行交互,所以编排器自身的性能可能成为整体系统的性能瓶颈,造成延时等问题。另外,如果编排器出现故障,则整个系统将会瘫痪(可以同时部署多个编排器实例,当编排器出现故障时进行迁移)。

⓶ 基于协同的 Saga(Choreography-based)

  基于协同的 saga 中各参与者之间直接进行消息交互,一个参与者在执行完本地事务后向消息中间件发布消息,而订阅了该消息的参与者在收到消息后执行自身的本地事务,然后再向消息中间件发布消息,依此类推,直到事务完成。如果在事务执行过程中有参与者的本地事务执行失败,参与者同样会发布消息,订阅了该消息的参与者在收到消息后对已经完成的本地事务进行补偿。

  基于协同的 saga 没有中央协调者的角色,结构和实现都相对简单,同时也避免了因为中央协调者失效而导致的整个系统瘫痪的问题。同样是因为没有中央协调者角色,导致系统整体的维护以及对业务流程的理解都比较困难。参与者之间互相通信会造成依赖关系,进一步可能会出现循环依赖以及高耦合。