一.什么是MQ中的消息重复?
消费者在消费MQ的时候,可能会多次接收到同一条消息。
消息发送接受的流程大概如下:
第一类原因:消息发送端应用的消息重复发送
生产者发送消息到MQ的时候出现重复,有以下几种情况(图中1、2、3步):
1.消息发送端发送消息给消息中间件,消息中间件收到消息并成功存储,而这时消息中间件出现了问题,导致应用端没有收到消息发送成功的相应而进行重试产生了重复(生产者做了消息确认操作,但是MQ出现了问题,导致生成者没有收到消息发送成功的响应,导致生产者会进行消息发送重试,这样就会导致消息重发);
2.消息中间件因为负载高响应变慢,成功把消息存储到消息存储中后,返回“成功”这个结果时超时(消息原本是被正常存储了,但是在网络通信时,有一个超时时间,响应变慢导致超过了超时时间,所以生产者也没有收到成功发送的响应,导致消息重发);
3.消息中间件将消息成功写入消息存储,在返回结果时网络出现问题,导致应用发送端重试,而重试时网络恢复,由此导致重复;
总结:
上诉三点可以了解到,通过消息发送端产生消息重复的主要原因是消息成功进入消息存储后,因为各种原因使得消息发送端没有收到“成功”的返回结果,并且又有重试机制,因而导致重复。
第二类原因:消息到达了消息存储,由消息中间件进行向外的投递时产生重复
MQ发送消息给消费者的时候出现重复,有以下几种情况(图中4步):
1.消息被投递到消息接收者应用进行处理,处理完毕后应用出问题了,消息中间件不知道消息处理结果,会再次投递(因为出问题后没有给MQ发送收到消息确认信息,MQ就会认为没有收到消息,该消息不会被删除,会进行重新发送);
2.消息被投递到消息接收者应用进行处理,处理完毕后网络出现问题了,消息中间件没有收到消息处理结果,会再次投递(本身消费者是发了收到消息确认信息的,由于网络因素导致MQ没有收到,MQ也会认为你没有收到该消息,也是会进行重新发送);
3.消息被投递到消息接收者应用进行处理,处理时间比较长,消息中间件因为消息超时会再次投
递(超时会导致MQ认为数据没有收到);
4.消息被投递到消息接收者应用进行处理,处理完毕后消息中间件出问题了,没能收到消息结果并处理,会再次投递(消息投递完以后,在未收到响应前宕机了,所以重启后会重发发送一条MQ);
5.消息被投递到消息接收者应用进行处理,处理完毕后消息中间件收到结果但是遇到消息存储故障,没能更新投递状态,会再次投递。
总结:
可以看到,在投递过程中产生的消息重复接收主要是因为消息接收者成功处理完消息后,消息中间
件不能及时更新投递状态造成的。
二.如何解决MQ中的消息重复?
主要是要求消息接收者来处理这种重复的情况,也就是要求消息接收者的消息处理是幂等操作。
什么是幂等性?
对于消息接收端的情况,幂等的含义是采用同样的输入多次调用处理函数,得到同样的结果。
例如,一个 SQL 操作update stat table set count= 10 where id =1这个操作多次执行,id 等于1的记录中的 count 字段的值都为10,这个操作就是幂等的,我们不用担心这个操作被重复操作导致结果发生变化。
再来看另外一个 SQL 操作 update stat table set count= count +1 where id= 1;
这样的 SQL 操作就不是幂等的,一旦操作重复,结果就会产生变化。
因此应对消息重复的办法是,使消息接收端的处理是一个幂等操作,这样的做法降低了消息中间件的整体复杂性,不过也给使用消息中间件的消息接收端应用带来了一定的限制和门槛。
常见办法:
1.MVCC
多版本并发控制,乐观锁的一种实现,在生产者发送消息时、进行数据更新时需要带上数据的版本号,消费者去更新时需要去比较持有数据的版本号,版本号不一致的操作无法成功。
例如博客点赞次数自动 +1 的接口:public boolean addCount(Long id, Long version);update blogTable set count= count+1,version= version+1 where id=321 and version=123,每一个 version 只有一次执行成功的机会,一旦失败了生产者必须重新获取数据的最新版本号再次发起更新。(但是这种对于业务的开始不是很友好,这生产者发送消息的时候就需要把版本好加上,而且还需要在数据表中加字段,每次修改还需要去修改对应的版本好,所以MVCC看起来处理起来比价完美,但是处理门槛比较高)
2.去重表
利用数据库表单的特性来实现幂等,常用的一个思路是在表上构建唯一性索引,保证某一类数据一旦执行完毕,后续同样的请求不再重复处理了(利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息)。
以电商平台为例子,电商平台上的订单 id 就是最适合的 token,当用户下单时,会经历多个环节,比如生成订单,减库存,减优惠券等等,每一个环节执行时都先检测一下该订单 id 是否已经执行过这一步骤,对未执行的请求,执行操作并缓存结果,而对已经执行过的id,则直接返回之前的执行结果,不做任何操作,这样可以在最大程度上避免操作的重复执行问题,缓存起来的执行结果也能用于事务的控制等。
从MQ中的数据A第一次从消息队列中发送出来,业务进行插入,肯定是成功的,此时再去执行会员系统、商品系统逻辑,数据A的重复数据再次进来,由于中间有一个去重表,所以同样的数据就会插入失败,此时可以通过异常捕获,直接返回就行,就不需要再做其他的处理了。
而且这种方式的优势是适合分布式系统,因为这种去重表,可以设计在会员系统的数据库,也可以设计在商品系统的数据库,这个根据业务来判断去重是为了防止什么业务逻辑的出错来设计在不同的数据库中,这对业务的侵入性很小,只需要在以前的逻辑上加上去重表的插入和去重表的异常处理的代码即可完成,通常可以添加一个模块或者是通过AOP的拦击进行处理,所以对原本的业务改造是相当的方便。