问题背景
业务中有个场景,用户设置一个计划,计划包含开始时间和截止时间,到了截止时间计划自动变更为结束状态。 因为项目中其他模块引入了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
而2147483647转换成天才不到25天
也就是说,如果延迟消息比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出现问题,可能导致消息丢失且发现不及时,从而影响业务,可以考虑搭配其他实现方案进行兜底