定时消息:消息在特定的时间点触发消费。比如:在火车出发前或飞机起飞前 2 小时提醒乘客
延时消息:消息在特点的时间之后触发消费。比如:下单后 30 分钟内必须支付等。
./mqadmin updatetopic -n localhost:9876 -c DefaultCluster -t MY_DELAY_TOPIC -a +message.type=DELAY
注意主题类型为 DELAY 。
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.SendReceipt;
import java.io.IOException;
import java.time.Duration;
public class DelayProducerDemo {
public static void main(String[] args) throws ClientException {
// 用于提供:生产者、消费者、消息对应的构建类 Builder
ClientServiceProvider provider = ClientServiceProvider.loadService();
// 构建配置类(包含端点位置、认证以及连接超时等的配置)
ClientConfiguration configuration = ClientConfiguration.newBuilder()
// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
.setEndpoints(MyMQProperties.ENDPOINTS)
.build();
// 构建生产者
Producer producer = provider.newProducerBuilder()
// Topics 列表:生产者和主题是多对多的关系,同一个生产者可以向多个主题发送消息
.setTopics("MY_DELAY_TOPIC")
.setClientConfiguration(configuration)
// 构建生产者,此方法会抛出 ClientException 异常
.build();
// 构建消息类
Message message = provider.newMessageBuilder()
// 设置消息发送到的主题
.setTopic("MY_DELAY_TOPIC")
// 设置消息索引键,可根据关键字精确查找某条消息。其一般为业务上的唯一值。如:订单id
.setKeys("order_id_1001")
// 设置消息Tag,用于消费端根据指定Tag过滤消息。其一般用作区分不同的业务,最好给它定义好命名规范
.setTag("ORDER_AUTO_CANCEL")
// 设置消息投递时间(示例为 2 分钟后未支付,自动取消订单)
.setDeliveryTimestamp(System.currentTimeMillis() + Duration.ofMinutes(2).toMillis())
// 消息体,单条消息的传输负载不宜过大。所以此处的字节大小最好有个限制
.setBody("{\"success\":true,\"order_id\":\"1001\",\"msg\":\"订单超过 2 分钟未支付,取消订单!\"}".getBytes())
.build();
// 发送消息(此处最好进行异常处理,对消息的状态进行一个记录)
try {
SendReceipt sendReceipt = producer.send(message);
System.out.println("Send message successfully, messageId=" + sendReceipt.getMessageId());
} catch (ClientException e) {
System.out.println("Failed to send message");
}
// 发送完,关闭生产者
try {
producer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
生产者代码执行之后,我们通过 statsAll 命令查看,发现 Accumulation 累积量信息为0,说明,此时消息尚未被投递,且信息也没有写入 consumequeue(消费者队列) 文件夹下的存储文件。
延时消息与普通消息没什么区别,只需要调用 setDeliveryTimestamp 设置延时消息的投递时间即可。此时间为 long 类型的时间戳。
如果飞机起飞时间为 2023-09-01 09:00:00 那么提前两小时提醒使用:2023-09-01 09:00:00 减去 2 小时即可。
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;
import java.nio.ByteBuffer;
import java.util.Collections;
public class DelayConsumerDemo {
public static void main(String[] args) throws ClientException {
// 用于提供:生产者、消费者、消息对应的构建类 Builder
ClientServiceProvider provider = ClientServiceProvider.loadService();
// 构建配置类(包含端点位置、认证以及连接超时等的配置)
ClientConfiguration configuration = ClientConfiguration.newBuilder()
// endpoints 即为 proxy 的地址,多个用分号隔开。如:xxx:8081;xxx:8081
.setEndpoints(MyMQProperties.ENDPOINTS)
.build();
// 设置过滤条件(这里为使用 tag 进行过滤)
String tag = "ORDER_AUTO_CANCEL";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 构建消费者
PushConsumer pushConsumer = provider.newPushConsumerBuilder()
.setClientConfiguration(configuration)
// 设置消费者分组
.setConsumerGroup("ORDER_AUTO_CANCEL_GROUP")
// 设置主题与消费者之间的订阅关系
.setSubscriptionExpressions(Collections.singletonMap("MY_DELAY_TOPIC", filterExpression))
.setMessageListener(messageView -> {
System.out.println(messageView);
ByteBuffer rs = messageView.getBody();
byte[] rsByte = new byte[rs.limit()];
rs.get(rsByte);
System.out.println("Message body:" + new String(rsByte));
// 处理消息并返回消费结果。
System.out.println("Consume message successfully, messageId=" + messageView.getMessageId());
return ConsumeResult.SUCCESS;
}).build();
// 如果不需要再使用 PushConsumer,可关闭该实例。
// pushConsumer.close();
}
}
定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。
setDeliveryTimestamp(System.currentTimeMillis() - Duration.ofMinutes(2).toMillis())
setDeliveryTimestamp(System.currentTimeMillis() + Duration.ofHours(25).toMillis())
报错:Failed to send message
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class DelayProducerDemo {
/**
* 生产者分组
*/
private static final String PRODUCER_GROUP = "DELAY_PRODUCER_GROUP";
/**
* 主题
*/
private static final String TOPIC = "MY_DELAY_TOPIC";
public static void main(String[] args) throws MQClientException {
/*
* 创建生产者,并使用生产者分组初始化
*/
DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
/*
* NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876
*/
producer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);
/*
* 发送消息超时时间,默认即为 3000
*/
producer.setSendMsgTimeout(3000);
/*
* 启动生产者,此方法抛出 MQClientException
*/
producer.start();
try {
Message msg = new Message();
msg.setTopic(TOPIC);
// 设置消息索引键,可根据关键字精确查找某条消息。
msg.setKeys("messageKey");
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
msg.setTags("messageTag");
// 设置消息体
msg.setBody(("延时消息").getBytes());
// 延迟 30 秒
msg.setDelayTimeSec(30);
// 此为同步发送方式
SendResult rs = producer.send(msg);
System.out.printf("%s%n",rs);
} catch (Exception e) {
e.printStackTrace();
System.out.println("消息发送失败!");
}
// 如果生产者不再使用,则调用关闭
// 异步发送消息注意:异步发送消息,建议此处不关闭或者在sleep一段时间后再关闭
// 因为异步 SendCallback 执行的时候,shutdow可能已经执行了,生产者被关闭了
// producer.shutdown();
}
}
与普通消息的唯一区别在 msg.setDelayTimeSec(30); ,延迟时间最大为 259200000ms = 72 小时。否则会报错。
这里设置消息延迟时间有如下几个方法
- msg.setDelayTimeSec(30); 延时 30 秒,延迟时间最大为 259200s = 72 小时。否则会报错
- msg.setDelayTimeMs(30000);延时30秒,延迟时间最大为 259200000ms = 72 小时。否则会报错。
- msg.setDelayTimeLevel(); 设置延迟消息投递级别(5.x版本不建议使用该方法,因为时间不一定对应)
- msg.setDeliverTimeMs(System.currentTimeMillis() + 259200005); 设置延迟到设置的时间再执行。此方式可以设置超过 72 小时的延迟。(注:不建议设置超长延时时间,避免延时时间内延时消息过多,导致问题)
delay level | 延迟时间 |
---|---|
1 | 1s |
2 | 5s |
3 | 10s |
4 | 30s |
5 | 1min |
6 | 2min |
7 | 3min |
8 | 4min |
9 | 5min |
10 | 6min |
11 | 7min |
12 | 8min |
13 | 9min |
14 | 10min |
15 | 20min |
17 | 1h |
18 | 2h |
Remoting 协议客户端关于延时消息与gRPC协议客户端有不一样的地方,gRPC客户端只允许设置24小时内的延时(推荐使用方式),Remoting 协议客户端关于延时消息的设置更多,但是真不建议设置过长的延时时长,这样可以有效的避免消息的堆积。如果真的需要设置5天或者10天的延时消息,可以使用定时任务扫描 + 发送延时消息的方式来实现细粒度的延时任务,比如:2023-10-1 9:00 下单,在10 天后,2023-10-11 9:00 时需要撤销订单。那么我们在 2023-10-11 00:00 统一将当天需要触发的订单统一发送一个延时消息,延时时间根据每个订单具体的时间进行计算即可。
import com.yyoo.mq.rocket.MyMQProperties;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
public class DelayConsumerDemo {
/**
* 设置消费者分组
*/
public static final String CONSUMER_GROUP = "DELAY_CONSUMER_GROUP";
/**
* 主题
*/
public static final String TOPIC = "MY_DELAY_TOPIC";
public static void main(String[] args) throws MQClientException {
/*
* 通过消费者分组,创建消费者
*/
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
/*
* NamesrvAddr 的地址,多个用分号隔开。如:xxx:9876;xxx:9876
*/
consumer.setNamesrvAddr(MyMQProperties.NAMESRV_ADDR);
/*
* 指定从哪一个消费位点开始消费 CONSUME_FROM_FIRST_OFFSET 表示从第一个开始
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
/*
* 消费者订阅的主题,和过滤条件
* 我们这里使用 * 表示,消费者消费主题下的所有消息,多个tag 使用 || 隔开
*/
consumer.subscribe(TOPIC, "*");
/*
* 注册消费监听
*/
consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
for (MessageExt message : msg) {
// 当前时间减掉消息的存储时间,即为延时时间
System.out.printf("Receive message[msgId=%s %d ms later]\n", message.getMsgId(),
System.currentTimeMillis() - message.getStoreTimestamp());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
/*
* 启动消费者.
*/
consumer.start();
System.out.printf("Consumer Started.%n");
// 如果消费者不再使用,关闭
// consumer.shutdown();
}
}