延时队列
延时队列能做什么?
- 订单业务: 在电商/点餐中,都有下单后 30 分钟内没有付款,就自动取消订单。
- 短信通知: 下单成功后 60s 之后给用户发送短信通知。
- 失败重试: 业务操作失败后,间隔一定的时间进行失败重试
对于需要延时操作的事务,不用延时队列可以用定时任务的方式,来查询数据库,但这样的的效率很低。另一种就是用Java中的DelayQueue 位于java.util.concurrent包下,本质是由PriorityQueue和BlockingQueue实现的阻塞优先级队列。这玩意最大的问题就是不支持分布式与持久化。
RabbitMQ 实现思路
RabbitMQ队列本身是没有直接实现支持延迟队列的功能,但可以通过它的Time-To-Live Extensions 与 Dead Letter Exchange 的特性模拟出延迟队列的功能。
消息存活时间
Time-To-Live Extensions(TTL) 即设置消息或队列的存活时间,TTL表明一条消息可以在队列的最大存活时间,当一条消息设置了最大存活时间或者进入了设置了TTL的队列,这条消息会在TTL 后死亡,就成了 dead letter,。如果既设置了消息也设置了队列,以最小的时间为准。
死信交换机
上面的消息如果设置了 dead letter exachange(DLX)死信交换机,在消息死亡后,会进到死信交换机,然后路由到其他队列,继续消费,实现队列延时。
设置 TTL
设置 key
>路由
根据key路由
ACK
生产者生产消息
交换机
队列
没有消费,变成死信
死信交换机
死信交换机
其他队列
消费者消费
从队列中删除
具体编码
定义队列
public static final String QUEUE_1 = "DQUEUE_1";
public static final String EXCHANGE_1= "EXCHANGE_1";
public static final String ROUTING_KEY_1 = "ROUTING_KEY_1";
public static final String QUEUE_2 = "QUEUE_2"; //延时队列TTL名称:DELAY_QUEUE_A
public static final String EXCHANGE_2 = "EXCHANGE_2"; //DLX 死信交换机,消息到期发到这个
public static final String ROUTING_KEY_2 = "ROUTING_KEY_2";//路由关键字
@Bean
public Queue Queue1(){
return new Queue(QUEUE_1,true);
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE_1);
}
@Bean
public Binding rBinding(){
return BindingBuilder.bind(Queue1()).to(topicExchange()).with(ROUTING_KEY_1);
}
/**
* 延迟队列配置
* <p>
* 1、params.put("x-message-ttl", 5 * 1000);
* TODO 第一种方式是直接设置 Queue 延迟时间 但如果直接给队列设置过期时间,这种做法不是很灵活,(当然二者是兼容的,默认是时间小的优先)
* 2、rabbitTemplate.convertAndSend(book, message -> {
* message.getMessageProperties().setExpiration(2 * 1000 + "");
* return message;
* });
* TODO 第二种就是每次发送消息动态设置延迟时间,这样我们可以灵活控制
**/
@Bean
public Queue delayProcessQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
params.put("x-dead-letter-exchange", EXCHANGE_1);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
params.put("x-dead-letter-routing-key", ROUTING_KEY_1);
return new Queue(QUEUE_2, true, false, false, params);
}
/**
* 死信交换机
* @return
*/
@Bean
public DirectExchange delayExchange() {
return new DirectExchange(EXCHANGE_2);
}
@Bean
public Binding dlxBinding(){
return BindingBuilder.bind(delayProcessQueue()).to(delayExchange()).with(ROUTING_KEY_2);
}
控制器
@ApiOperation(value = "rabbitmq延时队列")
@RequestMapping(value = "/proDelayMsg",method = {RequestMethod.GET})
@ResponseBody
public String proDelayMsg() {
Student stu = new Student();
stu.setId((short)1);
stu.setName("学习延时队列的旭东");
stu.setAddress("上海宝山");
log.info("生产者生产 延时 消息:"+stu);
this.rabbitTemplate.convertAndSend(RabbitConfig.EXCHANGE_2,RabbitConfig.ROUTING_KEY_2 ,stu,message ->{
// TODO 第一句是可要可不要,根据自己需要自行处理
message.getMessageProperties().setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, Student.class.getName());
// TODO 如果配置了 params.put("x-message-ttl", 5 * 1000); 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间
message.getMessageProperties().setExpiration(5 * 1000 + "");
return message;
} );
log.info("[发送时间] - [{}]", LocalDateTime.now());
return "success";
}
消费者监听
@RabbitListener(queues = {RabbitConfig.QUEUE_1})
public void listenerDelayAck(Student stu, Message message, Channel channel) {
log.info("[listenerDelayQueue 监听的消息] - [消费时间] - [{}] - [{}]", LocalDateTime.now(), stu.toString());
try {
// TODO 通知 MQ 消息已被成功消费,可以ACK了
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
// TODO 如果报错了,那么我们可以进行容错处理,比如转移当前消息进入其它队列
try {
channel.basicRecover();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
测试结果
[2019-05-06 14:29:36:259] [INFO] - com.example.controller.StudentRabbitMqController.proDelayMsg(StudentRabbitMqController.java:60) - 生产者生产 延时 消息:[id:1,age:null,name:学习延时队列的旭东,address:上海宝山]
[2019-05-06 14:29:36:727] [INFO] - com.example.controller.StudentRabbitMqController.proDelayMsg(StudentRabbitMqController.java:68) - [发送时间] - [2019-05-06T14:29:36.726]
[2019-05-06 14:29:42:139] [INFO] - com.example.handler.BookHandler.listenerDelayAck(BookHandler.java:69) - [listenerDelayQueue 监听的消息] - [消费时间] - [2019-05-06T14:29:42.139] - [[id:1,age:null,name:学习延时队列的旭东,address:上海宝山]]
部分配置内容在上一篇