RabbitMQ基于Java的定时任务实现

RabbitMQ的定时任务实现主要原理是借助rabbitmq的消息过期机制,发送消息时可以指定一个expiration(单位毫秒),当一个消息在一个队列内过期时,在默认情况下会drop丢弃掉(此处有个条件,就是该消息必须位于队首,也就是即将被消费时才会判断是否过期,也就是说不在队首的消息即使expiration到期也无法丢弃到死信队列)。如果队列配置了死信队列exchange和routing key, 则会将此到期的消息路由到routing key对应的队列内。

    rabbitmq中对于发布出去但是无法路由到任意一个队列的消息会返回给publisher,如果publisher设置了
处理该情况的returnListener可以选择如何处理,如果没设置默认就drop丢弃掉了。
    对于设置了expiration的定时消息,到达过期时间后,默认行为也是drop丢弃掉,如果队列声明时,配置了
死信处理参数,x-dead-letter-exchange和x-dead-letter-routing-key,分别指定死信交换机和死信路由
key,则在消息过期后,该消息会通过x-dead-letter-exchange设置的exchange根据x-dead-letter-routing-key
设置的routing key将消息路由到routing key绑定的queue队列。(但是到期的消息需满足在队首,就是即将被消费,否则即使到期也无法被丢弃)

下面对应rabbitmq的amqp-client 5.7.2

package demo.rabbitmq.schedule;

import com.rabbitmq.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeoutException;

/**
 * @author ZHUFEIFEI
 */
public class Demo01 {

    private static final Logger log = LoggerFactory.getLogger(demo.rabbitmq.Demo01.class);

    private static String handleMsgQueue = "handleMsgQueue";

    private static String scheduleMsgQueue = "scheduleMsgQueue";
    //两个队列用的同一个exchange,也可以不同
    private static String deadLetterExchange = "deadLetterExchangeForScheduleMsg";

    private static String deadRoutingKey = "dead_routing_key";

    private static String scheduleRoutingKey = "schedule_routing_key";

    private static CountDownLatch cdl = new CountDownLatch(2);

    public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, URISyntaxException, IOException, TimeoutException, InterruptedException {
        ConnectionFactory factory = new ConnectionFactory();
//        factory.setUri("amqp://guest:guest@localhost:5672//");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("localhost");
        factory.setPort(5672);

        Connection conn = factory.newConnection();

        Channel channel = conn.createChannel();

        //声明死信队列, 发送的消息到死信队列,此处死信队列扮演的就是定时消息队列,进入该队列的消息需要设置expiration,该队列不需要消费者
        Map arguments = new HashMap<>(2);
        //指定死信处理exchange, 死信由该exchange处理
        arguments.put("x-dead-letter-exchange", deadLetterExchange);
        //配置消息过期后,路由到哪个routing key,该key应该是对应真正的任务队列,消费者消费真正的任务队列
        arguments.put("x-dead-letter-routing-key", deadRoutingKey);
        AMQP.Queue.DeclareOk resultQueue = channel.queueDeclare(scheduleMsgQueue, true, false, false, arguments);
        log.info("queue declare -> {}", resultQueue);
        //声明死信交换机
        AMQP.Exchange.DeclareOk resultExchange = channel.exchangeDeclare(deadLetterExchange, BuiltinExchangeType.DIRECT);
        log.info("exchange declare -> {}", resultExchange);
        //绑定消息队列到一个处理器,此处理器可以和死信队列处理器是一个也可以不是一个,此处用一个exchange
        AMQP.Queue.BindOk resultBind = channel.queueBind(scheduleMsgQueue, deadLetterExchange, scheduleRoutingKey);
        log.info("bind result -> {}", resultBind);

        //声明真正延迟消息处理的队列,当消息在定时消息队列过期后会当成死信放到该队列,消费者消费该队列
        resultQueue = channel.queueDeclare(handleMsgQueue, true, false, false, null);
        log.info("queue declare -> {}", resultQueue);
        //为处理消息队列绑定exchange,并指定routing key, 该routing key就是定时队列过期后的消息要路由的那个key
        resultBind = channel.queueBind(handleMsgQueue, deadLetterExchange, deadRoutingKey);
        log.info("bind result -> {}", resultBind);

        publishMsg(channel);
        
        consumeMsg(channel);

        cdl.await();
        channel.close();
        conn.close();
    }

    private static void consumeMsg(Channel channel) throws IOException {
        channel.basicConsume(handleMsgQueue, false, "myScheduleConsumerTag",
                new DefaultConsumer(channel) {
                    @Override
                    public void handleDelivery(String consumerTag,
                                               Envelope envelope,
                                               AMQP.BasicProperties properties,
                                               byte[] body)
                            throws IOException {
                        String routingKey = envelope.getRoutingKey();
                        String contentType = properties.getContentType();
                        long deliveryTag = envelope.getDeliveryTag();

                        log.info("consumerTag -> {}, routingKey -> {}, contentType -> {}, deliveryTag -> {}, content -> {}"
                                , consumerTag, routingKey
                                , contentType, deliveryTag
                                , new String(body)
                                );

                        log.info("basic properties -> {}", properties);
                        channel.basicAck(deliveryTag, false);

                        cdl.countDown();
                    }
                });
    }

    private static void publishMsg(Channel channel) throws IOException {
        byte[] messageBodyBytes = "Hello, world!".getBytes();
        //发送消息,并自定义属性, 同上效果
        channel.basicPublish(deadLetterExchange, scheduleRoutingKey,
                new AMQP.BasicProperties.Builder()
                        .contentType("text/plain")
                        .deliveryMode(2)
                        .priority(1)
                        .expiration("30000") // 30秒
                        .build(),
                messageBodyBytes);
        channel.basicPublish(deadLetterExchange, scheduleRoutingKey,
                new AMQP.BasicProperties.Builder()
                        .contentType("text/plain")
                        .deliveryMode(2)
                        .priority(1)
                        .expiration("60000") // 60秒
                        .build(),
                messageBodyBytes);
        log.info("message published!");
    }
}

        还有另一种方式的定时任务实现,rabbitmq插件社区提供了rabbitmq_delayed_message_exchange插件,提供定时 消息支持,内部采用erlang定时器定时检查消息的x-delay属性是否在0-(2^32-1)区间内,x-delay是发送消息时的一个属性,毫秒过期值,当该值小于0时,就需要发送到消息队列。

       插件形式的定是消息支持局限性较多,默认采用disc节点方式存储并且在当前节点只有一个副本,消息只是延迟路由到指定的消息队列,在路由到队列前存储在mnesia数据表中,会有其他逻辑来检测消息是否到期。该插件提供的exchange类型为x-delayed-message, 是默认的4种exchange的代理,内部路由逻辑还是使用的内置的4个exchange,由于多了层代理并且需要erlang定时器定时检测,因此性能上要差一些,并且数据量不宜过大(最好不要超过十万量级),不能严格保证是x-delay的时间后准时执行,插件内的定时器不会持久化,会在每次插件重启时初始化,一旦禁用插件,所有待发布的定时消息将会丢失。

 

注意:基于死信队列的实现有一个问题,就是消息队列对于每个消息带有ttl的情况,不会定时扫描队列检查消息是否过期,而是在消费者消费到某一个消息时(或者说某个消息到达队首时),对该消息的expiration进行判断,如果过期直接丢到死信队列。因此如果定时队列没有消费者,并且消息的过期时间参差不齐(不是有序的,就是后来的消息过期时间可能小于之前到达的消息),则消息无法定时被丢到死信队列中去!

 

参考:scheduling-messages-with-rabbitmq

参考:rabbitmq-delayed-message-exchange

参考:message/queue ttl

 

 

 

 

你可能感兴趣的:(RabbitMQ)