我们以订单创建为例,订单系统先创建订单(本地事务),再发送消息给下游处理;如果订单创建成功,然而消息没有发送出去,那么下游所有系统都无法感知到这个事件,会出现脏数据;
public void processOrder() {
// 订单处理(业务操作)
orderService.process();
// 发送订单处理成功消息(发送消息)
sendBizMsg ();
}
如果先发送订单消息,再创建订单;那么就有可能消息发送成功,但是在订单创建的时候却失败了,此时下游系统却认为这个订单已经创建,也会出现脏数据。
public void processOrder() {
// 发送订单处理成功消息(发送消息)
sendBizMsg ();
// 订单处理(业务操作)
orderService.process();
}
此时可能会想,我们可否将消息发送和业务处理放在同一个本地事务中来进行处理,如果业务消息发送失败,那么本地事务就回滚,这样是不是就能解决消息发送的一致性问题呢?
@Transactionnal
public void processOrder() {
try{
// 订单处理(业务操作)
orderService.process();
// 发送订单处理成功消息(发送消息)
sendBizMsg ();
}catch(Exception e){
事务回滚;
}
}
来分析下这种消息发送的异常情况
可能的情况 |
一致性 |
订单处理成功,然后突然宕机,事务未提交,消息没有发送出去 |
一致 |
订单处理成功,由于网络原因或者MQ宕机,消息没有发送出去,事务回滚 |
一致 |
订单处理成功,消息发送成功,但是MQ由于其他原因,导致消息存储失败,事务回滚 |
一致 |
订单处理成功,消息存储成功,但是MQ处理超时,从而ACK确认失败,导致发送方本地事务回滚 |
不一致 |
从上面的情况分析,我们可以看到,使用普通的处理方式,无论如何,都无法保证业务处理与消息发送两边的一致性,其根本的原因就在于:远程调用,结果最终可能为成功、失败、超时;而对于超时的情况,处理方最终的结果可能是成功,也可能是失败,调用方是无法知晓的。 为了保证两边数据的一致性,我们只能从其他地方寻找新的突破口。
由于传统的处理方式无法解决消息生成者本地事务处理成功与消息发送成功两者的一致性问题,因此事务消息就诞生了,它实现了消息生成者本地事务与消息发送的原子性,保证了消息生成者本地事务处理成功与消息发送成功的最终一致性问题。
https://help.aliyun.com/document_detail/29548.html?spm=a2c4g.11186623.2.16.2204500aVkjVW2
注意点:由于MQ通常都会保证消息能够投递成功,因此,如果业务没有及时返回ACK结果,那么就有可能造成MQ的重复消息投递问题。因此,对于消息最终一致性的方案,消息的消费者必须要对消息的消费支持幂等,不能造成同一条消息的重复消费的情况。
异常情况 |
一致性 |
处理异常方法 |
消息未存储,业务操作未执行 |
一致 |
无 |
存储待发送消息成功,但是ACK失败,导致业务未执行(可能是MQ处理超时、网络抖动等原因) |
不一致 |
MQ确认业务操作结果,处理消息(删除消息) |
存储待发送消息成功,ACK成功,业务执行(可能成功也可能失败),但是MQ没有收到生产者业务处理的最终结果 |
不一致 |
MQ确认业务操作结果,处理消息(根据就业务处理结果,更新消息状态,如果业务执行成功,则投递消息,失败则删除消息) |
业务处理成功,并且发送结果给MQ,但是MQ更新消息失败,导致消息状态依旧为待发送 |
不一致 |
同上 |
上面我们也分析了事务消息所存在的异常情况,即MQ存储了待发送的消息,但是MQ无法感知到上游处理的最终结果。对于RocketMQ而言,它的解决方案非常的简单,就是其内部实现会有一个定时任务,去轮训状态为待发送的消息,然后给producer发送check请求,而producer必须实现一个check监听器,监听器的内容通常就是去检查与之对应的本地事务是否成功(一般就是查询DB),如果成功了,则MQ会将消息设置为可发送,否则就删除消息。
实现简单
现在目前较为主流的MQ,比如ActiveMQ、RabbitMQ、Kafka、RocketMQ等,只有RocketMQ支持事务消息。因此,如果我们希望强依赖一个MQ的事务消息来做到消息最终一致性的话,在目前的情况下,技术选型上只能去选择RocketMQ来解决。
问:如果预发送消息失败,是不是业务就不执行了?
问:为什么要增加一个消息预发送机制,增加两次发布出去消息的重试机制,为什么不在业务成功之后,发送失败的话使用一次重试机制?
如果consumer消费失败,是否需要producer做回滚呢?
如果consumer端因为业务异常而导致回滚,那么岂不是两边最终无法保证一致性?
基于本地消息的最终一致性方案的最核心做法就是在消费者发送消息给MQ的时候,记录一条消息数据到DB,并且消息数据的发送、记录与业务的处理必须在同一个事务内完成(或者是镶嵌关系,内部异常抛出影响外部回滚),这是该方案的前提核心保障。它与普通消息处理流程的差异在于将发送的消息记录在了数据库,记录在数据库的消息数据同时记录发送状态(成功/失败),消费状态(成功/失败)。
异常情况 |
一致性 |
处理异常方法 |
业务处理失败/消息存储DB失败/消息发送失败,数据回滚 |
一致 |
无 |
消息发送成功。MQ存储数据成功,返回ACK失败(可能是MQ处理超时、网络抖动等原因) |
不一致 |
业务处理回滚,消费者消费消息时查询DB记录的发送状态,如果该条消息发送状态为失败,则不进行消费,设置重试时间;如果该条消息不存在,则说明消息在生产者方已回滚,消费成功不进行业务处理 |
对MQ选型没有强依赖性,实现较为简单
性能瓶颈集中在了DB/Redis或其他本地存储消息的数据库上
独立消息服务最终一致性与本地消息服务最终一致性最大的差异就在于将消息的存储单独地做成了一个RPC的服务,这个过程其实就是模拟了事务消息的消息预发送过程,如果预发送消息失败,那么生产者业务就不会去执行,因此对于生产者的业务而言,它是强依赖于该消息服务的。不过好在独立消息服务支持水平扩容,因此只要部署多台,做成集群模式,就能够保证其可靠性。在消息服务中,还有一个单独地定时任务,它会定期轮训长时间处于待发送状态的消息,通过一个check补偿机制来确认该消息对应的业务是否成功,如果对应的业务处理成功,则将消息修改为可发送,然后将其投递给MQ;如果业务处理失败,则将对应的消息更新或者删除即可。因此在使用该方案时,消息生产者必须同时实现一个check服务,来供消息服务做消息的确认。对于消息的消费,该方案与上面的处理是一样,都是通过MQ自身的重发机制来保证消息被消费。
对MQ选型无强依赖性,也没有较大的性能瓶颈
维护和开发成本高