一、什么是延迟队列
即消息进入延迟队列后,不会立即消费,会到达指定时间之后在消费。
应用场景:
用户下单后半个小时,需要判断订单是否支付金额,没有支付就取消订单
新用户注册七天后,需要发送短信问候
二、延迟队列的实现
延迟队列是由TTL+DLX(死信交换机)一起组成实现的。
TTL是设置队列消息的过期时间,有两种设置方式:
方式1:设置整个队列的过期时间,整个队列都统一过期时间,设置参数x-message-ttl,单位ms(毫秒)。
方式2:对单个消息设置过期时间,设置参数expiration,单位ms(毫秒),意思就是队列的消息过期时间都是独自设置的。
什么是死信交换机死信队列:首先需要两个交换机两个队列,一个交换机是普通交换机,一个交换机是死信交换机(Dead Letter Exchange),一个队列是普通队列,一个队列是私信队列(DLX).。生产者发送一条消息到普通交换机,普通交换机在发送给普通的队列,按照正常的逻辑普通队列需要绑定一个消费者,但是死信就不需要,普通队列需要绑定死信交换机,并且指定死信交换机的rontingKey,普通队列将消息发送给死信交换机,死信交换机再绑定死信队列,死信队列接收到死信交换机发送来的消息,再给绑定了死信队列的消费者消费。
上面流程哪里只讲了,普通队列将死信消息转发到死信交换机,下面就讲讲怎样才能成为死信
消息成为死信的三种情况:
1、队列消息长度达到限制
2、消费者拒接消费消息,并且不重回队列
3、队列时间存在ttl的时间设置,消费到达超时时间未被消费
三、延迟队列的案例
项目结构:
pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!-- 引入阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
<!--swagger文档-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger文档-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
配置application.yml
server:
tomcat:
max-http-form-post-size: -1 #base64图片上传大小
port: 8005 #测试服务器 打包
spring:
servlet:
multipart:
max-file-size: 50MB #单个数据大小
max-request-size: 100MB #总数据大小
rabbitmq:
port: 5672
username: wdp123
password: wdp123
addresses: 39.107.90.1
virtual-host: wdp
publisher-confirms: true
publisher-returns: true
listener:
simple:
acknowledge-mode: manual
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://120.24.70.201:33106/jypt?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
username: develop
password: Vhcat%@32105464567
type: com.alibaba.druid.pool.DruidDataSource
在com.wdp.rabbitmq.swagger包下创建Swagger2类
package com.wdp.rabbitmq.swagger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2 {
// http://127.0.0.1:8080/swagger-ui.html
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.wdp.rabbitmq"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot中使用spring-boot-starter-amqp集成rabbitmq")
.description("测试SpringBoot整合进行各种工作模式信息的发送")
/*
.termsOfServiceUrl("https://www.jianshu.com/p/c79f6a14f6c9")
*/
.contact("roykingw")
.version("1.0")
.build();
}
}
创建启动类OrderMqApplication
package com.wdp.rabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderMqApplication {
public static void main(String[] args) {
SpringApplication.run(OrderMqApplication.class,args);
}
}
在com.wdp.rabbitmq.entity包下创建Order
package com.wdp.rabbitmq.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class Order implements Serializable {
private static final long serialVersionUID = -2221214252163879885L;
private String orderId; // 订单id
private Integer orderStatus; // 订单状态 0:未支付,1:已支付,2:订单已取消
private String orderName; // 订单名字
}
在com.wdp.rabbitmq.service包下创建订单接口
package com.wdp.rabbitmq.service;
import com.wdp.rabbitmq.entity.Order;
import org.springframework.stereotype.Service;
public interface DelaySenderService {
public void sendDelay(Order order);
//public void sendDelay2(Order order);
}
在com.wdp.rabbitmq.service.impl包下创建DelaySenderServiceImpl类
package com.wdp.rabbitmq.service.impl;
import com.wdp.rabbitmq.config.DelayRabbitConfig;
import com.wdp.rabbitmq.entity.Order;
import com.wdp.rabbitmq.service.DelaySenderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.UUID;
@Service
@Slf4j
public class DelaySenderServiceImpl implements DelaySenderService {
// AMQP 高级消息队列协议
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendDelay(Order order) {
log.info("【订单生成时间1】" + new Date().toString() +"【100毫秒后检查订单是否已经支付】" + order.toString() );
rabbitTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE,DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY,order,message -> {
message.getMessageProperties().setExpiration(1 * 1000 * 6 + "");
return message;
},new CorrelationData(UUID.randomUUID().toString()));
}
// @Override
// public void sendDelay2(Order order) {
// log.info("【订单生成时间2】" + new Date().toString() +"【1分钟后检查订单是否已经支付】" + order.toString() );
// rabbitTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE,DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY,order,message -> {
// message.getMessageProperties().setExpiration(1 * 10000 * 6 + "");
// return message;
// },new CorrelationData(UUID.randomUUID().toString()));
// }
}
重点来了,在com.wdp.rabbitmq.config包下创建DelayRabbitConfig类,用于配置死信交换机和死信队列
package com.wdp.rabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@Slf4j
public class DelayRabbitConfig {
// 延迟队列 TTL 名称
private static final String ORDER_DELAY_QUEUE = "order.delay.queue";
// DLX,dead letter发送到的 exchange
// 延时消息就是发送到该交换机的
public static final String ORDER_DELAY_EXCHANGE = "order.delay.exchange";
// routing key 名称
// 具体消息发送在该 routingKey 的
public static final String ORDER_DELAY_ROUTING_KEY = "order_delay";
//立即消费的队列名称
public static final String ORDER_QUEUE_NAME = "order.queue";
// 立即消费的exchange
public static final String ORDER_EXCHANGE_NAME = "order.exchange";
//立即消费 routing key 名称
public static final String ORDER_ROUTING_KEY = "order";
//创建一个延时队列需要死信交换机绑定
@Bean
public Queue delayOrderQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY);
return new Queue(ORDER_DELAY_QUEUE, true, false, false, params);
}
// 创建一个死信队列
@Bean
public Queue orderQueue() {
// 第一个参数为queue的名字,第二个参数为是否支持持久化
return new Queue(ORDER_QUEUE_NAME, true);
}
//创建延迟交换机
@Bean
public DirectExchange orderDelayExchange() {
// 一共有三种构造方法,可以只传exchange的名字, 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除,
// 第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
// new DirectExchange(ORDER_DELAY_EXCHANGE,true,false);
return new DirectExchange(ORDER_DELAY_EXCHANGE);
}
//创建死信交换机
@Bean
public TopicExchange orderTopicExchange() {
return new TopicExchange(ORDER_EXCHANGE_NAME);
}
// 把延时队列delayOrderQueue和订单延迟交换的orderDelayExchange进行绑定
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
}
// 把死信队列orderQueue和死信交换的exchange进行绑定
@Bean
public Binding orderBinding() {
// TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键
return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
}
}
在com.wdp.rabbitmq.config包下创建RabbitMQConfirmAndReturn类配置消息的确认和回退机制
package com.wdp.rabbitmq.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class RabbitMQConfirmAndReturn implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this); //指定 ConfirmCallback
rabbitTemplate.setReturnCallback(this); //指定 ReturnCallback
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息发送成功" + correlationData+"----"+"cause:"+cause);
} else {
System.out.println("消息发送失败:" + cause);
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
// 反序列化对象输出
System.out.println("消息主体: " + new String(message.getBody()));
System.out.println("应答码: " + replyCode);
System.out.println("描述:" + replyText);
System.out.println("消息使用的交换器 exchange : " + exchange);
System.out.println("消息使用的路由键 routing : " + routingKey);
}
}
在com.wdp.rabbitmq.receiver包下创建DelayReceiver类配置死信队列的消费者,并签收
package com.wdp.rabbitmq.receiver;
import com.rabbitmq.client.Channel;
import com.wdp.rabbitmq.config.DelayRabbitConfig;
import com.wdp.rabbitmq.entity.Order;
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.io.IOException;
import java.util.Date;
@Component
@Slf4j
public class DelayReceiver {
@RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME})
public void orderDelayQueue(Order order, Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
log.info("订单手动签收消息确认 {}",message);
log.info("【orderDelayQueue 监听的消息】 - 【消费时间】 - [{}]- 【订单内容】 - [{}]", new Date(), order.toString());
if (order.getOrderStatus() == 0) {
order.setOrderStatus(2);
log.info("【该订单未支付,取消订单】" + order.toString());
} else if (order.getOrderStatus() == 1) {
log.info("【该订单已完成支付】");
} else if (order.getOrderStatus() == 2) {
log.info("【该订单已取消】");
}
channel.basicAck(deliveryTag,true);
log.info("签收成功:{}",deliveryTag);
}catch (Exception e){
/*
第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
*/
channel.basicNack(deliveryTag,true,false);
}
}
}
在com.wdp.rabbitmq.controller包下创建订单的controller
package com.wdp.rabbitmq.controller;
import com.wdp.rabbitmq.entity.Order;
import com.wdp.rabbitmq.service.DelaySenderService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private DelaySenderService delaySenderService;
@GetMapping("/sendDelay")
@ApiOperation(value = "下单")
public Object sendDelay() {
Order order1 = new Order();
order1.setOrderStatus(0);
order1.setOrderId("123321123");
order1.setOrderName("这是order1时间短的");
Order order2 = new Order();
order2.setOrderStatus(1);
order2.setOrderId("2345123123");
order2.setOrderName("这是order2时间长的");
Order order3 = new Order();
order3.setOrderStatus(2);
order3.setOrderId("983676");
order3.setOrderName("小米alpan阿尔法");
delaySenderService.sendDelay(order1);
delaySenderService.sendDelay(order2);
delaySenderService.sendDelay(order3);
return "test--ok";
}
}
延迟队列有个注意事项:
那就是不能把不同的时间往同一个队列里面塞,我们来举个例子,因为消息队列是先进先出,如果我同时传入三个消息A、B、C,A为3个小时,B为两个小时,C为1个小时,这里就要出问题,B和C到了时间后根本消费不了,因为他们前面有A为3个小时的消息,必须A消费了才能放出B和C,总结出了一点,前面的消息时间不能大于后面的消息时间,这里我也做了一个测试,把sendDelay2方法放开,在controller里面调用,时间设置为不一样,就可以做测试了,这里就不演示了。