场景: 比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。常用解决方案:.spring的schedule定时任务轮询数据库缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差解决: rabbitmq的消息TTL和死信Exchange结合
消息的TTL (Time To Live)
消息的TTL就是消息的存活时间
RabbitMQ可以对队列和消息分别设置TTL
对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x-message-tt属性来设置时间,两者是一样的效果。
死信路由Dead Letter Exchanges(DLX)
一个消息在满足如下务件下,会进死信路由,这里是路由而不是队列,一个路由可以对应很多a(什么是死信)一个消息被Consumer拒收了,开Ereeci的参数里requeue是false,也就是说不会被再次放在队列里,被其他消费者使用。(basicreject/ basic.nack) requeue=false上面的消息的TTL到了,消息过期了。队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列
使用消息TTL+死信路由就可以实现延时队列
实现方式1:队列设置所有消息的过期时间
这里实现的逻辑是生产者通过指定路由键给交换机发送一条消息,交换机将消息转发给队列,这个队列假设设置了5分钟的TTL也就是5分钟后进入队列的消息就会失效,同时,这个队列还指定了死信路由,而这个死信路由又会将失效的消息转发给指定的死信交换机,同时由死信交换机转发给专门接收过期消息的队列,在消费端又有指定的服务监听这个队列,所以这里的TTL5分钟的值也就是队列的延时时间,那么也就完成了消息的延时推送!
实现方式2:给每条消息单独设置过期时间
这个的核心原理和上面的队列设置TTL是差不多的,但是这里不建议使用消息设置TTL来实现延时队列!推荐使用队列设置过期时间,
原因: RabbitMq采用惰性检查机制,也就是懒检查机制,比如消息队列中存放了多条消息,第一条是5分钟过期,第二条是1分钟过期,第三条是1秒钟过期,按照正常的过期逻辑,应该是1秒过期的先排出这个队列,进入死信队列中,但是实际RabbitMQ是先拿第一条消息,也就是5分钟过期的,一看5分钟还没到过期时间,然后等待5分钟会将第一条消息拿出来,放入死信队列,这里就会出现问题,第二条设置1分钟的和第三条设置1秒钟的消息必须要等待第一条5分钟过期后才能过期,等待第一条消息过期5分钟了,拿第二条、三条的时候都不需要判断就已经过期了,直接就放入死信队列中,所以第二条、三条需要等待第一条消息过了5分钟才能过期,这样的延时根本就没产生对应的效果!
代码演示
1.导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.主启动类开启RabbitMQ
@EnableRabbit
3.创建RabbitMQ配置类
@Slf4j
@Configuration
public class MyMQConfig {
/**
* 容器中的 Binding,Queue Exchange 都会自动创建,(RabbitMq没有的情况)
* RabbitMQ 只要有 @Bean属性发生变换也不会覆盖
* @return
*/
@Bean
public Queue orderDelayQueue() {//死信队列
Map<String,Object> arguments=new HashMap<>();
arguments.put("x-dead-letter-exchange","order-event-exchange");
arguments.put("x-dead-letter-routing-key","order.release.order");
arguments.put("x-message-ttl",60000);
Queue queue = new Queue("order.delay.queue", true, false, false,arguments);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue() {//普通队列
Queue queue = new Queue("order.release.queue", true, false, false);
return queue;
}
@Bean
public Exchange orderEventExchange() {//交换机
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrderBingding() {//绑定关系-死信队列
return new Binding("order.delay.queue", Binding.DestinationType.QUEUE,"order-event-exchange","order.create.order",null);
}
@Bean
public Binding orderReleaseOrderBingding() {//绑定关系-普通队列
return new Binding("order.release.queue", Binding.DestinationType.QUEUE,"order-event-exchange","order.release.order",null);
}
//监听由死信队列过期转到普通队列的消息
@RabbitListener(queues={"order.release.queue"})
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
log.info("收到过期的订单信息,准备关闭订单"+entity.getOrderSn());
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
4.编写消息发送接口
@Autowired
RabbitTemplate rabbitTemplate;
@ResponseBody
@GetMapping("/sendTest")
public String sendTest(){
//new CorrelationData(UUID.randomUUID().toString())指定消息的唯一id
//项目实际情况还会将消息的唯一id存入数据库中,用作后期队列中的消息消费情况做对比
String id= UUID.randomUUID().toString();
OrderEntity entity=new OrderEntity();
entity.setOrderSn(id);
entity.setModifyTime(new Date());
log.info("消息发送中...消息唯一id"+id);
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", entity);
return "ok";
}
5.这里还有一块配置,就是MQ的消息确认机制,消息系列化类型
@Configuration
public class MyRabbitConfig {
//讲对象序列化为JSON
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@Autowired
RabbitTemplate rabbitTemplate;
//定制RabbitTemplate
@PostConstruct//MyRabbitConfig对象创建完成后执行这个初始化方法
public void initRabbitTemplate(){
//设置发送消息确认回调p->b
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback(){
/**
* @param correlationData 当前消息的唯一关联数据 ,这个是消息的唯一id
* @param ack 消息是否成功收到
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm...correlationData=>"+correlationData+"----------ack==>"+ack+"--------cause ==>"+cause);
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就会触发这个失败回调
* @param message 投递失败的消息
* @param replyCode 回复的状态码
* @param replyText 回复的文本内容
* @param exchange 消息发送的交换机
* @param routingKey 消息走的路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("returnedMessage-->"+message+"\nreplyCode-->"+replyCode+"\nexchange-->"+exchange+"\nroutingKey-->"+routingKey);
}
});
}
}
6.application配置
rabbitmq:
host: 192.168.0.177
virtual-host: /
port: 5672
#开启发送端确认p->b
publisher-confirms: true
#开启消息抵达队列的确认 e->q
publisher-returns: true
#只要消息的大队列,以异步的方式优先回调我们这个returnconfirm
template:
mandatory: true
#手动签收消息
listener:
simple:
acknowledge-mode: manual
这里的第5、6步之前也有写过相关文章,有详细介绍RabbitMQ消息确认机制-可靠抵达
7.测试效果
服务启动后,只要当前服务有链接RabbitMQ,也就是有监听器存在,就会自动创建队列、交换机、绑定关系!
检查消息队列的消息
消息数都是0
先用浏览器发送3个sendTest请求
查看队列消息
等待1分钟
消息差不多延迟1分钟到达消费端!