RabbitMQ如何实现延迟队列?

1. 业务场景

超过30分钟未付款的订单自动关闭,这个功能该如何实现?

思路:发一条跟订单相关的消息,30分钟以后被消费,在消费者的代码中查询订单数据,如果支付状态是未付款,就关闭订单。

问题来了,怎么实现在指定的时候之后消息才会发给消费者呢?

RabbitMQ本事不支持延迟投递,但是可以借助其他方法,总的来说有两种实现方案。

  • 先存储到数据库,然后用定时任务扫描
  • 利用RabbitMQ的死信队列实现

定时任务比较容易实现,比如每隔一分钟扫描一次,查出30分钟之前未付款的订单,把状态改为关闭。但是如果瞬间要处理的数据量过大,比如10万条,把这些数据都扫描一遍会给服务器带来很大的压力。所以不太建议使用。

利用死信队列怎么实现呢?

2. 死信队列(Message TTL)

2.1 队列属性

首先,队列有一个消息过期属性,通过设置这个属性,超过了指定时间的消息将会被丢弃。

这个属性叫x-message-ttl;

所以队列中的消息超过时间未被消费时都会过期。

@Bean("ttlQueue")
public Queue queue() {
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("x-message-ttl", 11000); // 队列中的消息未被消费11秒后过期
    // map.put("x-expire", 30000); // 队列30秒没有使用以后会被删除
    return new Queue("TTL_QUEUE", true, false, false, map);
}

但是这种方式似乎不是那么的灵活,所以RabbitMQ的消息也有单独的过期时间属性。

2.2 消息属性

在发送消息的时候通过MessageProperties指定相关属性。

MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("4000"); // 消息的过期属性,单位ms
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
Message message = new Message("这条消息4秒后过期".getBytes(), messageProperties);
rabbitTemplate.send("TTL_EXCHANGE", "ttl", message);

问题:如果队列TTL是6秒过期,消息TTL是10秒过期,这个消息会在什么时候丢弃?

答:如果同时指定了过期时间,则小的那个时间生效。

有了过期时间还不够,这个消息不能直接丢弃,我们应该将其丢入到一个指定的容器中,这样就可以实现延迟消费的功能了。

2.3 什么是死信?

消息过期以后,如果没有任何配置,是会直接丢弃的。我们可以通过配置让这样的消息变成死信,在别的地方存储。

2.3.1 死信会去哪里?

队列在创建的时候可以指定一个死信交换机DLX。死信交换机绑定的队列被称为死信队列DLQ,DLX实际上也是一个普通的交换机,DLQ也是一个普通的队列。

image-20211203112935967

也就是说,如果消息过期了,队列指定了DLX,消息就会发送到DLX;如果DLX绑定了DLQ,那么消息就会被路由到DLQ进行消费。

2.3.2 死信队列的使用

  • 声明原交换机(ORI_USE_EXCHANGE)、原队列(ORI_USE_QUEUE),相互绑定。然后指定原交换机的的私信交换机(DEAD_LETTER_EXCHANGE)

  • 声明死信交换机、死信队列(DEAD_LETTER_QUEUE),并且通过#绑定,代表无条件路由。

  • 最终消费者监听死信队列,在这实现订单检查逻辑。

  • 生产者发送消息,设置10秒过期。

public class DlxConsumer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 指定队列的死信交换机
        Map<String, Object> arguments = new HashMap<String, Object>();
        arguments.put("x-dead-letter-exchange", "DEAD_LETTER_EXCHANGE");
        // arguments.put("x-expires",9000L); // 设置队列的TTL
        // arguments.put("x-max-length", 4); // 如果设置了队列的最大长度,超过长度时,先入队的消息会被发送到DLX

        // 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare("ORI_USE_QUEUE", false, false, false, arguments);

        // 声明死信交换机
        channel.exchangeDeclare("DEAD_LETTER_EXCHANGE", "topic", false, false, false, null);
        // 声明死信队列
        channel.queueDeclare("DEAD_LETTER_QUEUE", false, false, false, null);
        // 绑定,此处 Dead letter routing key 设置为 #
        channel.queueBind("DEAD_LETTER_QUEUE", "DEAD_LETTER_EXCHANGE", "#");
        System.out.println(" Waiting for message....");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("Received message : '" + msg + "'");
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume("DEAD_LETTER_QUEUE", true, consumer);
    }
}
public class DlxProducer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        String msg = "Hello world, Rabbit MQ, DLX MSG";

        // 设置属性,消息10秒钟过期
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) // 持久化消息
                .contentEncoding("UTF-8")
                .expiration("10000") // TTL
                .build();

        // 发送消息
        channel.basicPublish("", "ORI_USE_QUEUE", properties, msg.getBytes());

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

2.3.3 总结

利用消息的过期时间,过期之后投递到DLX,路由到DLQ,监听DLQ,实现了延迟队列。

消息的流转流程如下:

生产者——》原交换机——》原队列——》超过时间后进入死信交换机——》私信队列——》最终消费者。

2.4 补充

除了消息过期,还有什么情况消息会变成死信?

  • 消息被消费者拒绝并且未设置重回队列
  • 队列达到最大长度,超过了Max Length或者超过最大字节数;最先入队的消息会被发送到死信队列。

3. 延迟队列的其他实现

使用死信队列实现延时消息的缺点:

  • 如果统一用队列来设置消息的TTL,当梯度非常多的情况下,比如1分钟,2分钟,5分钟…需要我们创建多个交换机和队列来进行路由消息。
  • 如果单独设置消息的TTL,则可能会造成队列中的消息阻塞——前一条消息没有出队(没有被消费),后面的消息则无法投递。(比如第一条消息TTL是30Min,第二条消息TTL是10Min。10分钟后,即使第二条消息该出队了,但是由于第一条消息还未出队,所以无法投递。)
  • 可能存在一定的时间误差。

在RabbitMQ3.5.7及以后的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延时队列功能。同时依赖插件Erlang/OPT 18.0及以上。

3.1 插件安装

  • 下载插件

    下载地址:https://www.rabbitmq.com/community-plugins.html

    image-20211203124027137

  • 将文件复制到插件目录:

    linux

    /usr/lib/rabbitmq/lib/rabbitmq_server-3.7.17/plugins
    

    windows

    C:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.17\plugins
    

    进入插件目录

    image-20211203123630504

  • 启动

    rabbitmq-plugins enable rabbitmq_delayed_message_exchange
    
  • 停用

    rabbitmq-plugins disable rabbitmq_delayed_message_exchange
    
  • 查看

    image-20211203124859765

3.2 插件使用

通过声明一个x-delayed-message类型的Exchange来使用插件特性。x-delayed-message是插件提供的类型,并不是rabbitmq自带的类型。(区别于direct,topic,fanout,headers);

public class DelayPluginConsumer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://guest:guest@127.0.0.1:5672");
        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 声明x-delayed-message类型的exchange
        Map<String, Object> argss = new HashMap<String, Object>();
        argss.put("x-delayed-type", "direct");
        channel.exchangeDeclare("DELAY_EXCHANGE", "x-delayed-message", false,
                false, argss);

        // 声明队列
        channel.queueDeclare("DELAY_QUEUE", false, false, false, null);

        // 绑定交换机与队列
        channel.queueBind("DELAY_QUEUE", "DELAY_EXCHANGE", "DELAY_KEY");

        // 创建消费者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                System.out.println("收到消息:[" + msg + "]\n接收时间:" + sf.format(new Date()));
            }
        };

        // 开始获取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume("DELAY_QUEUE", true, consumer);
    }
}
public class DelayPluginProducer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://guest:guest@127.0.0.1:5672");

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        // 延时投递,比如延时1分钟
        Date now = new Date();
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, +10);//
        Date delayTime = calendar.getTime();

        // 定时投递,把这个值替换delayTime即可
        // Date exactDealyTime = new Date("2019/01/14,22:30:00");

        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String msg = "发送时间:" + sf.format(now) + ",投递时间:" + sf.format(delayTime);

        // 延迟的间隔时间,目标时刻减去当前时刻
        Map<String, Object> headers = new HashMap<String, Object>();
        headers.put("x-delay", delayTime.getTime() - now.getTime());

        AMQP.BasicProperties.Builder props = new AMQP.BasicProperties.Builder()
                .headers(headers);
        channel.basicPublish("DELAY_EXCHANGE", "DELAY_KEY", props.build(),
                msg.getBytes());

        channel.close();
        conn.close();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

、楽.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值