RabbitMQ实现延时消息的两种方法

1、死信队列

1.1消息什么时候变为死信(dead-letter)

  1. 消息被否定接收,消费者使用basic.reject 或者 basic.nack并且requeue 重回队列属性设为false。
  2. 消息在队列里得时间超过了该消息设置的过期时间(TTL)。
  3. 消息队列到达了它的最大长度,之后再收到的消息。

1.2死信队列的原理

当一个消息再队列里变为死信时,它会被重新publish到另一个exchange交换机上,这个exchange就为DLX。因此我们只需要在声明正常的业务队列时添加一个可选的"x-dead-letter-exchange"参数,值为死信交换机,死信就会被rabbitmq重新publish到配置的这个交换机上,我们接着监听这个交换机就可以了。

1.3 代码实现

  1. 引入amqp依赖
  2. 声明交换机,队列
package com.lank.demo.config;

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; /**
* @author lank
* @since 2020/12/14 10:44
*/
@Configuration
public class RabbitmqConfig { //死信交换机,队列,路由相关配置
public static final String DLK_EXCHANGE = "dlk.exchange";
public static final String DLK_ROUTEKEY = "dlk.routeKey";
public static final String DLK_QUEUE = "dlk.queue"; //业务交换机,队列,路由相关配置
public static final String DEMO_EXCHANGE = "demo.exchange";
public static final String DEMO_QUEUE = "demo.queue";
public static final String DEMO_ROUTEKEY = "demo.routeKey"; //延时插件DelayedMessagePlugin的交换机,队列,路由相关配置
public static final String DMP_EXCHANGE = "dmp.exchange";
public static final String DMP_ROUTEKEY = "dmp.routeKey";
public static final String DMP_QUEUE = "dmp.queue"; @Bean
public DirectExchange demoExchange(){
return new DirectExchange(DEMO_EXCHANGE,true,false);
} @Bean
public Queue demoQueue(){
//只需要在声明业务队列时添加x-dead-letter-exchange,值为死信交换机
Map<String,Object> map = new HashMap<>(1);
map.put("x-dead-letter-exchange",DLK_EXCHANGE);
//该参数x-dead-letter-routing-key可以修改该死信的路由key,不设置则使用原消息的路由key
map.put("x-dead-letter-routing-key",DLK_ROUTEKEY);
return new Queue(DEMO_QUEUE,true,false,false,map);
} @Bean
public Binding demoBind(){
return BindingBuilder.bind(demoQueue()).to(demoExchange()).with(DEMO_ROUTEKEY);
} @Bean
public DirectExchange dlkExchange(){
return new DirectExchange(DLK_EXCHANGE,true,false);
} @Bean
public Queue dlkQueue(){
return new Queue(DLK_QUEUE,true,false,false);
} @Bean
public Binding dlkBind(){
return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(DLK_ROUTEKEY);
} //延迟插件使用
//1、声明一个类型为x-delayed-message的交换机
//2、参数添加一个x-delayed-type值为交换机的类型用于路由key的映射
@Bean
public CustomExchange dmpExchange(){
Map<String, Object> arguments = new HashMap<>(1);
arguments.put("x-delayed-type", "direct");
return new CustomExchange(DMP_EXCHANGE,"x-delayed-message",true,false,arguments);
} @Bean
public Queue dmpQueue(){
return new Queue(DMP_QUEUE,true,false,false);
} @Bean
public Binding dmpBind(){
return BindingBuilder.bind(dmpQueue()).to(dmpExchange()).with(DMP_ROUTEKEY).noargs();
} }
  1. 声明一个类用于发送带过期时间的消息
package com.lank.demo.rabbitmq;

import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; /**
* @author lank
* @since 2020/12/14 10:33
*/
@Component
@Slf4j
public class MessageSender { @Autowired
private RabbitTemplate rabbitTemplate; //使用死信队列发送消息方法封装
public void send(String message,Integer time){
String ttl = String.valueOf(time*1000);
//exchange和routingKey都为业务的就可以,只需要设置消息的过期时间
rabbitTemplate.convertAndSend(RabbitmqConfig.DEMO_EXCHANGE, RabbitmqConfig.DEMO_ROUTEKEY,message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置消息的过期时间,是以毫秒为单位的
message.getMessageProperties().setExpiration(ttl);
return message;
}
});
log.info("使用死信队列消息:{}发送成功,过期时间:{}秒。",message,time);
} //使用延迟插件发送消息方法封装
public void send2(String message,Integer time){
rabbitTemplate.convertAndSend(RabbitmqConfig.DMP_EXCHANGE, RabbitmqConfig.DMP_ROUTEKEY,message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//使用延迟插件只需要在消息的header中添加x-delay属性,值为过期时间,单位毫秒
message.getMessageProperties().setHeader("x-delay",time*1000);
return message;
}
});
log.info("使用延迟插件发送消息:{}发送成功,过期时间:{}秒。",message,time);
}
}
  1. 编写一个类用于消费消息
package com.lank.demo.rabbitmq;

import com.lank.demo.config.RabbitmqConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; /**
* @author lank
* @since 2020/12/15 15:57
*/ @Component
@Slf4j
public class MessageReceiver { @RabbitHandler
@RabbitListener(queues = RabbitmqConfig.DLK_QUEUE)
public void onMessage(Message message){
log.info("使用死信队列,收到消息:{}",new String(message.getBody()));
} @RabbitHandler
@RabbitListener(queues = RabbitmqConfig.DMP_QUEUE)
public void onMessage2(Message message){
log.info("使用延迟插件,收到消息:{}",new String(message.getBody()));
}
}
  1. 编写Controller调用发送消息方法测试结果
package com.lank.demo.controller;
import com.lank.demo.rabbitmq.MessageSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; /**
* @author lank
* @since 2020/12/14 17:05
*/
@RestController
public class MessageController { @Autowired
public MessageSender messageSender; //死信队列controller
@GetMapping("/send")
public String send(@RequestParam String msg,Integer time){
messageSender.send(msg,time);
return "ok";
} //延迟插件controller
@GetMapping("/send2")
public String sendByPlugin(@RequestParam String msg,Integer time){
messageSender.send2(msg,time);
return "ok";
} }
  1. 配置文件application.properties
server.port=4399
#virtual-host使用默认的/就好,如果需要/demo需自己在控制台添加
spring.rabbitmq.virtual-host=/demo
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
  1. 启动项目,打开rabbitmq控制台,可以看到交换机和队列已经创建好。



  2. 在浏览器中请求http://localhost:4399/send?msg=hello&time=5,从控制台的输出来看,刚好5s后接收到消息。
2020-12-16 22:47:28.071  INFO 13304 --- [nio-4399-exec-1] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:hello发送成功,过期时间:5秒。
2020-12-16 22:47:33.145 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:hello

1.4死信队列的一个小坑

当我往死信队列中发送两条不同过期时间的消息时,如果先发送的消息A的过期时间大于后发送的消息B的过期时间时,由于消息的顺序消费,消息B过期后并不会立即重新publish到死信交换机,而是会等到消息A过期后一起被消费。

依次发送两个请求http://localhost:4399/send?msg=消息A&time=30和http://localhost:4399/send?msg=消息B&time=10,消息A先发送,过期时间30S,消息B后发送,过期时间10S,我们想要的结果应该是10S收到消息B,30S后收到消息A,但结果并不是,控制台输出如下:

2020-12-16 22:54:47.339  INFO 13304 --- [nio-4399-exec-5] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用死信队列消息:消息A发送成功,过期时间:30秒。
2020-12-16 22:54:54.278 INFO 13304 --- [nio-4399-exec-6] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:消息B发送成功,过期时间:10秒。
2020-12-16 22:55:17.356 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:消息A
2020-12-16 22:55:17.357 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:消息B

消息A30S后被成功消费,紧接着消息B被消费。因此当我们使用死信队列时应该注意是否消息的过期时间都是一样的,比如订单超过10分钟未支付修改其状态。如果当一个队列各个消息的过期时间不一致时,使用死信队列就可能达不到延时的作用。这时候我们可以使用延时插件来实现这需求。

2 、延时插件

RabbitMQ Delayed Message Plugin是一个rabbitmq的插件,所以使用前需要安装它,可以参考的GitHub地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange

2.1如何实现

  1. 安装好插件后只需要声明一个类型type为"x-delayed-message"的exchange,并且在其可选参数下配置一个key为"x-delayed-typ",值为交换机类型(topic/direct/fanout)的属性。
  2. 声明一个队列绑定到该交换机
  3. 在发送消息的时候消息的header里添加一个key为"x-delay",值为过期时间的属性,单位毫秒。
  4. 代码就在上面,配置类为DMP开头的,发送消息的方法为send2()。
  5. 启动后在rabbitmq控制台可以看到一个类型为x-delayed-message的交换机。



  6. 继续在浏览器中发送两个请求http://localhost:4399/send2?msg=消息A&time=30和http://localhost:4399/send2?msg=消息B&time=10,控制台输出如下,不会出现死信队列出现的问题:
2020-12-16 23:31:19.819  INFO 13304 --- [nio-4399-exec-9] c.l.rabbitmqdlk.rabbitmq.MessageSender   : 使用延迟插件发送消息:消息A发送成功,过期时间:30秒。
2020-12-16 23:31:27.673 INFO 13304 --- [io-4399-exec-10] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用延迟插件发送消息:消息B发送成功,过期时间:10秒。
2020-12-16 23:31:37.833 INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver : 使用延迟插件,收到消息:消息B
2020-12-16 23:31:49.917 INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver : 使用延迟插件,收到消息:消息A

死信交换机官网介绍:https://www.rabbitmq.com/dlx.html

延时插件GitHub:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange

RabbitMQ实现延时消息的两种方法的更多相关文章

  1. RabbitMQ Consumer获取消息的两种方式(poll,subscribe)解析

    以下转自:http://blog.csdn.net/yangbutao/article/details/10395599 rabbitMQ中consumer通过建立到queue的连接,创建channe ...

  2. C#实现在Form上截取消息的两种方法

    比较常用的是重载Form的DefWndProc方法,例如截取鼠标按下的消息: protected override void DefWndProc(ref Message m) { if ( m.Ms ...

  3. C#代码像QQ的右下角消息框一样,无论现在用户的焦点在哪个窗口,消息框弹出后都不影响焦点的变化,那么有两种方法

    你QQ的右下角消息框一样,无论现在用户的焦点在哪个窗口,消息框弹出后都不影响焦点的变化,那么有两种方法: 要么重写需要弹出的窗体的事件: protected override CreateParams ...

  4. rabbitmq 实现延迟队列的两种方式

    原文地址:https://blog.csdn.net/u014308482/article/details/53036770 ps: 文章里面延迟队列=延时队列 什么是延迟队列 延迟队列存储的对象肯定 ...

  5. android 之 启动画面的两种方法

    现在,当我们打开任意的一个app时,其中的大部分都会显示一个启动界面,展示本公司的logo和当前的版本,有的则直接把广告放到了上面.启动画面的可以分为两种设置方式:一种是两个Activity实现,和一 ...

  6. [转]Delphi调用cmd的两种方法

    delphi调用cmd的两种方法vars:string;begins:='cmd.exe /c '+edit1.Text+' >c:\1.txt';winexec(pchar(s),sw_hid ...

  7. 安卓ListView操作的两种方法

    举例做一个微信的中间部分(好友消息等信息通知) 第一种:BaseAdapter() package com.example.wx; import java.util.ArrayList;import ...

  8. Delphi Windows API判断文件共享锁定状态(OpenFile和CreateFile两种方法)

    一.概述 锁是操作系统为实现数据共享而提供的一种安全机制,它使得不同的应用程序,不同的计算机之间可以安全有效地共享和交换数据.要保证安全有效地操作共享数据,必须在相应的操作前判断锁的类型,然后才能确定 ...

  9. 在Delphi中使用C++对象(两种方法,但都要改造C++提供的DLL)

    Delphi是市场上最好的RAD工具,但是现在C++占据着主导地位,有时针对一个问题很难找到Delphi或Pascal的解决方案.可是却可能找到了一个相关的C++类.本文描述几种在Delphi代码中使 ...

随机推荐

  1. Servlet原理解析&使用指南

    Servlet(Server Applet)全称Java Servlet,是用Java编写的服务器端程序.广义上指任何实现了Server接口的类,主要功能在于交互式地浏览和生成数据,生成动态Web内容 ...

  2. 利用eigen库简单实现矩阵功能

    eigen是目前运行速度较快的C++矩阵运算库,而且其轻便小巧安装方便的特点简直是吸引人啊!特做此笔记,记录一下这个安装简单.体积轻巧.功能强大的C++库. 1. Download and Insta ...

  3. io流(io流的引入与文件字节流)

    io流的引入与文件字节流 io流:就是一根吸管,插入后,可以操作目标文件 io流的分类: 按方向:输入,输出 按大小:字节,字符 按处理方式: 处理流:"管套着管" --- 流结合 ...

  4. ArrayList这篇就够了

    提起ArrayList,相信很多小伙伴都用过,而且还不少用.但在几年之前,我在一场面试中,面试官要求说出ArrayList的扩容机制.很显然,那个时候的我并没有关注这些,从而错过了一次机会.不过好在我 ...

  5. 网络对抗技术Exp2-后门原理与实践

    后门概念 后门就是不经过正常认证流程而访问系统的通道. 哪里有后门呢? 编译器留后门 操作系统留后门 最常见的当然还是应用程序中留后门 还有就是潜伏于操作系统中或伪装为特定应用的专用后门程序. 下面是 ...

  6. vue实现拖拽排序

    基于vue实现列表拖拽排序的效果 在日常开发中,特别是管理端,经常会遇到要实现拖拽排序的效果:这里提供一种简单的实现方案. 此例子基于vuecli3 首先,我们先了解一下js原生拖动事件: 在拖动目标 ...

  7. Kubernetes 常见问题总结

    Kubernetes 常见问题总结 如何删除不一致状态下的 rc,deployment,service 在某些情况下,经常发现 kubectl 进程挂起现象,然后在 get 时候发现删了一半,而另外的 ...

  8. 重磅!解锁Apache Flink读写Apache Hudi新姿势

    感谢阿里云 Blink 团队Danny Chan的投稿及完善Flink与Hudi集成工作. 1. 背景 Apache Hudi 是目前最流行的数据湖解决方案之一,Data Lake Analytics ...

  9. java面试-JVM常用的基本配置参数有哪些?

    1.-Xms 初始大小内存,默认为物理内存 1/64,等价于 -XX:InitialHeapSize 2.-Xmx 最大分配内存,默认为物理内存的 1/4,等价于 -XX:MaxHeapSize 3. ...

  10. 开源一周岁,MindSpore新特性巨量来袭

    摘要:MindSpore很多新特性与大家见面了,无论是在效率提升.易用性,还是创新方面,都是干货满满. 最近,AI计算框架是业界的热点,各大厂商纷纷投身AI框架的自研发,究其原因:AI框架在整个人工智 ...