开启掘金成长之旅!这是我参与「掘金日新计划 · 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再转发给实际的消息队列,供消费者直接消费,以此实现延迟消费的效果。
实践
因为Exchange和Queue的创建代码很简单且固定,为了方便,这里以UI界面来演示
新建交换机
在Exchanges里创建DLX
新建队列
RabbitMQ可以在消息和队列上配置TTL,在队列上配置,则整个队列的TTL一致,在消息里配置,则可以不同消息具备不同的TTL
在Queues界面创建以下3个队列:
- delay_message_process:实际消费队列
- delay_message_ttl:消息的缓冲队列
队列的Arguments里配置2组参数,一个是x-dead-letter-exchange=DLX,另一个是x-dead-letter-routing-key=delay_message_process
- delay_queue_ttl:队列上的缓冲队列
队列TTL除了上面两个参数外,还要再增加一个配置x-message-ttl,这是TTL的时间,这里填入10000ms来演示
队列绑定交换机
DLX绑定delay_message_process
测试验证
新建一个项目,引入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());
}
测试结果
可以看到发送与接收消息相差之前配置的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());
}
可以看到发送与收消息之间相差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());
}
可以看到,因为第一条30S的消息阻塞了,所以3S的消息在30S的消费后才紧接着发送出来。那能不能解决这个阻塞的问题呢?其实RabbitMQ通过插件解决了这个问题
插件
GitHub - rabbitmq/rabbitmq-delayed-message-exchange: Delayed Messaging for RabbitMQ插件地址
下载对应版本下来后,放到plugin目录下面,并执行rabbitmq-plugins enable rabbitmq_delayed_message_exchange就安装成功了
这时Exchange里就会多一个选项x-delayed-message
新建一个Exchange xxx,配置参数x-delayed-type=direct
新建一个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());
}
大功告成!
关注我,下次更新redis实现延时队列