延时队列之Spring Boot整合RabbitMQ实现

99 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

需求

马上就要告别2022迎来2023了,与之伴随而来的一个需求就是用户的“年终回顾”。至于这个页面多花里胡哨,数据怎么统计都不是我考虑的重点,我唯一关心的是,当这个消息推送给用户后,带来的并发是不是系统能扛得住的。毕竟谁也不想元旦还要加班呀。解决因消息导致的并发,短时间内增加系统并发的可行性较低,另一个方案就是把消息进行分散发送,不同地区的用户收到的消息的时间不一致,以此来主动降低可能会造成的并发情况。

解决方案

实现这个方案的方式有很多,比如定时任务,但是根据上述需求,需要对不同地区的人建立不同的定时任务,较为繁琐,有没有其它对操作简单点的方案呢?这就引出了延时队列了,业务生产者根据用户的地区把用户发送消息的时间计算好后,一次性发送出去,业务消费者按需消费,发送消息即可。

技术方案

那解决方案定完了,接下来要定技术方案了。实现延时队列的技术方案也有很多,比如java自带的DelayQueue,DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这个需要一个线程,不断的循环队列。但是它是基于java内存的,数据量较大时会产生OOM,因此它只适合单机且数据量不大的场景中,不太适合我们。

下面开始介绍基于中间件RabbitMQ去实现延时队列。 阅读前题:需要了解RabbitMQ相关功能

两个特性

TTL Extensions

RabbitMQ可以为消息或者队列设置TTL(time to live),也就是过期时间,消息会经过TTL秒后,成为Dead Letter

Dead Letter Exchange

被设置了TTL的消息在过期后会成为Dead Letter,如果队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新publish到Dead Letter Exchange,通过Dead Letter Exchange路由到其他队列。

流程图

由此,就出来了下图的方案。生产者发送的消息首先会发送到缓存队列,通过RabbitMQ提供的TTL扩展,这些消息在过期后,通过DLX再转发给实际的消息队列,供消费者直接消费,以此实现延迟消费的效果。

image.png

实践

因为Exchange和Queue的创建代码很简单且固定,为了方便,这里以UI界面来演示

新建交换机

在Exchanges里创建DLX

image.png

新建队列

RabbitMQ可以在消息和队列上配置TTL,在队列上配置,则整个队列的TTL一致,在消息里配置,则可以不同消息具备不同的TTL

在Queues界面创建以下3个队列:

  • delay_message_process:实际消费队列

image.png

  • delay_message_ttl:消息的缓冲队列 队列的Arguments里配置2组参数,一个是x-dead-letter-exchange=DLX,另一个是x-dead-letter-routing-key=delay_message_process image.png
  • delay_queue_ttl:队列上的缓冲队列 队列TTL除了上面两个参数外,还要再增加一个配置x-message-ttl,这是TTL的时间,这里填入10000ms来演示 image.png

队列绑定交换机

DLX绑定delay_message_process image.png

测试验证

新建一个项目,引入MAVEN

<dependency>
   <groupId>org.springframework.amqp</groupId>
   <artifactId>spring-rabbit</artifactId>
</dependency>

编写一个消费者

@Component
public class RConsumer {

    @RabbitListener(queues = "delay_message_process")
    public void handleMessage(String message){
        System.out.println("接收消息"+message+"___时间:"+ LocalDateTime.now());
    }
}

首先测试队列上的缓冲队列delay_queue_ttl

@Test
public void test(){
    rabbitTemplate.convertAndSend("delay_queue_ttl", (Object) "队列上的缓冲队列");
    System.out.println("发送消息时间"+LocalDateTime.now());
}

测试结果

image.png

image.png

可以看到发送与接收消息相差之前配置的10S时间

再测试一下消息上的缓冲队列delay_message_ttl,建立一个配置类,将TTL时间做参数

public class ExpirationMessagePostProcessor implements MessagePostProcessor {

    private final Long ttl;

    public ExpirationMessagePostProcessor(Long ttl) {
        this.ttl = ttl;
    }
    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        // 设置per-message的失效时间
        message.getMessageProperties()
                .setExpiration(ttl.toString());

        return message;
    }
}
@Test
public void test(){
    rabbitTemplate.convertAndSend("delay_message_ttl", (Object) "队列上的缓冲队列",
            new ExpirationMessagePostProcessor( 5000L));
    System.out.println("发送消息时间"+LocalDateTime.now());
}

image.png

image.png

可以看到发送与收消息之间相差5秒

至此,整个延时队列的2种形式都展示出来了。可是这里有个问题,RabbitMQ的消息是先进先出的,如果队头的消息阻塞了,那么后续的消息都会阻塞。

比如我们同时发送一个30S的消息和一个3S的消息

@Test
public void test(){
    rabbitTemplate.convertAndSend("delay_message_ttl", (Object) "30S的消息",
            new ExpirationMessagePostProcessor( 30000L));
    rabbitTemplate.convertAndSend("delay_message_ttl", (Object) "3S的消息",
            new ExpirationMessagePostProcessor( 3000L));
    System.out.println("发送消息时间"+LocalDateTime.now());
}

image.png

image.png

可以看到,因为第一条30S的消息阻塞了,所以3S的消息在30S的消费后才紧接着发送出来。那能不能解决这个阻塞的问题呢?其实RabbitMQ通过插件解决了这个问题

插件

GitHub - rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ插件地址

下载对应版本下来后,放到plugin目录下面,并执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange就安装成功了

image.png

这时Exchange里就会多一个选项x-delayed-message

image.png

新建一个Exchange xxx,配置参数x-delayed-type=direct

image.png

新建一个Queue xxx-delay并绑定 xxx,修改消费者消费xxx-delay队列

修改一下ExpirationMessagePostProcessor配置消息的请求头

public class ExpirationMessagePostProcessor implements MessagePostProcessor {

    private final Long ttl;

    public ExpirationMessagePostProcessor(Long ttl) {
        this.ttl = ttl;
    }
    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        
//        message.getMessageProperties()
//                .setExpiration(ttl.toString());
        message.getMessageProperties().setHeader("x-delay",ttl.toString());
        return message;
    }
}

测试结果

@Test
public void test2(){
    rabbitTemplate.convertAndSend("xxx","xxx_delay", (Object) "30S的消息",
            new ExpirationMessagePostProcessor( 30000L));
    rabbitTemplate.convertAndSend("xxx","xxx_delay", (Object) "3S的消息",
            new ExpirationMessagePostProcessor(3000L));
    System.out.println(LocalDateTime.now());
}

image.png

image.png

大功告成!

关注我,下次更新redis实现延时队列