大纲
1.全链路分析为什么用户支付完成后却没有收到红包
2.RocketMQ的事务消息机制实现发送消息零丢失
3.RocketMQ事务消息机制的底层实现原理
4.是否可以通过同步重试方案来代替事务消息方案来实现发送消息零丢失
5.使用RocketMQ事务消息的代码案例细节
6.同步刷盘+Raft协议同步实现发送消息零丢失
7.手动提交offset+自动故障转移的消费消息零丢失
8.基于RocketMQ全链路的消息零丢失方案总结
1.全链路分析为什么用户支付完成后却没有收到红包
(1)客服反馈用户支付后没收到红包
(2)订单系统推送消息到RocketMQ可能会丢失消息
(3)消息到达RocketMQ后也可能会丢失消息
(4)就算消息进入磁盘了也不是万无一失
(5)即使红包系统获取到消息也可能会丢失
(6)用户支付完后红包没发送出去的原因汇总
(1)客服反馈用户支付后没收到红包
有用户反馈,按照规则,在支付成功后应该是可以拿到一个现金红包的,但是他在支付完一个订单后,却没有收到这个现金红包,于是就反馈给了客服。
技术团队经过排查,找了系统中打印的很多日志,发现一个奇怪的现象。正常情况下,订单系统在完成支付后,会推送一条消息到RocketMQ,然后红包系统会从RocketMQ中消费到这条消息去给用户发送现金红包,如下图示。
但是从订单系统和红包系统当天那个时间段的日志来看,居然只看到订单系统推送消息到RocketMQ的日志,却没看到红包系统从RocketMQ中消费消息以及发现金红包的日志。问题可能就出在这里,订单支付消息在传输过程中丢失了,导致现金红包没有派发出去。所以接下来需要分析,在使用RocketMQ的过程中,到底有哪些地方会导致消息丢失。
(2)订单系统推送消息到RocketMQ可能会丢失消息
订单系统在接收到订单支付成功的回调后,会推送一条订单支付成功的消息到RocketMQ。在这个过程中,是有可能会出现丢失消息的情况的。因为订单系统在推送消息到RocketMQ时是通过网络去进行传输的,如果网络发生了抖动,就会导致网络通信失败,于是可能某条消息就没有成功投递给RocketMQ。所以订单系统投递消息到RocketMQ可能会因为网络问题而导致失败。
除此之外,还有其他的原因可能会导致订单系统推送消息到RocketMQ失败。比如RocketMQ确实收到消息了,但是它网络通信模块的代码出现了异常,导致消息没成功处理。比如生产者在写消息到RocketMQ的过程中,刚好遇到了某个Leader Broker故障,其他的Follower Broker正在尝试切换为Leader Broker,这个过程中也可能会有异常。
所以我们在使用任何一个MQ时,无论是RocketMQ、还是RabbitMQ、或者是Kafka,首先都要明确一点:不一定发送消息出去就一定会成功、也有可能会失败,此时代码里可能会抛出异常、也有可能不会抛异常。
(3)消息到达RocketMQ后也可能会丢失消息
即使订单系统成功把消息写入了RocketMQ,那么消息也有可能出现丢失。因为根据RocketMQ的底层原理可以知道:消息写入RocketMQ后,RocketMQ可能仅仅是把这个消息写入到PageCache里,也就是OS管理的一个缓冲区,这本质也属于内存。
当生产者认为已经向RocketMQ成功写入了一条消息,但实际上该消息可能只是仅仅进入OS的PageCache,还没刷入磁盘。此时如果Broker机器宕机,那么OS的PageCache中的数据也就丢失了。
所以根据RocketMQ底层原理,消息数据在进入OS的PageCache时,如果碰上机器宕机,那么内存里的数据必然会丢失。此后机器即便重启了,并启动好Broker进程,那么这条消息数据也没了。
(4)就算消息进入磁盘了也不是万无一失
当Broker把消息写入了OS的PageCache,操作系统会在一段时间后把消息从内存中刷入磁盘文件里。
但是即便写入RocketMQ的一条消息已经进入Broker机器的磁盘文件里了,那么这条消息也是有可能会丢失的。因为如果Broker机器的磁盘出现了故障,比如磁盘坏了,那么上面存储的数据就可能会丢失。
(5)即使红包系统获取到消息也可能会丢失
即使红包系统顺利从RocketMQ里获取到一条消息,那么它也不一定能把现金红包发出去。要解释这种情况,需要了解消息offset的概念。
offset表示的是消息的位置,代表了消息的标识。如下图示,假设有两条消息,offset分别为1和2。现在红包系统已经获取到了消息1,然后消息1此时就在红包系统的内存里,正准备运行代码去派发现金红包(还没派发)。
由于默认情况下,RocketMQ的消费者会自动提交已经消费的offset。如果此时红包系统在还没处理完根据消息1派发红包的情况下,提交了消息1的offset到Broker,标识已成功处理了消息1。接着恰好红包系统突然重启或者宕机、或者在派发红包时更新数据库失败了。那么此时内存里的消息1必然会丢失,而且红包也还没发出去。
(6)用户支付完后红包没发送出去的原因汇总
原因一:订单系统推送消息到RocketMQ失败了,消息没有被成功发送到RocketMQ上
原因二:消息确实推送到RocketMQ了,但是结果Broker机器故障,把消息弄丢了
原因三:红包系统拿到了消息,但是在红包还没发完的时候把消息弄丢了
如果订单系统推送了消息,结果红包系统连消息都没收到,那么可能消息根本就没发到RocketMQ,或者RocketMQ弄丢了消息。如果红包系统收到了消息,结果红包没派发,那么就是红包系统弄丢了消息。
2.RocketMQ的事务消息机制实现发送消息零丢失
(1)解决消息丢失的第一个问题:推送消息时丢失
(2)事务消息机制之发送half消息试探是否正常
(3)事务消息机制之如果half消息写入失败
(5)事务消息机制之如果本地事务执行失败
(6)事务消息机制之如果本地事务执行成功
(7)事务消息机制之没收到half消息成功的响应
(8)事务消息机制之rollback或commit发送失败
(9)RocketMQ事务消息的全流程总结
(1)解决消息丢失的第一个问题:推送消息时丢失
首先要解决的第一个问题,就是在订单系统推送消息到RocketMQ的过程中,可能会因为常见的网络故障等问题导致推送消息失败。
在RocketMQ中,有一个事务消息机制。凭借这个事务消息机制,就可以确保订单系统推送的消息一定会成功写入RocketMQ,不会出现推送消息失败。
(2)事务消息机制之发送half消息试探是否正常
订单系统收到一个订单支付成功的回调后,需要在自己的订单数据库里做一些增删改操作,比如更新订单状态等。然后发一条消息到RocketMQ,让其他关注这个订单支付成功消息的系统可以从RocketMQ获取消息做对应的处理。
在基于RocketMQ的事务消息机制中,首先会让订单系统发送一条half消息到RocketMQ中。这个half消息本质就是一个订单支付成功的消息,只不过该消息状态是half状态,此时红包系统是看不见该half消息的。然后订单系统会等待接收这个half消息写入成功的响应通知。
可以想象一下,假设订单系统在收到订单支付成功的通知后,直接进行本地的数据库操作,比如更新订单状态为已完成,然后再发送消息给RocketMQ,结果才发现RocketMQ挂了报异常。这时就会导致没法通过消息通知到红包系统去派发红包,那用户一定会发现自己订单支付了,结果红包没收到。
所以订单系统在收到订单支付成功的通知后做的第一件事,不是先让订单系统做一些增删改操作,而是先发一个half消息给RocketMQ以及等待它的成功响应,也就是初步和RocketMQ建立联系和沟通,从而让订单系统确认RocketMQ还活着,RocketMQ也会知道订单系统后续可能想发送一条很关键的不希望丢失的消息给它。
(3)事务消息机制之如果half消息写入失败
如果订单系统发送half消息给RocketMQ失败了,可能因为订单系统报错了、可能因为RocketMQ挂了、或者网络故障了等原因导致half消息都没发送成功,此时订单系统应该执行一系列的回滚操作,比如对订单状态做一个更新,让状态变成"关闭交易",同时通知支付系统自动进行退款。
因为订单虽然支付了,但是包括派发红包、发送优惠券之类的后续操作无法执行,所以此时需要把钱款退还给用户,表示交易失败了。
(4)事务消息机制之如果half消息写入成功
如果订单系统发送half消息给RocketMQ成功了,此时订单系统就应该在自己本地的数据库里执行一些增删改操作。因为一旦half消息写成功,就说明RocketMQ肯定已经收到了这条消息、RocketMQ还活着而且目前生产者可以跟RocketMQ正常沟通,所以订单系统下一步应该执行自己的增删改操作。
(5)事务消息机制之如果本地事务执行失败
如果订单系统更新数据库失败了,比如数据库也出现网络异常、或者数据库挂了,那么订单系统就无法把订单更新为"已完成"这个状态。
此时应该让订单系统发送一个rollback请求给RocketMQ,意思是RocketMQ可以把订单系统之前发过来的half消息删除掉。
订单系统发送rollback请求给RocketMQ删除half消息后,订单系统就必须执行回退流程,通知支付系统退款。当然回退流程中可能还要考虑订单系统自己的高可用降级机制:比如数据库无法更新时,订单系统需要在机器本地磁盘文件里写入订单支付失败的记录,然后订单系统会启动一个后台线程在MySQL数据库恢复后再把订单状态更新为"已关闭"。
(6)事务消息机制之如果本地事务执行成功
如果订单系统成功完成了本地事务的操作,比如把订单状态都更新为"已完成",那么订单系统就可以发送一个commit请求给RocketMQ,要求RocketMQ对之前的half消息进行commit操作,让红包系统可以看见这个订单支付成功消息。
所谓的half消息实际就是订单支付成功的消息,只不过它的状态是half。也就是当消息的状态是half状态时,红包系统是看不见该消息的,没法获取到该消息。必须等到订单系统执行commit请求,该half消息被commit后,红包系统才可以看到和获取该消息进行后续处理。
(7)事务消息机制之没收到half消息成功的响应
如果订单系统把half消息发送给RocketMQ了,RocketMQ也把half消息保存下来了,但是订单系统却没能收到RocketMQ返回的响应,那么此时会发生什么事情?
订单系统没收到响应,可能是由于网络超时问题,也可能是由于其他的异常问题。如果订单系统没能收到RocketMQ返回half消息成功的响应,那么就会误以为发送half消息到RocketMQ失败了,从而执行退款流程,订单状态也会被标记为"已关闭"。