Rabbitmq延迟队列时间超出Integer类型问题处理

649 阅读3分钟

问题背景

业务中有个场景,用户设置一个计划,计划包含开始时间和截止时间,到了截止时间计划自动变更为结束状态。 因为项目中其他模块引入了Rabbitmq组件,就考虑也用Rabbitmq的延迟队列插件来实现此功能。

大致逻辑如下: 用户在新增计划时,将计划存入数据库,并往Rabbitmq的延迟队列中发布一条延迟消息,延迟的时长就是计划的截止时间与当前时间的差值

配置一个监听器,负责指定的延迟队列,因为消息数量不是很多,所以是多个业务场景的消息共用一个延迟队列,封装一个消息实体,实体中根据枚举值指定消息类别,具体消费时根据不同的类型各自处理各自的逻辑

具体的实现用的是AmqpTemplate的convertAndSend方法

amqpTemplate.convertAndSend(QueueConstant.DELAY_EXCHANGE, delayRoute, message, new MessagePostProcessor() {
   @Override
   public Message postProcessMessage(Message message) throws AmqpException {
      message.getMessageProperties().setDelay(finalDelayTime);
      return message;
   }
});

它接收的参数分别为交换机名、队列名、消息以及一个内部方法设置延迟时间,而这个延迟时间的类型是Integer

public void setDelay(Integer delay) {
    if (delay != null && delay >= 0) {
        this.headers.put("x-delay", delay);
    } else {
        this.headers.remove("x-delay");
    }

}

是时间的毫秒值,而Integer类型的最大值是2^31-1,即2147483647

image.png

而2147483647转换成天才不到25天

image.png

也就是说,如果延迟消息比25天长,那Integer就装不下了,我们在传参的时候需要将延迟时间转换成Integer类型的毫秒值,超过了25天就会发生integer overflow异常

解决方法

业务方传的延迟时间用Long类型,在封装的工具类中对延迟时间的大小进行判断,如果超过了Integer类型的表示范围,就进行多次延迟,第一次延迟时间为Integer.MAX,并记录剩余需要延迟的时间,即原始延迟时间减去Integer.MAX,并设置一个专门的消费类别来消费多次延迟的消息,直到延迟时间在Integer的表示范围内才进行真正的业务逻辑处理

具体实现

封装的消息实体

public class TaskMessage implements Serializable {

   private TaskMessageType taskMessageType;

   private Object data;

   public TaskMessage() {
   }

   public TaskMessage(TaskMessageType taskMessageType) {
      this.taskMessageType = taskMessageType;
   }

   public TaskMessage(TaskMessageType taskMessageType, JSONObject data) {
      this.taskMessageType = taskMessageType;
      this.data = data;
   }
}

正常业务发送消息

/**
 * 往延时队列发消息
 *
 * @param msgId
 * @param sendTime
 */
private void sendDelayMsg(Integer msgId, LocalDateTime sendTime) {
   TaskMessage taskMessage = new TaskMessage();
   taskMessage.setTaskMessageType(TaskMessageType.TERM);
   Map<String, Object> dataMap = Maps.newHashMap();
   dataMap.put("msgId", msgId);
   taskMessage.setData(dataMap);
   LocalDateTime nowTime = LocalDateTime.now();
   Long endTime = 0L;
   if (nowTime.isBefore(sendTime)) {
      endTime = Duration.between(LocalDateTime.now(), sendTime).toMillis();
   }
   messageSendUtil.delay(taskMessage, QueueConstant.DELAY_ROUTE, endTime);
}

封装的messageSendUtil

@Component
@RequiredArgsConstructor
@Slf4j
public class MessageSendUtil {

   private final AmqpTemplate amqpTemplate;

   /**
    *
    * @param message 发送的消息
    * @param delayTime 毫秒
    */
   public void delay(TaskMessage message, String delayRoute, Long delayTime) {

      try {
         if (delayTime < 0) {
            delayTime = 0L;
         }

         //超出最大过期时间的时间补偿
         //超出的会立马执行
         if (!message.getTaskMessageType().equals(TaskMessageType.TIME_OFFSET)
               && delayTime.compareTo((long) Integer.MAX_VALUE)> 0) {
            long offsetTime = (long) Integer.MAX_VALUE;
            TaskMessage taskMessage = new TaskMessage();
            taskMessage.setTaskMessageType(TaskMessageType.TIME_OFFSET);
            Map<String, Object> dataMap = Maps.newHashMap();
            dataMap.put("remainderTime", delayTime-offsetTime);
            dataMap.put("taskMessage", message);
            taskMessage.setData(dataMap);
            this.delay(taskMessage, QueueConstant.DELAY_ROUTE, offsetTime);

            return;
         }

         Integer finalDelayTime = delayTime.intValue();
         amqpTemplate.convertAndSend(QueueConstant.DELAY_EXCHANGE, delayRoute, message, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
               message.getMessageProperties().setDelay(finalDelayTime);
               return message;
            }
         });
         log.info("{}:延迟消息已推送", message.getTaskMessageType().name());
      } catch (Exception e) {
         log.error(e.getMessage(), e);
      }
   }

}

消息消费


@Component
@AllArgsConstructor
@Slf4j
public class RabbitQueueListener {

   @Autowired
   @Lazy
   private MsgService msgService;

   @Autowired
   @Lazy
   private TermService termService;

   private MessageSendUtil messageSendUtil;

   @Bean
   public DelayRabbitListenerExceptionHandler delayListenerErrorHandler() {
      return new DelayRabbitListenerExceptionHandler();
   }

   /**
    * 监听延时队列的处理器
    *
    * @param message
    */
   @RabbitListener(bindings = @QueueBinding(
         value = @Queue(QueueConstant.DELAY_QUEUE),
         key = QueueConstant.DELAY_ROUTE,
         exchange = @Exchange(value = QueueConstant.DELAY_EXCHANGE, type = "x-delayed-message")
   ), errorHandler = "delayListenerErrorHandler")
   @RabbitHandler
   public void onDelayMessage(Message message, Channel channel) throws Exception {
      long tagId = message.getMessageProperties().getDeliveryTag();
      try {
         log.info("delay到时消费:{} ", tagId);
         SimpleMessageConverter simpleMessageConverter = new SimpleMessageConverter();
         TaskMessage taskMessage = (TaskMessage) simpleMessageConverter.fromMessage(message);

         if (taskMessage != null) {
            switch (taskMessage.getTaskMessageType()) {
               case NOTICE:
                  Map<String, Object> data = (Map<String, Object>) taskMessage.getData();
                  msgService.handleDelayMsg((Integer) data.get("msgId"));
                  break;
               case TERM:
                  Map<String, Object> msgData = (Map<String, Object>) taskMessage.getData();
                  termService.handleDelayMsg(msgData);

               case TIME_OFFSET:// 定义的特殊消息类型
                  Map<String, Object> timeOffsetData = (Map<String, Object>) taskMessage.getData();
                  messageSendUtil.delay((TaskMessage) timeOffsetData.get("taskMessage"), QueueConstant.DELAY_ROUTE, (Long) timeOffsetData.get("remainderTime"));
               default:
                  break;

            }
         }
         channel.basicAck(tagId, false);
      } catch (Exception e) {
         //拒绝
         channel.basicReject(tagId, false);
         //抛出异常,触发重试
         throw new CheckedException(e.getMessage());
      }
   }

}

总结

消息延迟时间如果超过Integer.MAX,就进行多次延迟,直到延迟时间在Integer范围内

潜在风险

延迟时间过长,夜长梦多,如果中途mq出现问题,可能导致消息丢失且发现不及时,从而影响业务,可以考虑搭配其他实现方案进行兜底