分布式事务解决方案

2pc

核心特点:

2PC和3PC都是保证强一致性

2pc是同步阻塞的,参与者会锁定资源并阻塞等待协调者的命令,等不到命令就会一直持有资源不释放,影响其他程序的运行,所以效率低,

2pc存在协调者单点故障问题

3pc引入预提交阶段解决了同步阻塞问题。

2pc和3pc在极端条件下都无法保证数据一致

正常流程

第一阶段: prepare Done
在这里插入图片描述
事务协调者的节点会首先向所有的参与者节点发送Prepare请求。
在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“Done”消息。
当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段

第二阶段: commit ack
在这里插入图片描述
事务协调者向所有事务参与者发出Commit请求。
接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“ACK”消息。
当事务协调者接收到所有事务参与者的ACK反馈,整个分布式事务完成。

异常处理

以上所描述的是XA两阶段提交的正常流程,接下来我们看一看失败情况的处理

第一阶段失败——回滚
在XA的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,
于是在第二阶段,事务协调节点向所有的事务参与者发送Abort回滚命令。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作

第二阶段失败——不断重试
第二阶段执行的是回滚事务操作或是提交事务操作两种情况。
办法都是不断重试,直到所有节点的事务都回滚或提交成功

2PC的不足

同步阻塞导致性能差

2pc追求强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,只有所有的节点都提交完成,分布式事物才算执行完毕,才能释放资源,否则一直阻塞等待,影响其他程序的运行;

协调者单点故障

事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。

协调者挂了的影响,挂在不同时间点的影响不同:

  • 假设协调者在发送准备命令之前挂了,还行,等于事务还没开始。

  • 假设协调者在发送准备命令之后挂了,这就不太行了,有些参与者处于了事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统中的其它操作。

  • 假设协调者在发送回滚事务命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功的参与者都阻塞着。

  • 假设协调者在发送回滚事务命令之后挂了,这个还行,至少命令发出去了,很大的概率都会回滚成功,资源都会释放。

  • 假设协调者在发送提交事务执行命令之前挂了,这个最糟糕!这下是所有资源都阻塞着。

  • 假设协调者在发送提交事务执行命令之后挂了,这个还行,也是至少命令发出去了,很大概率都会提交成功,然后释放资源。

协调者挂了的解决办法:可以通过选举等操作选出一个新协调者来顶替。

极端情况下无法保证数据一致性

1、如果协调者挂在第一阶段,其实影响不大都回滚好了,在第一阶段事务肯定还没提交。

2、如果协调者挂在第二阶段

2.1、假设参与者都没挂,此时新协调者可以向所有参与者确认它们自身情况来推断下一步的操作。

2.2、假设有个别参与者挂了*
比如协调者发送了回滚命令,此时第一个参与者收到了并执行,然后协调者和第一个参与者都挂了。
新选举的协调者不知道挂掉的参与者是否提交事务成功,所以不知道当前应该发送提交还是回滚的命令

所以说极端情况下,2pc是无法避免数据不一致问题的。

3PC

3PC 相对于 2PC 做了一定的改进:

  1. 增加了预提交阶段减少执行业务代码的次数。节约资源
  2. 引入了参与者超时机制,避免资源被永久锁定。

提交流程
在这里插入图片描述

准备提交阶段

在准备提交阶段,协调者询问参与者是否可以执行事务提交操作

2pc的预提交阶段需要我们执行完耗时的业务代码,然后进行事务的预提交,这时候我们才能知道事务预提交是否可以成功,如果这时候超时,那么前面耗时的业务代码执行就白费了。

所以,preCommit拆分之后,就把事务超时问题先提前询问一次,可以进行事务提交才执行耗时的业务代码。可以减少执行业务代码的次数。节约资源

预提交阶段

准备提交阶段中没有超时问题才会进入预提交阶段

preCommit阶段等同于2pc的第一阶段,同样是执行业务代码,最后不真正的提交事务

提交阶段

分两种,正常的提交和异常中断

正常提交阶段:

  • 发送提交请求。协调者接收到所有参与者发送的 Ack 响应,从预提交状态进入到提交状态,并向所有参与者发送 DoCommit 提交消息。
  • 事务提交。参与者接收到 DoCommit 消息之后,正式提交事务。完成事务提交之后,释放所有锁住的资源。
  • 响应反馈。参与者提交完事务之后,向协调者再次发送 Ack 响应。
  • 完成事务。协调者接收到所有参与者的 Ack 响应之后,完成事务。

异常中断阶段:

  • 当有节点没能正常执行事物的话,协调者向所有参与者发送 Abort中断请求。
  • 事务回滚。参与者接收到 Abort 消息之后,回滚并释放所有锁住的资源。
  • 反馈结果。参与者完成事务回滚之后,向协调者发送 Ack 消息。
  • 终断事务。协调者接收到参与者反馈的 Ack 消息之后,执行事务的终断,并结束事务。

超时机制解决单点故障和同步阻塞

我们知道 2PC 是同步阻塞的,3pc的超时机制可以很好的解决同步阻塞问题
并且当第三阶段时协调者挂了事务也能顺利提交,解决了协调者单点故障问题,具体如下:

  • canCommit阶段,事务管理器等待参与者的响应超时,超时则中断事务;

  • preCommit阶段,事务管理器等待参与者的响应超时,超时则回滚事务;

  • doCommit阶段,参与者等待事务管理器的commit或者rollback指令,超时则默认自动commit;所以即使协调者挂了,事务也能提交,解决了单点故障问题。

TCC 方案

TCC和2PC、 3PC类似,追求强一致性,适合金融转账之类的场景。

  • Try初步操作:
    对各个服务的资源做检测和锁定
  • Confirm确认操作:
    是对try的一个确认,确认try执行完毕。
  • Cancel 取消操作:
    是对try的一个回撤,如果任何一个参与节点执行出错,就对执行成功的业务逻辑进行回滚操作

基于MQ的事物消息的最终一致性方案

无论是 2PC & 3PC 还是 TCC,基本都遵守 XA 协议的思想,追求的是强一致性,存在同步阻塞等对性能开销很大的操作,适合金融类对数据一致性要求很高但并发量不大的场景
而互联网这种追求最终一致性,提高并发量的场景,适合的分布式事务解决方案为:基于MQ的事物消息的最终一致性方案

需要引入了一个消息中间件,经过调研发现RocketMQ是所有mq中对事务支持做的最好的,所以选择RocketMQ来实现。

举例:
下单成功和清空购物车这两个操作原本在一个事物里面,现在利用mq将核心功能下单和非核心功能清空购物车异步。
在这里插入图片描述
对于订单系统来说,它创建订单的过程中实际上执行了 2 个步骤的操作:

  • 在订单库中插入一条订单数据,创建订单;
  • 发消息给消息队列,消息的内容就是刚刚创建的订单。

购物车系统订阅相应的主题,接收订单创建的消息,然后清理购物车,在购物车中删除订单中的商品。

在分布式系统中,上面提到的这些步骤,任何一个步骤都有可能失败,如果不做任何处理,那就有可能出现订单数据与购物车数据不一致的情况,造成下面两种问题:
问题一: 创建了订单,没有清理购物车;
问题二:订单没创建成功,购物车里面的商品却被清掉了。

那我们需要解决的问题可以总结为:
在上述任意步骤都有可能失败的情况下,还要保证订单库和购物车库这两个库的数据一致性。

上面问题一很好解决,可以这样设置:只有成功执行购物车清理后,才提交消费确认,如果失败,由于没有提交消费确认,消息队列会自动重试,然后重新清空购物车,保证能成功清空。

关键是问题二不好解决,创建订单和发送消息这两个步骤要么都操作成功,要么都操作失败,不允许一个成功而另一个失败的情况出现。 这就是消息队列需要实现的分布式事务

我们一起来看下如何用消息队列来实现分布式事务,如下图所示:
在这里插入图片描述
首先,订单系统在消息队列上开启一个事务。然后订单系统给消息服务器发送一个“半消息”,半消息包含的内容就是完整的消息内容,和普通消息的唯一区别是:在事务提交之前,对于消费者来说,这个消息是不可见的。
半消息可理解成:交了全款但不发货。
半消息发送成功后,订单系统就可以执行本地事务了,在订单库中创建一条订单记录,并提交订单库的数据库事务。然后根据本地事务的执行结果决定提交或者回滚事务消息。如果订单创建成功,那就提交事务消息,购物车系统就可以消费到这条消息继续后续的流程。如果订单创建失败,那就回滚事务消息,购物车系统就不会收到这条消息。这样就基本实现了“要么都成功,要么都失败”的一致性要求。

为什么不直接在本地事务成功/失败之后再决定是否发送完整消息,而使用这种半消息设计呢?
原因是可能在本地事务提交成功后突然断电了,这样发不出消息 。采用半消息的话,断电之前我的消息已经发出去了,机器重启之后消息还在mq中,我们也能根据此已经发出的半消息做最终处理,达成最终一致。

还有一个问题没有解决

如果在第四步提交事务消息时失败了怎么办?
对于这个问题,Kafka 和 RocketMQ 给出了 2 种不同的解决方案。

Kafka 的解决方案比较简单粗暴,直接抛出异常,让用户自行处理。我们可以在业务代码中反复重试提交,直到提交成功。

RocketMQ 则给出了另外一种解决方案:
在 RocketMQ 中的事务实现中,增加了事务反查的机制来解决事务消息提交失败的问题。如果 Producer 也就是订单系统,在提交或者回滚事务消息时发生网络异常,RocketMQ 的 Broker 没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。
怎么反查?
本例中可以根据订单ID查找订单表中是否有该笔订单,若有则说明本地事物提交成功,订单创建成功,那么消息提交,否则回滚。

综合上面讲的通用事务消息的实现和 RocketMQ 的事务反查机制,使用 RocketMQ 事务消息功能实现分布式事务的流程如下图:
在这里插入图片描述
总结:
最终的目的是保证 订单生成和清空购物车两件事 要么都做要么都不做
而订单生成是主要逻辑,清空购物车是次要逻辑
先保证订单成功提交,然后由mq的重试机制保证清空购物车一定能够成功完成。

半消息不是过度设计,而是防止本地提交之后马上断电,订单信息没发出去,无法清空购物车

提交订单的优先级高于清空购物车。所以如果本地事物提交了,那么就一定要清空购物车,如果消费失败就重复消费,即使最后也没消费成功也不能回滚订单提交的本地事物、

反查操作不止做一次,有限次反查失败之后,才返回本地事物提交失败,不清空购物车

MQ做了高可用方案,不会存在单个broker挂机就不能工作的情况

自己的一些思考:

!先开启本地事务,然后创建订单,订单创建成功后再发消息,根据发消息是否成功来决定提交还是回滚本地事务。这样不需要事务消息也能解决这个场景的问题,不行吗?
——1、如果本地事务提交失败,已经发出去的消息是无法撤回的,会导致数据不一致。
2、消息发送失败就重复发送,不能因此而回滚本地事物,因为提交订单的优先级高于清空购物车。

!比如订单创建这些都成功了,也发送了消息,但是清空购物车的时候失败了,这个时候我看到做法就是消息重试,但是会不会有一种场景,下游决定上游需要回滚,也就是需要回滚订单创建的事务,这种可能存在吗?怎么处理呢?
——RocketMQ4.6.0提供Request-Reply模式,可解决
——分布式事务补偿,一般代价都蛮大的,不得已再用。

!使用事务消息的话,如果此时mq服务挂了,是否订单就没办法下单了?
——一般MQ的Broker都有高可用方案,不会出现一个节点宕机就无法发消息的问题。所以不用担心这个问题。

!如果流程的第五步消息投递完成,但是执行购物车操作失败了。但是这个时候订单事务已经成功提交了,这种情况购物车数据回滚了,但是订单那边怎么解决呢?
—— 对于这个例子来说,购物车的数据应该以订单的数据为准,只要订单提交了,购物车就应该清空。即使清空购物车失败,也不应该回滚订单数据。所以,购物车清空失败的话,应该反复重试。即使重试不成功也不要去回滚订单。
因为从业务重要性来说,下单的重要程度是高于清空购物车的,下单成功,清空购物车失败,最多是用户看到购物车里还有商品而已,不会影响到用户后续的支付购买。

!2.RocketMq反查时有没有可能本地事务还没提交呢,导致broker取消了事务造成了不一致
——RocketMQ给出的解决方案是,反查的结果返回的状态中,不仅有成功和失败,还有一个“不确定”的状态,意思就是“我现在不知道本地事务是不是成功了,将来它可能会成功,也可能会失败”,这种情况下反查接口都应该返回不确定的状态,RocketMQ在收到这个状态后,会定时多次进行反查,直到得到成功、失败的状态或者事务超时才结束。

总结

在这里插入图片描述
二阶段提交、三阶段提交方法,遵循的是 ACID 原则,
而消息最终一致性方案遵循的就是 BASE 理论。
所以消息最终一致性方案并发度更高,性能更高。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值