分布式事务解决方案


分布式理论

CAP定理

在一个分布式系统中,以下三点特性无法同时满足,「鱼与熊掌不可兼得」

一致性(C):
在分布式系统中的所有数据备份,「在同一时刻是否拥有同样的值」。(等同于所有节点访问同一份最新的数据副本)

可用性(A):
在集群中一部分节点「故障」后,集群整体「是否还能响应」客户端的读写请求。(对数据更新具备高可用性)

分区容错性(P):
即使出现「单个组件无法可用,操作依然可以完成」

具体地讲在分布式系统中,在任何数据库设计中,一个Web应用「至多只能同时支持上面的两个属性」。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。

BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢?

前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:

  • 「Basically Available(基本可用)」

  • 「Soft state(软状态)」

  • 「Eventually consistent(最终一致性)」

基本可用( Basically Available):指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用

  • 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
  • 功能上的损失:购物网站在购物高峰(如双十—)时,为了保护系统的稳定性部分消费者可能会被引导到一个降级页面

**软状态( Soft state)**:指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许同副本同步的延时就是软状态的体现。 mysql replication的异步复制也是一种体现

**最终一致性( Eventual Consistency)**:是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

强一致性、弱一致性、最终一致性
从客户端角度,多进程并发访间时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

分布式系统实现一致性算法:raft算法原理动画展示

分布式事务解决方案

两阶段提交(2PC)

数据库支持的2pc【2 phase commit二阶提交】,又叫做 XA Transaction。MSQL从5.5版本开始支持, SOL Serve2005开始支持, Oracle7开始支持
其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交 (precommit)此操作,并反映是否可以提交
第二阶段:事务协调器要求毎个数据库提交数据。其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息

可能会存在哪些问题?

  • 单点故障:一旦事务管理器出现故障,整个系统不可用

  • 数据不一致:在阶段二,如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录 prepare阶段日志,主备切换会导致主库与备库数据不一致。

  • 响应时间较长:整个消息链路是串行的,要等待响应结果,不适合高并发的场景

  • 不确定性:当事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。

  • 许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。

三阶段提交(3PC)

三阶段提交又称3PC,相对于2PC来说增加了CanCommit阶段和超时机制。如果段时间内没有收到协调者的commit请求,那么就会自动进行commit,解决了2PC单点故障的问题。

但是性能问题和不一致问题仍然没有根本解决。下面我们还是一起看下三阶段流程的是什么样的?

  • 第一阶段:CanCommit阶段这个阶段所做的事很简单,就是协调者询问事务参与者,你是否有能力完成此次事务。
    • 如果都返回yes,则进入第二阶段
    • 有一个返回no或等待响应超时,则中断事务,并向所有参与者发送abort请求
  • 第二阶段:PreCommit阶段此时协调者会向所有的参与者发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
  • 第三阶段:DoCommit阶段在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”转变为“提交状态”。然后向所有的参与者节点发送”doCommit”请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

柔性事务(TCC事务补偿方案)

TCC其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

Try,Confirm,Cancel

  • Try阶段主要是对业务系统做检测及资源预留,其主要分为两个阶段
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放

TCC 事务机制相比于上面介绍的2PC,解决了其几个缺点:

  • 1.解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
  • 2.同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
  • 3.数据一致性,有了补偿机制之后,由业务活动管理器控制一致性

总之,TCC 就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,并且很大程度的增加了业务代码的复杂度,因此,这种模式并不能很好地被复用。

本地消息表

执行流程:

  • 消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。

  • 如果消息发送失败,会进行重试发送。

  • 消息消费方,需要处理这个消息,并完成自己的业务逻辑。

    • 如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

    • 此时如果本地事务处理成功,表明已经处理成功了

    • 如果处理失败,那么就会重试执行。

  • 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。

消息事务

消息事务的原理是将两个事务通过消息中间件进行异步解耦,和上述的本地消息表有点类似,但是是通过消息中间件的机制去做的,其本质就是’将本地消息表封装到了消息中间件中。

执行流程:

  • 发送prepare消息到消息中间件
  • 发送成功后,执行本地事务
  • 如果事务执行成功,则commit,消息中间件将消息下发至消费端。如果事务执行失败,则回滚,消息中间件将这条prepare消息删除
  • 消费端接收到消息进行消费,如果消费失败,则不断重试

这种方案也是实现了最终一致性,对比本地消息表实现方案,不需要再建消息表,不再依赖本地数据库事务了,所以这种方案更适用于高并发的场景。目前市面上实现该方案的只有阿里的 RocketMQ

最大努力通知

最大努力通知的方案实现比较简单,适用于一些最终一致性要求较低的业务。

执行流程:

  • 系统 A 本地事务执行完之后,发送个消息到 MQ;
  • 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
  • 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。

Sagas 事务模型

Saga事务模型又叫做长时间运行的事务

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

Seata框架中一个分布式事务包含3种角色:

**Transaction Coordinator (TC)**:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

**Transaction Manager (TM)**:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

**Resource Manager (RM)**:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

seata框架为每一个RM维护了一张UNDO_LOG表,其中保存了每一次本地事务的回滚数据。

具体流程:

  1. 首先TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID

  2. XID 在微服务调用链路的上下文中传播。

  3. RM 开始执行这个分支事务,RM首先解析这条SQL语句,生成对应的UNDO_LOG记录。下面是一条UNDO_LOG中的记录,UNDO_LOG表中记录了分支ID,全局事务ID,以及事务执行的redo和undo数据以供二阶段恢复。

  4. RM在同一个本地事务中执行业务SQL和UNDO_LOG数据的插入。在提交这个本地事务前,RM会向TC申请关于这条记录的全局锁

  5. 如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向TC汇报本地事务执行失败。

  6. RM在事务提交前,申请到了相关记录的全局锁,然后直接提交本地事务,并向TC汇报本地事务执行成功。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。

  7. TC根据所有的分支事务执行结果,向RM下发提交或回滚命令。

  • RM如果收到TC的提交命令,首先立即释放相关记录的全局,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。异步队列中的提交请求真正执行时,只是删除相应 UNDO LOG 记录而已。
  • RM如果收到TC的回滚命令,则会开启一个本地事务,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。将 UNDO LOG 中的后镜与当前数据进行比较,
  • 如果不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
  • 如果相同,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。

文章作者: wmg
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wmg !
  目录