定时/延时消息是 Apache RocketMQ 提供的一种高级消息类型,消息被发送至服务端后,在指定时间后才能被消费者消费。通过设置一定的定时时间可以实现分布式场景的延时调度触发效果。
应用场景
在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的定时事件触发。使用 Apache RocketMQ 的定时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。
典型场景一:分布式定时调度
在分布式定时调度场景下,需要实现各类精度的定时任务,例如每天5点执行文件清理,每隔2分钟触发一次消息推送等需求。传统基于数据库的定时调度方案在分布式场景下,性能不高,实现复杂。基于 Apache RocketMQ 的定时消息可以封装出多种类型的定时触发器。
典型场景二:任务超时处理
以电商交易场景为例,订单下单后暂未支付,此时不可以直接关闭订单,而是需要等待一段时间后才能关闭订单。使用 Apache RocketMQ 定时消息可以实现超时任务的检查触发。
基于定时消息的超时任务处理具备如下优势:
- 精度高、开发门槛低:基于消息通知方式不存在定时阶梯间隔。可以轻松实现任意精度事件触发(RocketMQ4.x以及之前的版本只能设置固定的延时时间),无需业务去重。
- 高性能可扩展:传统的数据库扫描方式较为复杂,需要频繁调用接口扫描,容易产生性能瓶颈。 Apache RocketMQ 的定时消息具有高并发和水平扩展的能力。
RocketMQ4.x以及之前版本的延时/定时消息
Apache RocketMQ 一共支持18个等级的延迟投递,具体时间如下:
投递等级(delay level) | 延迟时间 | 投递等级(delay level) | 延迟时间 |
---|---|---|---|
1 | 1s | 10 | 6min |
2 | 5s | 11 | 7min |
3 | 10s | 12 | 8min |
4 | 30s | 13 | 9min |
5 | 1min | 14 | 10min |
6 | 2min | 15 | 20min |
7 | 3min | 16 | 30min |
8 | 4min | 17 | 1h |
9 | 5min | 18 | 2h |
要发送延时消息,只需要设置消息的延时投递等级,修改MqProcuder的OldVersionProducer:
public SendResult sendDelayMsg(String topic,String msg, int delayLevel) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
Message message = new Message(topic, msg.getBytes(StandardCharsets.UTF_8));
message.setDelayTimeLevel(delayLevel);
return producer.send(message);
}
新建DelayController:
@RestController
public class DelayController {
@Autowired
private OldVersionProducer oldVersionProducer;
String topic = "MyTopic";
@RequestMapping("/sendDelayMsg")
public Map<String,Object> sendDelayMsg(@RequestParam("msg") String msg,@RequestParam("delayLevel") Integer delayLevel) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
Map<String,Object> map = new HashMap<>(4);
SendResult sendResult = oldVersionProducer.sendDelayMsg(topic, msg, delayLevel);
map.put("sendResult", sendResult);
map.put("sendDate", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
return map;
}
}
启动消费者后,在启动MqProcuder,访问http://localhost:8001/sendDelayMsg?msg=123&delayLevel=3,发送消息并设置延时投递等级为3,即延时10秒:
记住上图中的发送时间。
查看消费者控制台:
查看消费时间,和上面的发送时间对比,正好相差10秒。
继续访问http://localhost:8001/sendDelayMsg?msg=123&delayLevel=0,设置了延时投递等级为0,不在1到18之内,多试几次发现延时时间为随机时间。继续访问http://localhost:8001/sendDelayMsg?msg=123&delayLevel=50,发送成功后,在RocketMQ dashboard中的SCHEDULE_TOPIC_XXXX topic查到消息。访问http://localhost:8001/sendDelayMsg?msg=123&delayLevel=-3,发现延时时间为随机时间。
设置level等级:
- level <= 0,消息为非延迟消息
- 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
- level > maxLevel,则level== maxLevel,例如level==20,延迟2h
定时消息会暂存在名为SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消费SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。
RocketMQ5.0延时/定时消息
定时时间设置原则
- Apache RocketMQ 定时消息设置的定时时间是一个预期触发的系统时间戳,延时时间也需要转换成当前系统时间后的某一个时间戳,而不是一段延时时长。
- 定时时间的格式为毫秒级的Unix时间戳,您需要将要设置的时刻转换成时间戳形式。
- 定时时间必须设置在定时时长范围内,超过范围则定时不生效,服务端会立即投递消息。
- 定时时长最大值默认为24小时,不支持自定义修改
- 定时时间必须设置为当前时间之后,若设置到当前时间之前,则定时不生效,服务端会立即投递消息。
示例如下:
- 定时消息:例如,当前系统时间为2022-06-09 17:30:00,您希望消息在下午19:20:00定时投递,则定时时间为2022-06-09 19:20:00,转换成时间戳格式为1654773600000。
- 延时消息:例如,当前系统时间为2022-06-09 17:30:00,您希望延时1个小时后投递消息,则您需要根据当前时间和延时时长换算成定时时刻,即消息投递时间为2022-06-09 18:30:00,转换为时间戳格式为1654770600000。
定时消息生命周期
- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
- 定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息单独存储在定时存储系统中,等待定时时刻到达。
- 待消费:定时时刻到达后,服务端将消息重新写入普通存储引擎,对下游消费者可见,等待消费者消费的状态。
- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。
- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
定时精度约束
Apache RocketMQ 定时消息的定时时长参数精确到毫秒级,但是默认精度为1000ms,即定时消息为秒级精度。
Apache RocketMQ 定时消息的状态支持持久化存储,系统由于故障重启后,仍支持按照原来设置的定时时间触发消息投递。若存储系统异常重启,可能会导致定时消息投递出现一定延迟。
首先进入RocketMQ的安装目录,在进入bin目录,打开控制台,建立topic:
mqadmin.cmd updateTopic -c DefaultCluster -t myDelayTopic -n 127.0.0.1:9876 -a +message.type=DELAY
新建名为myDelayTopic的topic并指定消息类型是DELAY。
在Mq-consumer添加消费者:
@Slf4j
@Component
public class RocketMq5DelayConsumer {
@Value("${rocketmq.proxy}")
private String proxy;
String topic = "myDelayTopic";
@Bean(name = "mqDelayConsumer")
public void mqConsumer() {
ClientServiceProvider provider = ClientServiceProvider.loadService();
ClientConfigurationBuilder builder = ClientConfiguration.newBuilder().setEndpoints(proxy);
ClientConfiguration configuration = builder.build();
// 初始化Producer时需要设置通信配置以及预绑定的Topic。
try {
// 订阅消息的过滤规则,表示订阅所有Tag的消息。
String tag = "*";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
provider.newPushConsumerBuilder()
.setClientConfiguration(configuration)
// 设置消费者分组。
.setConsumerGroup("my-delay-consumer")
// 设置预绑定的订阅关系。
.setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
// 设置消费监听器。
.setMessageListener(messageView -> {
log.info("消费消息:{}", messageView);
log.info("消息内容为:{}", StandardCharsets.UTF_8.decode(messageView.getBody()).toString());
return ConsumeResult.SUCCESS;
}).build();
log.info("构建mq5.0消费者成功:proxy:{}, topic:{}", proxy, topic);
} catch (ClientException e) {
log.error("构建mq5.0消费者异常:proxy:{}, topic:{}", proxy, topic, e);
}
}
}
修改生产者MqProducer的MyController:
@RequestMapping("/sendNewDelayMsg")
public Map<String,Object> sendNewDelayMsg(@RequestParam("msg") String msg, @RequestParam("delaySecond") Integer delaySecond) throws ClientException {
Map<String,Object> map = new HashMap<>(4);
MessageBuilder messageBuilder = new MessageBuilderImpl();
Message message = messageBuilder.setBody(msg.getBytes(StandardCharsets.UTF_8))
.setTopic("myDelayTopic")
.setDeliveryTimestamp(System.currentTimeMillis() + delaySecond * 1000)
.build();
SendReceipt sendReceipt = producer.send(message);
map.put("sendResult", sendReceipt);
map.put("sendDate", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
return map;
}
在浏览器访问http://localhost:8001/sendNewDelayMsg?msg=123&delaySecond=2,设置延时时间为2秒,查看发送时间:
查看消费者控制台:
时间略有差异。改变delaySecond,多试几次,查看效果。RocketMQ5.0现在可以实现自定义的延时时间。但是不能最大值默认为24小时。
改变delaySecond=-80,设置时间为过去的时间,发现立刻消费了消息。
24小时等于86400秒,更改delaySecond=86401,发现报错,查看日志:
org.apache.rocketmq.client.java.exception.BadRequestException: [request-id=b46d32d8-b72b-4f80-8e7a-fa6d077b3818, response-code=40012] the max delay time of message is too large, max is 86400000
延时时间间隔毫秒不能超过86400000,也就是24小时。