分布式事务(二)->柔性事务之可靠消息队列

332 阅读7分钟

《分布式事务->前言》

《分布式事务(一)->分布式事务的缘由和涉及到的问题》

《分布式事务(二)->柔性事务之可靠消息队列》

《分布式事务(三)->柔性事务之TCC事务模型》

在上一篇文章里说道,在分布式环境中,CAP定理中,我们不得不放弃了一致性,也就是说,我们没有办法保证强一致性的存在。

但是分布式环境中,我们必须要考虑到事务的问题,这个时候,我们就不得不考虑柔性事务的可能性了。

本文主要讲述的是柔性事务中的一种方案,通过可靠消息队列,保证事务的最终一致性。

最终一致性的起源

最终一致性的起源来自一篇论文《BASE:An Acid Alternative》,这篇文章中提出了一个理论,叫做BASE理论。

  • BA(Basic Available)基本可用。
  • S(Soft State):柔性状态 同一数据的不同副本的状态,可以不需要实时一致。
  • E(Eventual Consisstency):最终一致性 同一数据的不同副本的状态,可以不需要实时一致,但一定要保证经过一定时间后仍然是一致的。

可靠消息队列即是作者在文章中介绍的其中一种手段。

可靠消息队列如何实现分布式事务?

可靠消息队列实现分布式事务,也经历了很多个阶段,不同的阶段有着不同的使用情况。

也因此,我们要注意的是,在不同的业务场景下,即便是只需要实现最终一致性的情况下,也需要多多思考,最终一致性的方案,应该如何确定。

通过 消息表 来实现最终一致性

首先举例一个事务流程。

  1. 有一个分布式事务操作。
  2. 在事务中有一个操作,分别要访问A服务,B服务,C服务。
  3. A服务是本地服务,B服务和C服务是远程调用的服务。

那么在这个案例中,我们如何实现最终一致性呢?如下所示:

  1. 首先,我们先执行本地服务A,如果执行失败了,就回滚了不再执行操作。
  2. A服务执行成功之后,会在本地数据库中建立一张消息表,里面存入一条消息:“A服务已经执行完成,B服务还没有执行成功,C服务还没有执行成功”。
  3. 在系统中创建一个新的服务D,这个服务D主要是用来轮询A服务中的消息表,如果发现有未执行的操作,就发送mq消息通知。比如说B服务还没有执行成功,那么D服务就会发送一条mq消息给到B服务。
  4. 如果B消费mq消息成功之后,就会发送mq消息通知到A服务(或feign调用也可以),A服务会将消息表里面的记录修改为“B服务已经执行成功”。

在如上的流程中,需要注意的几点是:

  1. 服务D只要发现有哪个服务没有执行成功,那么就会一直发送mq消息给到这个服务,直到服务执行完成。
  2. 如果服务B或者服务C执行成功了,但是通知A服务失败了,那么就会一直消费来自D服务发送的mq消息,这里就要注意B服务和C服务需要保证消息消费的幂等性。
  3. 为了保证事务的可追踪性,以及同一个事务的B服务和C服务多次消费的幂等性,一般都会在消息表里面加一个事务id,D服务在发送mq消息给到B服务和C服务的时候,都会带上这个事务id。
  4. 如果事务始终卡在那里,无法结束,那么就需要人工介入进行补偿操作,补偿的方案需要提前定好。

以上大概就是通过消息表来轮询解决事务一致性的问题,这个方案是属于比较古老的方案了,到现在为止,基本上很少有公司使用这个方案,是因为这个方案有一个很大的缺陷,就是这个方案严重依赖数据库的消息表了,这种依赖,一旦数据库出了问题,那么会导致整个分布式事务的崩溃。

也因此,随着时间的发展,一些其他的方案也替代了这个方案。

通过 事务消息(RocketMq) 实现最终一致性

还是要举例一个事务流程。

  1. 有一个分布式事务操作。
  2. 在事务中有一个操作,分别要访问A服务,B服务,C服务。
  3. A服务是本地服务,B服务和C服务是远程调用的服务。

那么,我们是如何通过RocketMq来实现最终一致性的呢?

  1. 首先A服务会为了B服务以及C服务,会依次向RocketMq发送一个prepare消息,如果其中一个发送失败,就结束事务,并且会自动将已经发送成功的prepare消息回滚。这个消息其实是一个对消费者不可见的消息,需要A服务后续发送一个确认消息,这条消息才能被消费者消费到。
  2. 然后A服务执行本地事务,如果执行失败,那么就回滚,结束事务,并且会自动将发送成功的prepare消息回滚。如果执行成功,A服务会向RocketMq发送两个确认消息,让之前那两条prepare消息被消费者消费到。
  3. RocketMq会轮询自己的prepare消息,并且去A服务确认,这个prepare消息是回滚还是让后续的消费者消费到,这里是为了防止A服务执行本地事务成功后,发送确认消息失败后准备的。
  4. B服务和C服务会消费RocketMq中的消息,如果消费失败,RocketMq会重试。重试的次数可以设置。

总结

如上的两种方案,我选的案例都是一个操作,要在三个服务进行操作,为什么这样操作呢?

我看到网络上很多人的关于可靠消息的案例,都是举例一个操作只有两个服务,我觉得这个不能完全体现分布式事务的真实应用场景。

在实际的开发过程中,如果是如下的例子:

  1. 有一个分布式事务操作。
  2. 在事务中有一个操作,分别要访问A服务,B服务。
  3. A服务是本地服务,B服务是远程调用的服务。

那么我相信大部分人选择的逻辑,就不会加任何分布式事务方案,流程会如下所示:

  1. 先执行本地服务A,如果执行失败了,事务会直接回滚。
  2. 执行本地服务A成功了,那么就直接远程调用服务B,如果执行失败了,事务回滚。
  3. 如果执行远程服务B成功了,事务结束。

在这种实际的开发场景下,压根就不需要通过可靠消息队列来实现分布式事务。

说到这里,如果例子是我之前举的例子:

  1. 有一个分布式事务操作。
  2. 在事务中有一个操作,分别要访问A服务,B服务,C服务。
  3. A服务是本地服务,B服务和C服务是远程调用的服务。

我们也完全可以先执行本地服务A,然后再远程调用服务B,再将C服务使用可靠消息队列来保证分布式事务的一致性。

为什么这样做呢?如果直接远程调用服务B,那么执行失败了,事务可以立马得到回滚,无须等待,以及也无须后面的人工介入过程,减少了对可靠消息队列的依靠性。

当然这个方案也会有一定的问题,可能会因为网络原因导致了调用B服务的失败,但是这种错误是可以接受的,只要保证我们每一个操作的幂等性,那么就不会有脏数据的产生。

当然,如果一个操作是四个服务参与的,那么后面两个服务的操作,也只能通过可靠消息队列来实现了。

这是我在实际开发过程总结出来的。