项目场景

订单系统与各个系统解耦,当订单支付成功,会发送一条消息到MQ,优惠券系统会从Broker机器中获取消息派发优惠券。
优惠券系统对同一条消息重复消费了两次,导致给一个用户重复派发了两个相同的优惠券。

问题原因

假设用户至支付成功后,订单系统收到了支付系统的支付成功通知,接着订单系统向MQ发送一条订单支付成功的消息,但此时,因为外部原因,订单系统处理业务速度过慢导致支付系统和订单系统之间的请求超时了,支付系统就重新请求了订单系统接口再次发送支付成功的通知,导致订单系统再次推送一条消息至MQ,此时MQ就会有两条重复的消息。优惠券系统也就会消费到一个订单的两条重复的支付成功的消息,也就导致给用户重复派发了两个优惠券。

(1) 支付系统 —[通知]—> 订单系统 —[推送消息]—> MQ
(2) 订单系统 —[超时]—> 支付系统 —[再次通知] —> 订单系统 — [再次推送] -> MQ
(3) MQ存在两条重复的消息数据 —[消费] —> 优惠券系统 —[重复派发]—> 用户

重试问题

重试的代码会增加 重发消息至MQ的概率。
场景:订单系统推送消息至MQ,MQ在收到消息之后,因为网络超时原因,响应回订单系统消息过慢。订单系统在收到网络超时的异常后,就会重试再次发送消息至MQ,此时就导致MQ再次收到一条一模一样的消息了。

如何解决消息重发后导致的数据重复问题

幂等性:用来避免对同一个请求或者同一条消息进行重复处理的机制。比如有一个接口,如果有多个相同的请求过来调用这个接口,接口需要保证请求处理的结果是正常的。不能因为重试导致的相同的请求,导致结果数据出现重复。
对于MQ来说,就是消费者从MQ里获取数据时,要能保证同一个消息只能处理一次,不能重复处理多次,导致重复数据的产生。

如何保证幂等性

业务判断法:数据库乐观锁(①版本号字段,②数据是否存在)
基于redis缓存幂等性:成功消费一条数据,就在Redis缓存里写一条数据,标记这个消息已经被消费过。

基于redis缓存实现的幂等性在极端情况下无法保证。比如在消费者消费了消息之后,在还未将消息消费的状态写入redis时,消费者系统崩溃,就导致并未将消息状态写入redis。之后如果消费者系统集群的其他机器在消费消息的时候,并未在redis中查到相关的消息状态,就会认为该条消息并未被消费过,就会再次进行消费。

为什么不在生产者系统到MQ环节中保证消息不重复发送?

生产者系统到MQ环节去检查消息是否重复,需要MQ在自己的数据里面查找是否存在这条消息,这个过程的检查性能并不好,会直接影响接口的性能。同时,消费者系统也可能因为网络超时的原因,对同一条消息进行反复的获取,即使这条消息在MQ内部并没有相同的消息。
所以只需要在消费者系统中对从MQ拉取的消息进行幂等性的判断,就能够解决好MQ消息重发导致的数据问题。

MQ消息幂等性方案总结

对于MQ的重复消息问题,我们往MQ里重复发送一样的消息是可以接受的,因为MQ里有多条重复消息并不会对系统的核心数据直接造成影响,
我们关键是需要保证,在从MQ里获取消息进行消费的时候,必须保证消息不能被重复消费处理。
保证消息的幂等性,优先推荐业务判断法。直接根据数据的存储记录来判断这个消息是否被处理过。处理过就不需要再次处理。基于Redis的消息发送状态的方案,在一些极端情况下是无法保证幂等性的。