RabbitMQ高级特性之延迟队列
前言
比如说在淘票票上面买电影票,锁定了一个座位后系统默认会帮你保留15分钟时间,如果15分钟后还没付款那么不好意思系统会自动把座位释放掉。怎么实现类似的功能呢?
又或者说,我们在淘宝上买了一件商品,30分钟没付款(但会锁定库存),30分钟之后自动取消,然后自动取消库存。
类似于中场景我们该如何取实现呢?
- 首先老方法,定时器轮训
- 使用ttl机制以及私信队列
上篇文章中我们实现了ttl机制以及ttl机制后消息过期然后丢如死信队列中对消息进行处理,这种适用于消息队列中消息过期时间一致或者消息过期时间相同的场景,如果各个消息过期时间不同呢?
如下图对每条消息设置过期时间,这样就意味着每条消息的过期时间不相同。
如果我们现在设置的消息如下队列所示,5秒和 1秒的两条消息,此时5秒的过期了,于此期间1秒的消息早就过期了,这样会导致5秒的这个消息还没从队列里删除,1秒的就已经过期了,消费者肯定消费不到。
对于这种情况,该如何解决?
首先生产者在发送消息时候,是首先发送到交换机,然后交换根据不同的key发送到绑定次交换机的不同队列。
那能否在交换器那边根据消息多起时间发送到不同的队列呢?答案是肯定的,mq中提供了这一个操作— 延迟交换器 。
延迟交换器
- 生产者将消息(msg)和路由键(routekey)发送指定的延时交换机(exchange)上
- 延时交换机(exchange)存储消息等待消息到期根据路由键(routekey)找到绑定自己的队列(queue)并把消息给它
- 队列(queue)再把消息发送给监听它的消费者(customer)
插件下载地址
将插件拷贝到rabbitmq-server的安装路径:/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.4/plugins
先启动插件rabbitmq-plugins enable rabbitmq_delayed_message_exchange
再启动mq systemctl restart rabbitmq-server
或者 sbin目录下 启动sudo ./rabbitmq-server;
代码实现
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot整合rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
定义mq以及使用插件 监听mq
package com.example.demo.config;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.amqp.core.*;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableRabbit//开启mq监听
@ComponentScan("com.example.demo")
public class RabbitConfig {
@Bean
public Queue queue() {
Queue queue = new Queue("q.delayed", false, false, false, null);
return queue;
}
@Bean
public Exchange exchange() {
Map<String, Object> props = new HashMap<>();
//延迟交换器类型
props.put("x-delayed-type", ExchangeTypes.FANOUT);
//"x-delayed-message"表示使用 delayed exchange 插件处理消息
Exchange exchange = new CustomExchange("ex.delayed", "x-delayed-message", true, false, props);
return exchange;
}
@Bean
public Binding binding() {
return new Binding("q.delayed",
Binding.DestinationType.QUEUE, "ex.delayed",
"key.delayed", null);
//return BindingBuilder.bind(queue()).to(exchange()).with("key.delayed").noargs();
}
//连接工厂 RabbitAdmin
@Bean
@Autowired
public RabbitAdmin rabbitAdmin(ConnectionFactory factory) {
return new RabbitAdmin(factory);
}
//注册RabbitTemplate
@Bean
@Autowired
public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
return new RabbitTemplate(factory);
}
//设置属性
@Bean
@Autowired
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory
= new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
//设置手动确认 也可以在配置文件里设置
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
}
监听消费
package com.example.demo.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
import java.io.IOException;
@Component
public class MeetingListener {
@RabbitListener(queues = "q.delayed") //监听推送消息
public void broadcastMeetingAlarm(Message message, Channel channel) throws IOException {
System.err.println("提醒:5秒后:" + new String(message.getBody(), "utf-8"));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
web
生产不同延迟时间消费的消息
package com.example.demo.web;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
@RestController
public class PublishController {
@Autowired
private AmqpTemplate rabbitTemplate;
@RequestMapping("/test/{seconds}")
public String test(@PathVariable Integer seconds) throws UnsupportedEncodingException {
MessageProperties properties = new MessageProperties();
properties.setHeader("x-delay", (seconds - 10) * 1000);
Message message = new Message((seconds + "消费消息。").getBytes("utf-8"), properties);
// 如果不设置message的properties,也可以使用下述方法设置x-delay属性的 值
rabbitTemplate.convertAndSend("ex.delayed", "key.delayed", message, msg -> {
// // 使用定制的属性x-delay设置过期时间,也就是提前5s提醒
// // 当消息转换完,设置消息头字段
msg.getMessageProperties().setHeader("x-delay", (seconds - 5) * 1000);
return msg;
});
rabbitTemplate.convertAndSend("ex.delayed", "key.delayed", message);
return "已经设置好延迟消费的消息";
}
}
分别发送不同时间的消息
http://localhost:8080/api/test/100
http://localhost:8080/api/test/15
分别发送100s和15s后消费消息的消息,可以看到,15s的消息先被消息,100s的消息后被消息。
不同点
ttl 是将消息放在死信队列里面,队列设置过期时间或者消息设置过期时间,面向队列。
延迟交换器是基于插件存放消息在延时交换机里,等消息快过期时,将消息发送给队列,是面向交换器的。