【SpringCloud】RabbitMQ 延迟消息实现(TTL+死信队列 & 延迟插件)

为什么要延迟消息?

场景:在电商的支付业务中,对于一些库存有限的商品,为了更好的用户体验,通常都会在用户下单时立刻扣减商品库存。例如电影院购票、高铁购票,下单后就会锁定座位资源,其他人无法重复购买。

因此,电商中通常的做法就是:对于超过一定时间未支付的订单,应该立刻取消订单并释放占用的库存。

这种在一段时间以后才执行的任务,称之为延迟任务,而要实现延迟任务,最简单的方案就是利用MQ的延迟消息。

在 RabbitMQ 中实现延迟消息也有两种方案:

  • TTL + 死信队列

  • 延迟消息插件

1、TTL + 死信队列

What is 死信?

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.rejectbasic.nack声明消费失败,并且消息的requeue参数设置为false

  • 消息是一个过期消息,超时无人消费

  • 要投递的队列消息满了,无法投递

如果一个队列中的消息已经成为死信,并且这个队列通过dead-letter-exchange属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就称为死信交换机(Dead Letter Exchange,简称DLX)。

而此时加入有队列与死信交换机绑定,则最终死信就会被投递到这个队列中。

在这里插入图片描述

死信交换机的作用呢?

  • 收集那些因处理失败而被拒绝的消息

  • 收集那些因队列满了而被拒绝的消息

  • 收集因TTL(有效期)到期的消息

把死信交换机当做一种消息处理的最终兜底方案,与消费者重试时的RepublishMessageRecoverer作用类似。

具体实现:

消费者:交换机队列声明 + 绑定死信交换机

package com.itheima.consumer.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class NormalConfig {
    /**
     * 声明交换机
     *
     * @return Direct类型交换机
     */
    @Bean
    public DirectExchange normalExchange() {
        return ExchangeBuilder.directExchange("normal.direct").build();
    }

    /**
     * 声明队列且绑定死信交换机
     */
    @Bean
    public Queue normalQueue() {
        return QueueBuilder
                .durable("normal.queue")
                .deadLetterExchange("dlx.direct")
                .build();
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding normalExchangeBinding(Queue normalQueue, DirectExchange normalExchange) {
        return BindingBuilder.bind(normalQueue).to(normalExchange).with("hi");
    }

}

消费者:监听dlx.queue死信队列消息

/**
 * 死信队列模拟延迟消息
 *
 * @param msg
 */
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "dlx.queue", durable = "true"),
        exchange = @Exchange(name = "dlx.direct", type = ExchangeTypes.DIRECT),
        key = {"hi"}
))
public void listenDlxQueue(String msg) {
    System.out.println("消费者1接收到dlx.queue的消息:【" + msg + "】");
}

生产者:发送消息

    /**
     * 测试发送延迟消息(死信交换机实现)
     */
    @Test
    void testSendDelayMessage() {
        // 不能用Message转换器,因为Message是转换成字节,而消费者已经设置消息转换器了
        rabbitTemplate.convertAndSend("normal.direct", "hi", "hello", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置过期消息
                message.getMessageProperties().setExpiration("10000");
                return message;
            }
        });
    }

注意:这里的normal.directRoutingKey,当消息变为死信并投递到死信交换机时,会沿用之前的RoutingKey,这样dlx.direct才能正确路由消息。

注意:RabbitMQ的消息过期是基于追溯方式来实现的,也就是说当一个消息的TTL到期以后不一定会被移除或投递到死信交换机,而是在消息恰好处于队首时才会被处理。
当队列中消息堆积很多的时候,过期消息可能不会被按时处理,因此你设置的TTL时间不一定准确

2、DelayExchange 延迟消息插件

在这里插入图片描述
基于死信队列虽然可以实现延迟消息,但是太麻烦了。

因此RabbitMQ社区提供了一个延迟消息插件来实现相同的效果。

官方文档说明:https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq
插件下载:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange

由于练习测试安装的MQ是3.8版本,因此这里下载3.8.17版本:

安装插件

首先:查看RabbitMQ的插件目录对应的数据卷

  • docker volume inspect mq-plugins

发现:插件目录被挂载到了/var/lib/docker/volumes/mq-plugins/_data这个目录,于是上传插件到该目录下。

上传后,执行命令,安装插件:

  • docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange

具体实现

首先,声明延迟交换机

/**
 * 延迟消息
 * @param msg
 */
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "delay.queue", durable = "true"),
        exchange = @Exchange(name = "delay.direct", delayed = "true"),
        key = "delay"
))
public void listenDelayMessage(String msg) {
    log.info("接收到delay.queue的延迟消息:{}", msg);
}

其次,发送延迟消息

(基于注解声明,若未安装MQ延迟插件Spring会报错,无法声明队列)

    /**
     * 测试发送延迟消息(延迟插件实现)
     */
    @Test
    void testSendDelayMessageByPlugin() {
        // 不能用Message转换器,因为Message是转换成字节,而消费者已经设置消息转换器了
        rabbitTemplate.convertAndSend("delay.direct", "delay", "hello", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 设置过期消息
                message.getMessageProperties().setExpiration("10000");
                return message;
            }
        });
    }

注意:
延迟消息插件内部会维护一个本地数据库表,同时使用Elang Timers功能实现计时。如果消息的延迟时间设置较长,可能会导致堆积的延迟消息非常多,会带来较大的CPU开销,同时延迟消息的时间会存在误差。
因此,不建议设置延迟时间过长的延迟消息。

练习代码参考黑马商城

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值