造成重复消费的原因:

MQ向消费者推送message,消费者向MQ返回ack,告知所推送的消息消费成功。但是由于网络波动等原因,可能造成消费者向MQ返回的ack丢失。MQ长时间(一分钟)收不到ack,于是会向消费者再次推送该条message,这样就造成了重复消费。

解决重复消费的办法:

用存储(redis或者mysql)记录一下已经消费的message的id,当message被消费前先去存储中查一下消费记录,没有该条message的id则正常消费返回ack,有该条message的id的话不用消费直接返回ack给MQ。

当然实际生产中的话选用redis是比较好的选择,毕竟查mysql要进行磁盘IO,效率要低得多,而且绝大多数重复消费都是由于MQ没有收到消费者的ack于是造成MQ再次向消费者进行同一条message的投递。所以message的消费记录其实我们并不需要一直记录,只需要保存一段时间,当下次投递过来的时候消费者能查到消费记录然后准确返回ack给MQ就行。

yml


#配置rabbitMq 服务器
  rabbitmq:
    host: xxxx
    #rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
    port: xxxx
    username: xxxx
    password: xxxx
    #虚拟host 可以不设置,使用server默认host
    virtual-host: xxxx
    connection-timeout: 0
    #确认消息已发送到队列(Queue)
    publisher-returns: true
    #确认消息已发送到交换机(Exchange)
    publisher-confirm-type: correlated
    # 设置消费端手动 ack
    listener:
      simple:
        retry:
          # 开启消费者(程序出现异常的情况下)进行重试
          enabled: true
          #重试间隔时间
          max-interval: 1000
          # 最大重试次数
          max-attempts: 3
          #开启手动确认消息
          acknowledge-mode: manual


监听类
 


package com.rabbitmqprovider.service;

import com.rabbitmq.client.Channel;
import com.rabbitmqprovider.commons.CommonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.io.IOException;

/***
 * 防止重复消费
 */
@Slf4j
@Service
public class TestBasicService {
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * RabbitListener 可以写在类、方法上
     * @param channel
     * @param message
     * @throws IOException
     */
    //
    @RabbitListener(queues = {CommonUtils.queueStr})
    @RabbitHandler
    public void getMessage(Channel channel, Message message) throws IOException {
        try{
            String messageId= message.getMessageProperties().getMessageId();
            String msg = new String(message.getBody(),"UTF-8");
            //判断messageId在redis中是否存在
            boolean flage=stringRedisTemplate(messageId,msg);
            if(!flage){
                log.error("消息已重复处理失败,拒绝再次接收...");
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
            }else{
                //如果要防止 重复消费,则需要将 id值存在 redis,每次 都要去redis中拿id比对,是否存在,存在则消费过->messageId
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                log.info("接收到的消息{}->"+redisTemplate.opsForValue().get(messageId));
            }
        }catch (Exception e){
            if (message.getMessageProperties().getRedelivered()) {
                log.error("消息已重复处理失败,拒绝再次接收...");
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
            } else {
                log.error("消息即将再次返回队列处理...");
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
        }

    }
    /**
     * 判断Key是否存在
     * @param messageId 唯一表示key
     * @param msg       value值
     * @return
     */
    private boolean stringRedisTemplate(String messageId,String msg){
        log.info("messageId="+messageId);
        //判断Key是否存在 有则返回true,没有则返回false
        if(redisTemplate.hasKey(messageId)){
            return false;
        }else{
            redisTemplate.opsForValue().setIfAbsent(messageId, msg);
        }
        return true;
    }
}


------------------------------------------controller--------------------------------------------------


/**
 * 解决重复消费问题
 */
@GetMapping("/sendMessageTestOnly")
public void sendMessageTestOnly(){
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("message","世界很大!");
    jsonObject.put("msg","你想去看看么?");
    String json = jsonObject.toJSONString();
    String messageId=UUID.randomUUID()+"";
    Message message = MessageBuilder.withBody(json.getBytes())
                                    .setContentType(MessageProperties.CONTENT_TYPE_JSON)
                                    .setContentEncoding("UTF-8")
                                    .setMessageId(messageId).build();
    rabbitTemplate.convertAndSend(CommonUtils.dirExchange,CommonUtils.routingKey,message,new CorrelationData(UUID.randomUUID().toString()));
}


---------------------------------回调------------------------------------------------------


package com.rabbitmqprovider.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * 队列防止消息丢失
 */
@Slf4j
@Component
public class QueueCallback implements RabbitTemplate.ReturnCallback{
    @Override
    public void returnedMessage(Message message,int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息 {} 经交换机 {} 通过routingKey={} 路由到队列失败,失败code为:{}, 失败原因为:{}",
                new String(message.getBody()), exchange, routingKey, replyCode, replyText);
    }
}


package com.rabbitmqprovider.callback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 当消息由生产者发到交换机后会回调该接口中的confirm方法
 */
@Component
@Slf4j
public class ExchangeCallback implements RabbitTemplate.ConfirmCallback{
    /* correlationData 内含消息内容
     * ack 交换机接受成功或者失败。 true表示交换机接受消息成功, false表示交换机接受失败
     * cause 表示失败原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack){
            log.info("交换机收到消息 消息内容为{}->", correlationData);
        }else {
            log.info("交换机未收到消息消息内容为{}, 原因为{}->", correlationData, cause);
        }
    }

}


-----------------------------------------------------------------------------------------------------------------

执行顺序时:先发送消息;然后在接收消息,并判断消息是否重复,如果不重复 则回复消息,否则 拒绝回复;最后回调。