系列目录:
一、常见分布式事务解决方案
- 两阶段提交(2PC,Two-phase Commit)
- TCC补偿模式
- 基于本地消息表实现最终一致性
- 基于可靠消息最终一致方案
- 最大努力通知
一、基于本地消息表实现最终一致
基于本地消息表实现数据一致性的方案最初是由eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定势任务将消息发送到消息中间件,待确认消息发送给消费方成功再将定时任务的消息删除。这种方式的主要逻辑就是通过定势任务一致发送消息,直到成功为止。其优缺点也就一目了然了,最大的问题也就是在定时任务上面。所以其适用于一些必须通知到位的场景。如果在有可能不需要通知到位(比如发短信)这种,有可能造成定时任务的浪费和性能瓶颈。
下面以注册懂积分为例来说明这种方式的用法:
下例公有两个微服务,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
交互流程如下:
- 1、用户注册
用户服务在本地事务新增用户和增加积分消息日志。 - 2、定时任务扫描日志
经过第一步消息已经写到消息日志表中,可以地洞独立的线程,定时对消息日志表中的消息进行扫描并发送至中间件,在消息中间件反馈发送成功后删除该日志,否则等待定势任务下一周期重试。 - 3、消费消息
如何保证消费者一定能消费到消息呢?
可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接受到消息并且业务处理完成后向MQ发送ack,此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则会不断重试向消费者发送消息。
由于消息会重复投递,积分服务的增加积分功能需要实现幂等性
总结:
上述的方法是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存在瓶颈。频繁的读写消息会给数据库造成压力,在真正的高并发场景下,该方案也会有瓶颈和限制
二、基于可靠消息最终一致方案
在上面基于本地消息的方案中,是由一个定时任务查询需要发送的消息,同时接受执行完成的消息,来实现数据的最终一致的,其主要的问题是在定时任务上,如果存在一个可靠的MQ,能够解决消息的发送端和本地事务的原子性,这样就不需要利用定时任务来重复的发送消息了。目前能满足这个功能的MQ常用的就是RocketMQ
RocketMQ是一个来自阿里巴巴的分布式消息中间件,于2012年开源,并在2017年正式成为Apache顶级项目。据了解,包括阿里云上的消息产品以及收购的子公司在内,阿里集团的消息产品全线都运行在RocketMQ上,并且最近几年的双十一大促中,RocketMQ都有十分优秀的表现。Apache RocketMQ 4.3以后得版本正式支持事务消息,为分布式事务实现提供便利性支持。
RocketMQ事务消息设计则主要是为了解决Producer端的消息发送与本地事务执行的原子性问题。RocketMQ的设计中broker与producer端的双向通信能力,使的broker天生可以作为一个事务协调者存在;而RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ的高可用机制以及可靠的消息设计为事务消息在系统议程发生时依然能够保证达成事务的最终一致性。
在RocketMQ4.3后实现了完整的事务消息,其实是对本地消息表的一个封装,将本地消息表移动到了MQ的内部,解决Producer端的消息发送与本地事务执行的原子性问题。
为方便我们理解,我们以注册送积分的例子来描述整个过程。
Producer即MQ的发送方,本例中是用户服务,负责新增用户,MQ订阅方是消息消费方,本例中是积分服务,负责新增积分。其流程如下:
- 1、Producer发送事务消息
Producer发送事务消息到MQ Server,MQ Server将消息状态标记为Prepared(预览状态),注意此时这条消息的消费方(MQ订阅方)是无法消费到的。 - 2、MQ Server回应消息发送成功
MQ Server接收到Producer发送给的消息则回应发送成功表示MQ已经接收到消息。 - 3、Producer执行本地事务
Producer端执行业务代码逻辑,通过本地数据库控制事务,本例中,就是添加用户操作。 - 4、消息投递
若Producer本地事务执行成功则自动向MQ Server发送commit消息,MQ Server接收到commit消息后将“增加积分消息”状态标记为可消费,此时MQ订阅方即正常消费消息。
若Producer本地事务执行失败则自动向MQ Server发送rollback消息,MQ Server接收到rollback消息后删除“增加积分消息”
MQ订阅方消费消息,消费成功则向MQ回应ack,否则经重复接受消息。这里ack默认自动回应,即程序执行正常则自动回应ack。 - 5、事务回查
如果执行Producer端本地事务的过程中,执行端挂掉,或者超时,MQ Server将会不停地询问同组的其他Producer来获取事务执行状态,这个过程就叫做事务回查。MQ Server 会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对于用户来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需要关注本地事务的执行状态即可。
三、基于可靠消息最终一致方案在库存扣减场景中的应用
从上面的方案中可以看出,基于可靠消息的最终一致性方案,是解决了本地事务到消息中间件发送消息的一致性问题,但是没有解决消费到消息后是否能够成功的问题。也就是说,这种方案适用于MQ的订阅方一定会执行成功或者就算执行不成功,发送方也不需要感知的场景,但是对于放松法需要感知订阅方是否会执行成功的情况,这种方案还是有问题的。
比如创建订单的场景,创建订单的服务是要知道库存扣减的服务是否能够扣减成功的,主要是要知道库存是否充足,如果不充足,那创建订单是不能成功的,那这种场景要如何利用基于可靠消息的最终一致的方案呢?
其实我们需要换个角度考虑问题,就是创建订单的服务正常调用库存扣减的服务,如果库存不够,就直接创建失败,这样的话就剩下了如何解决库存扣减成功,但是创建订单业务失败后回滚库存的问题,回滚库存就是增加库存,这个订单创建服务是不在乎的,所以创建订单的服务在库存扣减成功后,就给MQ发送一个回滚库存的事物消息,如果订单创建成功,这个消息就不发送,如果订单创建失败,这个消息就发送,库存服务收到回滚库存的消息,就直接回滚库存,这个问题就解决了。
所以在遇到这种正面无法解决的问题,我们可以试着用逆向思维考虑一下,也许就能解决这种问题。
后记
个人总结,欢迎转载、评论、批评指正