分布式事务解决方案——可靠消息最终一致性(本地消息表、MQ自身提供的可靠性保证)
什么是可靠消息最终一致性
指的是当事务发起方执行完成本地事务后并发送一条消息,事务参与方(消息的消费者)一定可以收到该消息,并完成接下来的业务处理,保证事务的成功。在此方案中,强调的是最终一致性,这就要保证发送的消息是一定能被事务参与者接收到的。
完成消息通知是需要使用消息中间件:MQ
- 事务发起方执行业务开启事务,发送消息到消息中间件
- 事务参与者监听消息中间件的队列,从队列中获取消息
- 在消息的传递过程中,消息是必须可靠的,必须能被事务参与者获取到的,这就是消息的可靠性
- 在事务的参与者接收到消息后,一定能保证业务的正常处理,这就是最终一致性
常见的MQ有RabbtiMQ、RocketMQ、Kafka、ActiveMQ等,其中消息可靠性最高的是RocketMQ,消息延迟最低的是RabbitMQ。综合其他因素所以在我看来最适用于消息可靠性保证的是RocketMQ,延迟较低的RabbitMQ适合相对复杂一些的消息补偿机制。两种方式都是为了达到最终一致性。
RabbitMQ的相关知识参阅我的专栏:RabbitMQ
消息补偿机制可以参阅我的另一篇博文:RabbitMQ应用问题——消息补偿机制以及幂等性的保证简单介绍
只要是网络请求,都会遇到可能不确定的意外因素,所以在该方案中要解决如下问题:
- 本地事务与消息发送的原子性问题
- 事务参与者接收消息的可靠性
- 消息重复消费的幂等性
本地事务与消息发送的原子性问题
该问题是指:事务的发起方在本地事务执行成功后发送消息也必须是成功的,保证本地事务和消息的发送,要么一起成功,要么一起失败。
- 如果先发消息,再操作数据库,数据库是有可能操作失败的,但是消息发送成功了,不能保证两者的原子性
- 如果先操作数据库,再发送消息,消息的发送超时抛出异常,导致数据库事务回滚,但是消息可能最终发送成功了,同样会造成不一致的情况。
这里不像传统数据库操作事务中,可以通过本地事务进行回滚那么简单,是需要一些业务机制来弥补的。
事务参与者接收消息的可靠性
事务的参与者,也就是消息的消费者,必须能够从消息中间件中获取到消息并执行业务成功,否则也是不能达到最终的一致性,在接收消息这里,一定需要消息的重发机制,来保证意外情况下事务的参与者某次接收消息失败后能够最终接收到消息。
消息重复消费的幂等性
由于可靠消息最终一致性方案中存在消息重发的机制,是有可能造成同一条消息的重复消费的,所以就要保证重复消费同一条业务消息的幂等性:即多次消费同一条消息产生的结果是一致的。
解决方案1:本地消息表方案
还是以新用户注册送积分为例,如上图所示。
- 用户服务在用户注册完成后,也就是用户信息插入本地表用户表
- 同时向本地的一张增加积分的消息日志表增加一条日志,用来记录要给哪些用户增加积分,该表的每一条数据都会有一个状态字段,用来标识消息是否发送成功
- 有一个定时任务来扫描这张增加积分的消息日志表,扫描到没有发送成功的日志,就会像MQ发送消息,供积分服务消费。即使MQ出问题了,也没有问题,需要发送消息的日志在本地表会一直被定时任务扫描发送,待MQ恢复正常后继续发送消息,这就保证了新增用户和发送消息的原子性。
- 积分服务从MQ消费消息的时候,是需要实现MQ的ack机制(很多MQ都有ack,如RabbitMQ高级特性——超详细说明Consumer Ack(消费者端消息确认,附代码)),消费者接收到消息并且业务处理完成后,需要发送ack到MQ,告诉MQ我已经处理好了,此时该消息才是正式的消费成功,MQ停止向积分服务发送消息,否则MQ会一致向积分服务发送该条消息。
- 当MQ多次向积分服务发送消息的时候,要保证幂等性,如使用乐观锁机制
解决方案2:MQ中间件的消息可靠性保证
通常消息中间件都会提供响应的保证可靠性的机制,以及相关的开放接口,供开发者使用。
-
RabbitMQ
- 消息的可靠性投递
- confirm模式
- return模式
- consumer ACK
- 消息的持久化
- 消息的可靠性投递
-
RocketMQ
- 事务消息
- 生产者发消息的回执判断
- consumer ACK
- 消息的持久化