《RabbitMQ》如何保證訊息不被重複消費

Java旅途發表於2020-08-06

一 重複訊息

為什麼會出現訊息重複?訊息重複的原因有兩個:1.生產時訊息重複,2.消費時訊息重複。

1.1 生產時訊息重複

由於生產者傳送訊息給MQ,在MQ確認的時候出現了網路波動,生產者沒有收到確認,實際上MQ已經接收到了訊息。這時候生產者就會重新傳送一遍這條訊息。

生產者中如果訊息未被確認,或確認失敗,我們可以使用定時任務+(redis/db)來進行訊息重試。

@Component
@Slf4J
public class SendMessage {
    @Autowired
    private MessageService messageService;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 最大投遞次數
    private static final int MAX_TRY_COUNT = 3;

    /**
     * 每30s拉取投遞失敗的訊息, 重新投遞
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void resend() {
        log.info("開始執行定時任務(重新投遞訊息)");

        List<MsgLog> msgLogs = messageService.selectTimeoutMsg();
        msgLogs.forEach(msgLog -> {
            String msgId = msgLog.getMsgId();
            if (msgLog.getTryCount() >= MAX_TRY_COUNT) {
                messageService.updateStatus(msgId, Constant.MsgLogStatus.DELIVER_FAIL);
                log.info("超過最大重試次數, 訊息投遞失敗, msgId: {}", msgId);
            } else {
                messageService.updateTryCount(msgId, msgLog.getNextTryTime());// 投遞次數+1

                CorrelationData correlationData = new CorrelationData(msgId);
                rabbitTemplate.convertAndSend(msgLog.getExchange(), msgLog.getRoutingKey(), MessageHelper.objToMsg(msgLog.getMsg()), correlationData);// 重新投遞

                log.info("第 " + (msgLog.getTryCount() + 1) + " 次重新投遞訊息");
            }
        });

        log.info("定時任務執行結束(重新投遞訊息)");
    }
}

1.2消費時訊息重複

消費者消費成功後,再給MQ確認的時候出現了網路波動,MQ沒有接收到確認,為了保證訊息被消費,MQ就會繼續給消費者投遞之前的訊息。這時候消費者就接收到了兩條一樣的訊息。

修改消費者,模擬異常

@RabbitListener(queuesToDeclare = @Queue(value = "javatrip", durable = "true"))
public void receive(String message, @Headers Map<String,Object> headers, Channel channel) throws Exception{

    System.out.println("重試"+System.currentTimeMillis());
    System.out.println(message);
    int i = 1 / 0;
}

配置yml重試策略

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 開啟消費者進行重試
          max-attempts: 5 # 最大重試次數
          initial-interval: 3000 # 重試時間間隔

由於重複訊息是由於網路原因造成的,因此不可避免重複訊息。但是我們需要保證訊息的冪等性

二 如何保證訊息冪等性

讓每個訊息攜帶一個全域性的唯一ID,即可保證訊息的冪等性,具體消費過程為:

  1. 消費者獲取到訊息後先根據id去查詢redis/db是否存在該訊息
  2. 如果不存在,則正常消費,消費完畢後寫入redis/db
  3. 如果存在,則證明訊息被消費過,直接丟棄。

生產者

@PostMapping("/send")
public void sendMessage(){

    JSONObject jsonObject = new JSONObject();
    jsonObject.put("message","Java旅途");
    String json = jsonObject.toJSONString();
    Message message = MessageBuilder.withBody(json.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("UTF-8").setMessageId(UUID.randomUUID()+"").build();
    amqpTemplate.convertAndSend("javatrip",message);
}

消費者

@Component
@RabbitListener(queuesToDeclare = @Queue(value = "javatrip", durable = "true"))
public class Consumer {

    @RabbitHandler
    public void receiveMessage(Message message) throws Exception {

        Jedis jedis = new Jedis("localhost", 6379);

        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(),"UTF-8");
        System.out.println("接收到的訊息為:"+msg+"==訊息id為:"+messageId);

        String messageIdRedis = jedis.get("messageId");

        if(messageId == messageIdRedis){
            return;
        }
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String email = jsonObject.getString("message");
        jedis.set("messageId",messageId);
    }
}

如果需要存入db的話,可以直接將這個ID設為訊息的主鍵,下次如果獲取到重複訊息進行消費時,由於資料庫主鍵的唯一性,則會直接丟擲異常。

> 如果覺得文章不錯,歡迎點贊、留言
> 關注公眾號《Java旅途》,每日推送精品文章

相關文章