[分布式]-分布式事务

前言

什么是分布式事务

学习过数据库系统,学习过 MySQL 的人都知道事务这种东西,但单机数据库环境下的这种事务属于本地事务,即事务中的所有操作都只需在同一个数据源里进行即可。

而分布式事务是分布式系统环境下出现的另一种事务,它与本地事务不同的地方在于,同一个事务中涉及到的操作分散在不同的数据源中,而不同数据源又分布在不同的节点上,那么事务要执行成功,就要保证这些不同节点上的操作都要成功;如果事务执行失败,就要保证这些不同节点上的操作都回滚。而且节点之间的协调需要通过网络通信来完成,网络的存在加大了分布式事务的难度。总而言之,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务产生的原因

  1. 分布式系统环境下,整个系统采取微服务架构,由多个不同服务组成,每个服务有自己对应的数据库。那么 一个业务功能就有可能涉及到不同的服务,比如下单功能,就可能涉及到商家服务,订单服务等等,为了保证整个功能不影响其所涉及到每个数据库的数据一致性,就需要引入分布式事务来进行协调
  2. 有的功能可能并不涉及多个服务,但其需要操作的数据却分布在不同的数据库中。例如,在单机数据库并发压力太大或者数据量太大的情况下,我们会采取 分库分表 的优化手段,那么经过分库分表后,就有可能某些功能涉及到的数据被拆分到了两个数据库中;再例如,不同地区用户的数据可能存放在 不同地区的数据库 中,当一个功能涉及到多个用户,比如转账功能,就需要对两个用户的数据都进行修改。

不论是数据在 服务层面 上被划分开还是在 数据库层面 上被拆分开,总而言之,由于涉及到了多个数据库上数据的修改,因此需要引入分布式事务来协调,保证一致性

解决方案

在考虑分布式事务的解决方案之前,我们更应该优先考虑的一个问题是,我们是否真的需要引入分布式事务。因为不管采取何种方案,分布式事务的引入必然会增加系统的复杂度。我们应该思考是否能将业务涉及到的服务合并为单个服务,从而使用传统的单机事务解决方案,实在不行的情况下,我们才应该引入分布式事务的解决方案

2PC

2PC 指的是两阶段提交,Two Phrase Commit,是一种强一致性的解决方案。数据源在其中的角色称为资源管理器,同时引入了事务管理器作为协调者。

整个事务的提交被分为两个阶段,准备阶段 以及 提交阶段。准备阶段中事务管理器向每个资源管理器发送命令,让每个资源管理器都去执行事务,但是不提交,然后将执行结果,即成功或失败响应给事务管理器。

当事务管理器收到每个参与者的响应后,进入提交阶段。提交阶段不一定是提交事务,也可能是回滚事务:如果第一阶段所有参与者都返回成功的结果,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,向客户端返回事务执行成功的信息;反之,如果有一个参与者第一阶段返回的是失败的结果,那么协调者就会向每个参与者发送回滚事务的命令,最终返回事务失败的信息

故障分析

  1. 如果协调者第一阶段的命令没有收到所有参与者的响应,可能是网络原因或者参与者宕机,那么协调者在超时后认为事务执行失败,直接进入第二阶段并且让每个参与者都回滚事务
  2. 如果是第二阶段中协调者没有及时收到所有响应,那么协调者只能不断重试,因为有些参与者可能成功执行了命令,为了使数据一致,只能不断对未响应的参与者继续发送命令,不管是提交命令还是回滚命令

缺点

  1. 单点故障 问题:事务的执行跟提交都需要协调者的协调,如果协调者故障了,整个事务都无法继续进行下去
  2. 性能 问题:在整个事务进行的过程中,每个节点的数据库资源都被占用了,只有当执行完第二阶段协调者的提交或者回滚命令后,参与者才能释放数据库资源。整个过程都处于同步阻塞的状态,降低了运行效率,交互体验 以及 吞吐量,一旦网络波动问题严重,阻塞的时间就会更长
  3. 容错 问题:协调在发送进入下一阶段发送下一个命令之前,需要接收到所有参与者的响应,那么一旦有一个参与者故障,整个事务都只能失败回滚,甚至无法正常结束

3PC

3PC 就是为了解决 2PC 的一些问题而出现的,它将 2PC 中的准备阶段拆分为了两个阶段,所以共包含三个阶段,分别是准备阶段 can commit,预提交阶段 pre commit 以及提交阶段 do commit,在准备阶段会询问每个参与者是否能够执行事务所有参与者都返回能的话才会进入预提交阶段,预提交其实就是 2PC 中的准备阶段,执行事务但不提交,然后最后的提交阶段就是 2PC 中的提交阶段,提交或回滚事务

相比 2PC 的改进

  1. 引入了 can commit 阶段这个询问操作,可以使参与者之间的状态统一,方便协调者掌握各个参与者的状态。具体来说,一旦有一个参与者处于预提交或者提交阶段,那么表明每个参与者都经过了准备阶段,而且在准备阶段给出的都是肯定的答复,那么后续应该执行的都是提交事务的命令,不用考虑其它的因素;而如果没有 can commit 阶段的询问操作,而是跟 2PC 那样第一个阶段就是执行事务但不提交,如果第一阶段后协调者跟某个参与者宕机了,那么新的协调者就不知道宕机的参与者是执行成功了还是失败了,也就不知道在第二阶段是要发出提交事务的命令还是回滚事务的命令,因此 3PC 相比于 2PC 就解决了这个问题

  2. 此外,3PC 在参与者也引入了 超时机制,这个解决的是 2PC 中协调者单点故障的问题。在 2PC 中,如果在第一阶段执行结束后协调者宕机了,此时参与者都会阻塞,等待协调者第二阶段的命令,而在 3PC 中,如果参与者等待提交阶段的命令超时了,那么参与者会自行提交事务,不再等待;如果是等待预提交命令超时了,那参与者会直接结束事务,因为在准备阶段中并没有执行任何写入操作,无需提交也无需回滚,直接认为事务结束即可

缺点

  1. 性能 问题:相比于 2PC 多了一个阶段,性能有所下降
  2. 数据不一致 的问题:在等待提交阶段的命令超时后参与者会自行提交事务,但是有可能原本应该执行的是回滚操作,此时有的参与者正常回滚,有的参与者因为超时提交事务,就出现了数据不一致的情况

TCC

2PC 跟 3PC 都是数据库层面的解决方案,即多个数据库之间内部进行协调,我们无法干涉,只需要做到发起事务以及接收结果两件事而已。而 TCC 就涉及到业务层面了

TCC 指的是 Try-Confirm-Cancel。Try 指的是预留,即资源的预留和锁定;Confirm 指的是确认,真正地执行事务;Cancel 指的是撤销,即把预留阶段的动作撤销了。具体来说,执行事务前需要对每个操作涉及到的数据源执行预留操作,如果每个数据源都预留成功了就接着执行确认操作,如果有一个预留操作失败,那就执行撤销操作

除了业务调用方跟数据源外,还引入了事务管理器的角色,负责记录全局事务的状态以及进行提交或回滚操作,从而控制数据的一致性

那么在 TCC 这种方案中,对于每一个涉及到分布式事务的业务都需要拆分为 Try,Confirm,Cancel 三部分来操作,因此我们才说它涉及到业务层面,对业务的侵入性较强,需要根据特定的场景特定的业务逻辑来设计相应的操作,所以代码开发量也会相应增大

但也是因为很多事情都是在业务层完成,所以通用性比较强,适用范围更大,可以跨数据库,跨业务系统来完成

缺点

最突出的缺点就是前面提到的业务侵入性强

本地消息表

2PC,3PC,TCC 其实都是强一致性的解决方案,接下来的几种都是最终一致性的方案。

本地消息表就是完全利用分布式事务等于多个本地事务这一点来设计的

在数据源中会有一张本地消息表,每个分布式事务会对应一个消息,记录事务的执行状态,执行时间等信息。在执行业务时,会把业务的执行跟消息表的插入放在某一个本地事务中,根据本地事务的特点,可以确保在消息写入时,这一个业务一定是执行成功了的。那么此时就可以去接着调用分布式事务中的下一个业务,如果调用成功,就可以把整个事务对应的消息状态改为成功;如果 调用失败,可以设置定时任务去定时读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。那么需要重试,就需要保证对应服务的幂等性,而且一般重试需要设置最大次数,可以交由用户进行配置,超过最大次数就记录日志然后报警,让人工介入处理

缺点

如果有某个操作执行失败会一直采取重试而没有回退撤销的操作,因此 该方案是不支持回滚的,如果需要回滚操作的话不应采取该方案

MQ 事务

MQ 事务类似于本地消息表,只不过把本地消息表拆分出来到 MQ 中实现而已

具体来说,某个服务先给 MQ 发送分布式事务对应的一个 半消息,半消息的意思是该消息对消费者不可见。半消息发送成功后发送者执行自己的本地事务,根据执行结果再向 MQ 发送 Commit 或者 Rollback 消息,如果是 Commit 消息,订阅者就能收到,然后进行相应的操作并消费掉这条消息; 如果是 Rollback,那么订阅方将不会收到这条消息,相当于事务没执行过

而且,发送方应该提供一个 反查事务状态的补偿接口,当 MQ 发现一段时间内半消息没有收到对应的 Commit 或 Rollback,就可以通过反查接口查询发送方事务是否执行成功,再执行 Commit 或 Rollback

如果订阅者操作失败,也需要进行重试,也要考虑幂等以及重试次数的问题

缺点

与本地消息表同理,不支持回滚事务

Saga 事务

Saga 事务的核心思想是将分布式长事务拆分为多个本地短事务,由 Saga 事务协调器协调每个短事务的执行,如果每个短事务都正常结束,那整个分布式事务就正常完成;如果某个步骤失败,则根据相反顺序依次调用补偿操作

具体来说,每个 Saga 事务分为多个子事务 sub-transaction ti,每个 ti 就是一个本地事务。每个 ti 又有相应的补偿动作 ci,补偿动作用于撤销 ti 造成的结果,相当于一个 undo 操作

假设一个分布式事务可以拆分为 n 个子事务 t1 到 tn,如果每个子事务都能成功执行,那么最终整个 Saga 事务的执行顺序就是 t1,t2,t3,…,tn,整个 Saga 事务的结果也是成功执行
而如果有子事务执行失败,那么就需要采取恢复策略

Saga 中又将恢复策略分为两种:

  1. 向后恢复:假设子事务 tj 失败了,那么就需要执行之前所有子事务的 补偿 动作,撤销每个子事务的执行结果,从而撤销整个 Saga 事务的产生效果,那么最终整个 Saga 事务的执行顺序就是 t1,t2,…,tj,cj,cj-1,…,c2,c1
  2. 向前恢复:这种策略适用于必须要成功的场景,当 tj 失败时,会继续重试 tj,直到执行成功,然后继续执行后续的子事务,直到最后一个子事务成功。执行顺序类似于:t1,t2,…,tj(失败),tj(重试),…,tn。这种情况下就不需要补偿动作 ci 了。

Saga 事务拆分为了多个子事务,而且某个子事务在执行时,其他事务并不会锁住资源,这就可能导致整个 Saga 事务还未执行完毕时,部分子事务产生的效果已经被其他事务覆盖了。所以整个 Saga 事务对外是不具有隔离性的

缺点

首先是,正如前文所说,子事务不会在其它事务未执行完毕时锁住资源,因此整个分布式事务 不具有隔离性,其它事务可以对分布式事务产生的部分结果进行覆盖,从而破坏全局的数据一致性;
第二是,对于每个子事务还需要额外定义对应的补偿动作

猜你喜欢

转载自blog.csdn.net/Pacifica_/article/details/127974536