目录

第七章-RabbitMQ之延迟队列

1. 延迟队列概念

2. 应用场景

3. 架构模式

3.1 队列TTL实现

3.2 消息TTL实现

3.3 插件实现

4. 代码验证

5. 总结


第七章-RabbitMQ之延迟队列

1. 延迟队列概念

延迟-意即 非实时,之前我们讨论大部分的案例都是生产者将消息发送至Broker,消费者立即进行了消费,若消费者消费能力小于生产者生产能力,产生了消息堆积,也会产生延迟。但这种延迟不是我们主观要求的延迟。

此次涉及到的延迟-是在生产者发消息时,就明确预知的,会产生延迟消费,而且延迟的时间也是设定好的。消息会暂存到queue中,等待预设时间到达后,再即时触发消费。

2. 应用场景

所有发出的消息不想立即消费的场景,例如:

2.1 用户下单后30M未支付会取消订单

2.2 用户下单后2M消息提醒其支付

2.3 用户注册后2天内未登录,进行提醒

.....

当然我们可以通过定时扫描的方式,来实现,但定时扫描会存在控制不精准,若扫描的数据量大对性能有影响。

优雅的方式可以引入延迟队列。

3. 架构模式

实现延迟队列的话,我们考虑两种方式,第一种是通过TTL ,第二种通过rabbitmq的插件,TTL这部分已经在死信队列的篇章介绍过了,与之前死信队列的TTL实现是一致的:

3.1 队列TTL实现

Spring boot rabbitmq 延迟队列 rabbitmq延迟队列阻塞_System

 如上图,我们将队列1 TTL 设置为10s,且队列1不再有消费者进行消费,那生产者生产的消息都会在队列1中暂存10S,然后投递到死信交换机,再路由到队列2,最后被消费者2成功消费,这样就实现了 消息的延迟消费。而且通过队列设置TTL,消息的延迟都是准确的。

问题:

若我们的业务,并非单一的失效时间,存在多种失效时间,或失效的时间不是固定的,这样就会比较麻烦,我们不可能为每一种失效时间再去增加一个队列吧,队列3-TTL20s,队列4-TTL30s,队列5-TTL50s ... 

 那我们试着从消息入手,在消息发出的时候就设定好TTL。

3.2 消息TTL实现

Spring boot rabbitmq 延迟队列 rabbitmq延迟队列阻塞_java_02

 这次是不是万无一失呢,队列1不再设置失效时间,发送的四条消息设置不同的时间,通过时间到期,自动转到队列2,消费者2成功去消费。

问题:

若按消息设置失效期,则会存在失效时间不准的情况。例如msg1 TTL 60s ,msg2 ttl 10s ,理论上是msg2先失效,结果是 msg1 60s失效后,再msg2失效。

结论呢,RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。原因是,rabbitmq在等到消息投递给消费者的时候判断当前消息是否过期。

那还有没有更好的办法,装插件,使用延迟的交换机,来实现。

3.3 插件实现

3.3.1 插件的安装

rabbitmq-delayed-message-exchange

官网下载插件:Community Plugins — RabbitMQ

安装完成后:面板展示:

Spring boot rabbitmq 延迟队列 rabbitmq延迟队列阻塞_java-rabbitmq_03

3.3.2 使用延迟插件架构

Spring boot rabbitmq 延迟队列 rabbitmq延迟队列阻塞_架构模式_04

 这次又回到最经典的模式了,生产者->交换机->队列->消费者,

只是交换机是一个特殊的 延迟交换机而已。

延迟交换机面板:

Spring boot rabbitmq 延迟队列 rabbitmq延迟队列阻塞_rabbitmq_05

 核心的不同是声明交换机的时候:

1. 绑定direct类型 与 延迟交换机参数:

Map<String, Object> argMap = new HashMap<>();
        argMap.put("x-delayed-type", "direct");

2. 声明交换机的时候:

channel.exchangeDeclare(PLUGINS_EXCHANGE, "x-delayed-message", true, false, argMap );

4. 代码验证

代码验证不再对TTL进行验证,可以参考前一章节 死信队列的内容

针对插件验证:

生产者:

/**
 * @author rabbit
 * @version 1.0.0
 * @Description -
 * @createTime 2022/07/27 19:34:00
 */
public class PluginsDelayProducer {

    public static String PLUGINS_EXCHANGE = "plugins.exchange";
    public static String PLUGINS_ROUTING_KEY = "plugins";
    public static String PLUGINS_QUEUE = "plugins.queue";

    //生产者
    public static void main(String[] args) throws Exception {
        //1、获取connection
        Connection connection = RabbitCommonConfig.getConnection();
        //2、创建channel
        Channel channel = connection.createChannel();
        List<Integer> delayedTimes = Arrays.asList(5, 2, 3, 4, 1);
        for (Integer delayedTime : delayedTimes) {
            sendMsg(channel, delayedTime);
        }
        //4、关闭管道和连接
        channel.close();
        connection.close();
    }

    private static void sendMsg(Channel channel, Integer delayedTime) throws IOException, InterruptedException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String content = String.format("消息时间:[%s],延时[%d]s", sdf.format(new Date()), delayedTime);
        byte[] msg = content.getBytes(StandardCharsets.UTF_8);
        Map<String, Object> headers = new HashMap<>();
        headers.put("x-delay", delayedTime * 1000);
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().headers(headers).build();
        channel.basicPublish(PLUGINS_EXCHANGE, PLUGINS_ROUTING_KEY, properties, msg);
        System.out.println("消息发送完成:" + content);
    }

}

消费者:

/**
 * @author rabbit
 * @version 1.0.0
 * @Description
 * @createTime 2022/11/17 16:53:00
 */
public class PluginsDelayConsumer {

    public static String PLUGINS_EXCHANGE = "plugins.exchange";
    public static String PLUGINS_ROUTING_KEY = "plugins";
    public static String PLUGINS_QUEUE = "plugins.queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //1、获取连对象、
        Connection connection = RabbitCommonConfig.getConnection();
        //2、创建channel
        Channel channel = connection.createChannel();

        // 延迟交换机参数
        Map<String, Object> delayParams = getNormalAndDeadParams();

        // 4.声明一个队列与交换机及绑定关系
        handleQueueAndBinding(channel, PLUGINS_QUEUE, delayParams, PLUGINS_EXCHANGE, PLUGINS_ROUTING_KEY);

        channel.basicQos(1);

        //5.开启监听Queue
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String content = sdf.format(new Date());
                System.out.println("延迟消费者接收消息: " + new String(body, "UTF-8") + "当前时间: " + content);
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        channel.basicConsume(PLUGINS_QUEUE, false, consumer);

        System.out.println("延迟消费者启动接收消息......");

        //5、键盘录入,让程序不结束!
        System.in.read();

        //6、释放资源
        channel.close();
        connection.close();

    }

    private static Map<String, Object> getNormalAndDeadParams() {
        Map<String, Object> argMap = new HashMap<>();
        argMap.put("x-delayed-type", "direct");
        return argMap;
    }

    /**
     * 处理队列与绑定关系
     *
     * @param channel
     * @param deadQueueName
     * @param o
     * @param exchangeName
     * @param routingKey
     * @throws IOException
     */
    private static void handleQueueAndBinding(Channel channel, String deadQueueName, Map<String, Object> o, String exchangeName, String routingKey) throws IOException {
        channel.exchangeDeclare(PLUGINS_EXCHANGE, "x-delayed-message", true, false, o);
        channel.queueDeclare(PLUGINS_QUEUE, true, false, false, new HashMap<>());
        channel.queueBind(PLUGINS_QUEUE, PLUGINS_EXCHANGE, PLUGINS_ROUTING_KEY);
    }

}

5. 总结

总的来说实现延迟队列有3种形式:

1. 队列TTL

2.消息TTL

3. 安装延迟插件

使用TTL会有限制且不通用,架构也相对复杂,但也有一些业务失效时间是明确的也可以使用。

使用插件会相对简单,但有些公司,中间件是独立管理的,安装插件还需要沟通,也不一定能够同意安装。

所已,还是因地制宜。适合的就是最好的!