RabbitMQ使用延迟队列(通俗易懂)
场景
延迟消息是指的消息发送出去后并不想立即就被消费,而是需要等(指定的)一段时间后才触发消费。
- 订单创建成功后,需要30分钟内支付成功。就可以用延迟队列,订单创建成功后发送一个延迟消息,这条消息30分钟后才能被消费,消费的时候去查询订单状态是否是已支付。
- 公司预约会议,22点有会议,21:45(提前15分钟)就通知参会人员最好准备,快开会了。
实现方式
延迟队列在AMQP协议和RabbitMQ中都没有相关的规定和实现。
- 可以借助“死信队列”来变相的实现(消息到期后加入死信队列,然后定义一个消费者消费死信队列的消息即可。PS:代码中有完整实现);
- 可以使用rabbitmq_delayed_message_exchange插件实现。
下面我们用插件的方式来实现延迟队列
延迟消息消费流程:
- 生产者将消息(msg)和路由键(routekey)发送指定的延迟交换机(exchange)上
- 延迟交换机(exchange)存储消息等待消息到期根据路由键(routekey)找到绑定自己的队列(queue)并把消息给它(消息到期后才会到达队列)
- 队列(queue)再把消息发送给监听它的消费者(customer)
文末有代码git地址(包含路由(Direct)模式、Work模式、主题(Topic)模式、发布订阅/广播(Fanout)模式、TTL、死信队列、延迟队列)
安装延迟队列插件
安装RabbitMQ教程:Mac安装RabbitMQ
1、下载插件
下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
2、将需要安装的插件拷贝到插件位置
/usr/local/Cellar/rabbitmq/3.9.7/plugins
然后查看插件列表
rabbitmq-plugins list
结果如下
[E*]、[e*]都是已安装的,其他都是未安装的
3、启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
再次查看插件列表
4、重启RabbitMQ
# 关闭RabbitMQ
rabbitmqctl stop
# 后台启动RabbitMQ
rabbitmq-server -detached
SpringBoot整合RabbitMQ使用延迟队列
1、添加依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--amqp依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies>
2、添加配置文件application.yml
server: port: 8899 spring: rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest publisher-confirm-type: correlated # 消息发送到交换机确认机制,是否确认回调。correlated&simple相当于publisher-confirms: true,none相当于publisher-confirms: false publisher-returns: true # 消息发送到队列确认机制,是否确认回调(消息发送失败返回队列中) virtual-host: / #连接到rabbitMQ的vhost # 如果使用自定义监听器,则下面的配置可以注释掉(也可以不用管) listener: simple: acknowledge-mode: manual #manual:采用手动应答;none:不确认;auto:自动确认,若采用手动确认,可以自定义监听器,不使用@RabbitListener注解来消费消息 prefetch: 1 #限制每次发送一条数据。 concurrency: 1 #指定最小消费者数量 max-concurrency: 1 #指定最大消费者数量 default-requeue-rejected: false #重试超过最大次数后是否拒绝 retry: enabled: true #是否开启消费者重试(为false时关闭消费者重试,意思不是“不重试”,而是消费端代码异常会一直重复收到消息直到ack确认或者一直到超时) max-attempts: 5 #最大重试次数 initial-interval: 500 #重试间隔时间(单位毫秒)第一次和第二次尝试发布或传递消息之间的间隔 max-interval: 1000 #重试最大时间间隔(单位毫秒) multiplier: 5 #应用于上一重试间隔的乘数 logging: level: com: qjc: mq: debug
3、定义常量
public class RabbitMQConstant { /** * 延迟交换器 */ public static final String EXCHANGE_DELAY = "delay_exchange"; /** * 延迟队列 */ public static final String QUEUE_DELAY = "delay.queue"; /** * 延迟队列路由key */ public static final String ROUTING_KEY_DELAY = "routing.key.delay"; }
4、定义交换机
package com.qjc.mq.config; import com.qjc.mq.constant.RabbitMQConstant; import org.springframework.amqp.core.DirectExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Description: * @Author: qjc * @Date: 2021/12/6 */ @Configuration public class ExchangeConfig { /** * 交换机说明: * durable="true" rabbitmq重启的时候不需要创建新的交换机 * auto-delete 表示交换机没有在使用时将被自动删除 默认是false * direct交换器相对来说比较简单,匹配规则为:如果路由键匹配,消息就被投送到相关的队列 * topic交换器你采用模糊匹配路由键的原则进行转发消息到队列中 * fanout交换器中没有路由键的概念,他会把消息发送到所有绑定在此交换器上面的队列中。 */ @Bean(name = RabbitMQConstant.EXCHANGE_DELAY) public DirectExchange delayExchange() { DirectExchange directExchange = new DirectExchange(RabbitMQConstant.EXCHANGE_DELAY, true, false); directExchange.setDelayed(true); return directExchange; // // 使用自定义交换器 // Map<String, Object> props = new HashMap<>(2); // props.put("x-delayed-type", ExchangeTypes.FANOUT); // return new CustomExchange(RabbitMQConstant.EXCHANGE_DELAY, "x-delayed-message", true, false, props); } }
5、定义队列
package com.qjc.mq.config; import com.qjc.mq.constant.RabbitMQConstant; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Description: * @Author: qjc * @Date: 2021/12/6 */ @Configuration public class QueueConfig { /** * durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列 * exclusive 表示该消息队列是否只在当前connection生效,默认是false * auto-delete 表示消息队列没有在使用时将被自动删除 默认是false */ @Bean(name = RabbitMQConstant.QUEUE_DELAY) public Queue delayQueue() { return new Queue(RabbitMQConstant.QUEUE_DELAY, true, false, false); } }
6、绑定
package com.qjc.mq.config; import com.qjc.mq.callbackconfig.MsgSendConfirmCallback; import com.qjc.mq.callbackconfig.MsgSendReturnCallback; import com.qjc.mq.constant.RabbitMQConstant; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.annotation.Resource; /** * @Description: * @Author: qjc * @Date: 2021/12/6 */ @Configuration @Slf4j public class RabbitMqConfig { @Resource private QueueConfig queueConfig; @Resource private ExchangeConfig exchangeConfig; @Resource private ConnectionFactory connectionFactory; @Bean public Binding bindingDelay() { // 使用自定义交换器时后面需要添加.noargs() // return BindingBuilder.bind(queueConfig.delayQueue()).to(exchangeConfig.delayExchange()).with(RabbitMQConstant.ROUTING_KEY_DELAY).noargs(); return BindingBuilder.bind(queueConfig.delayQueue()).to(exchangeConfig.delayExchange()).with(RabbitMQConstant.ROUTING_KEY_DELAY); } /** ======================== 定制一些处理策略 =============================*/ /** * 定制化amqp模版 * <p> * Rabbit MQ的消息确认有两种。 * <p> * 一种是消息发送确认:这种是用来确认生产者将消息发送给交换机,交换机传递给队列过程中,消息是否成功投递。 * 发送确认分两步:一是确认是否到达交换机,二是确认是否到达队列 * <p> * 第二种是消费接收确认:这种是确认消费者是否成功消费了队列中的消息。 */ @Bean public RabbitTemplate rabbitTemplate() { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); // 消息发送失败返回到队列中, yml需要配置 publisher-returns: true rabbitTemplate.setMandatory(true); /** * 使用该功能需要开启消息确认,yml需要配置 publisher-confirms: true * 通过实现ConfirmCallBack接口,用于实现消息发送到交换机Exchange后接收ack回调 * correlationData 消息唯一标志 * ack 确认结果 * cause 失败原因 */ rabbitTemplate.setConfirmCallback(new MsgSendConfirmCallback()); /** * 使用该功能需要开启消息返回确认,yml需要配置 publisher-returns: true * 通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发 * message 消息主体 message * replyCode 消息主体 message * replyText 描述 * exchange 消息使用的交换机 * routingKey 消息使用的路由键 */ rabbitTemplate.setReturnCallback(new MsgSendReturnCallback()); return rabbitTemplate; } }
7、消息发送成功的处理策略
package com.qjc.mq.callbackconfig; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Component; /** * @Description: 消息发送到交换机确认机制 * @Author: qjc * @Date: 2021/12/7 */ @Component @Slf4j public class MsgSendConfirmCallback implements RabbitTemplate.ConfirmCallback { /** * 使用该功能需要开启消息确认,yml需要配置 publisher-confirms: true * correlationData 消息唯一标志 * ack 确认结果 * cause 失败原因 * <p> * PS:通过实现ConfirmCallBack接口,用于实现消息发送到交换机Exchange后接收ack回调 * </p> */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (ack) { log.debug("消息发送到exchange成功,id: {}", correlationData.getId()); } else { log.debug("消息{}发送到exchange失败,原因: {}", correlationData.getId(), cause); } } }
package com.qjc.mq.callbackconfig; import com.qjc.mq.constant.RabbitMQConstant; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Component; /** * @Description: * @Author: qjc * @Date: 2021/12/7 */ @Component @Slf4j public class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback { /** * 使用该功能需要开启消息返回确认,yml需要配置 publisher-returns: true * message 消息主体 message * replyCode 消息主体 message * replyText 描述 * exchange 消息使用的交换机 * routingKey 消息使用的路由键 * <p> * PS:通过实现ReturnCallback接口,如果消息从交换机发送到对应队列失败时触发 * </p> */ @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { if (exchange.equals(RabbitMQConstant.EXCHANGE_DELAY)) { // 如果配置了发送回调ReturnCallback,rabbitmq_delayed_message_exchange插件会回调该方法,因为发送方确实没有投递到队列上,只是在交换器上暂存,等过期/时间到了才会发往队列。 // 所以如果是延迟队列的交换器,则直接放过,并不是bug return; } String correlationId = message.getMessageProperties().getCorrelationId(); log.debug("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {} 路由键: {}", correlationId, replyCode, replyText, exchange, routingKey); } }
8、定义消息发送者
package com.qjc.mq.mqsender.delay; import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; import com.qjc.mq.constant.RabbitMQConstant; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageBuilder; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Date; import java.util.UUID; /** * @ClassName: DelaySender * @Description: * @Author: qjc * @Date: 2021/12/9 11:42 上午 */ @Component public class DelaySender { @Resource RabbitTemplate rabbitTemplate; public String send(Integer seconds) { DateTime dateTime = DateUtil.offsetSecond(new Date(), seconds); String msg = "通知时间(" + DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss") + "),通知内容(" + DateUtil.formatDateTime(dateTime) + "召开会议)"; Message message = MessageBuilder.withBody(msg.getBytes()).build(); // RabbitMQ只会检查队列头部的消息是否过期,如果过期就放到死信队列 // 假如第一个过期时间很长,10s,第二个消息3s,则系统先看第一个消息,等 到第一个消息过期,放到DLX // 此时才会检查第二个消息,但实际上此时第二个消息早已经过期了,但是并没 有先于第一个消息放到DLX。 // 插件rabbitmq_delayed_message_exchange帮我们搞定这个。 MessageProperties messageProperties = message.getMessageProperties(); // 设置到期时间,也就是提前10s提醒 messageProperties.setDelay((seconds - 10) * 1000); rabbitTemplate.convertAndSend(RabbitMQConstant.EXCHANGE_DELAY, RabbitMQConstant.ROUTING_KEY_DELAY, message, new CorrelationData(UUID.randomUUID().toString().replaceAll("-", ""))); return seconds + "秒后召开会议,已经定好闹钟了,到时提前告诉大家"; } }
9、定义消息消费者
package com.qjc.mq.mqreceiver.delay; import com.qjc.mq.constant.RabbitMQConstant; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; /** * @ClassName: DelayReceiver * @Description: * @Author: qjc * @Date: 2021/12/9 11:39 上午 */ @Component @Slf4j public class DelayReceiver { @RabbitListener(queues = RabbitMQConstant.QUEUE_DELAY) public void process(Message message, Channel channel) throws Exception { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.err.println("接到通知【" + new String(message.getBody(), "utf-8") + "】,接收时间【" + simpleDateFormat.format(new Date()) + "】"); long deliveryTag = message.getMessageProperties().getDeliveryTag(); channel.basicAck(deliveryTag, false); } }
10、创建controller
package com.qjc.mq.controller; import com.qjc.mq.mqsender.delay.DelaySender; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; /** * @Description: * @Author: qjc * @Date: 2021/12/6 */ @RestController @CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.DELETE, RequestMethod.PUT}) public class MqSenderController { /** * rabbitMQ 的核心就是 交换器,路由键,队列; * <p> * 消息来了之后,会发送到交换器,交换器将根据路由键将消息发送到相对应的队列里面去! * <p> * 消息不用关系到达了那个队列.只需要带着自己的路由键到了交换器就可以了,交换器会帮你把消息发送到指定的队列里面去! */ @Resource DelaySender delaySender; /** * 测试延迟队列: http://localhost:8899/delay/20 * * @param seconds 多少秒后开会 */ @RequestMapping(value = "/delay/{seconds}", method = {RequestMethod.GET}) public String delaySender(@PathVariable Integer seconds) { return delaySender.send(seconds); } }
结果如下
完整代码地址
(包含路由(Direct)模式、Work模式、主题(Topic)模式、发布订阅/广播(Fanout)模式、TTL、死信队列、延迟队列):
https://gitee.com/xiaorenwu_dashije/rabbitmq_demo.git
劈天造陆,开辟属于自己的天地!!!与君共勉