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

千鋒Python唐小強發表於2020-08-07

一. 重複訊息

為什麼會出現訊息重複?訊息重複的原因有兩個: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設為訊息的主鍵,下次如果獲取到重複訊息進行消費時,由於資料庫主鍵的唯一性,則會直接丟擲異常。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69923331/viewspace-2710101/,如需轉載,請註明出處,否則將追究法律責任。

相關文章