RocketMQ 发送延时消息

应用场景

定时消息:消息在特定的时间点触发消费。比如:在火车出发前或飞机起飞前 2 小时提醒乘客
延时消息:消息在特点的时间之后触发消费。比如:下单后 30 分钟内必须支付等。

创建主题

./mqadmin updatetopic -n localhost:9876 -c DefaultCluster -t MY_DELAY_TOPIC -a +message.type=DELAY

注意主题类型为 DELAY 。

rocketmq-client-java 示例(gRPC 协议)

延时消息生产者示例

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())
  • 定时时间不能超过 24 小时,超过 24 小时会报错。如:
setDeliveryTimestamp(System.currentTimeMillis() + Duration.ofHours(25).toMillis())

报错:Failed to send message
  • RocketMQ 定时消息的定时时长参数精确到毫秒级,但是默认精度为1000ms,即定时消息为秒级精度。

rocketmq-client 示例(Remoting 协议)

生产者

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 小时的延迟。(注:不建议设置超长延时时间,避免延时时间内延时消息过多,导致问题)

延迟时间级别(4.x版本,5.x版本不建议使用,因为时间不一定对应)

delay level延迟时间
11s
25s
310s
430s
51min
62min
73min
84min
95min
106min
117min
128min
139min
1410min
1520min
171h
182h

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();

    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值