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. 声明交换机,队列
  1. package com.lank.demo.config;
  2. import org.springframework.amqp.core.*;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. /**
  8. * @author lank
  9. * @since 2020/12/14 10:44
  10. */
  11. @Configuration
  12. public class RabbitmqConfig {
  13. //死信交换机,队列,路由相关配置
  14. public static final String DLK_EXCHANGE = "dlk.exchange";
  15. public static final String DLK_ROUTEKEY = "dlk.routeKey";
  16. public static final String DLK_QUEUE = "dlk.queue";
  17. //业务交换机,队列,路由相关配置
  18. public static final String DEMO_EXCHANGE = "demo.exchange";
  19. public static final String DEMO_QUEUE = "demo.queue";
  20. public static final String DEMO_ROUTEKEY = "demo.routeKey";
  21. //延时插件DelayedMessagePlugin的交换机,队列,路由相关配置
  22. public static final String DMP_EXCHANGE = "dmp.exchange";
  23. public static final String DMP_ROUTEKEY = "dmp.routeKey";
  24. public static final String DMP_QUEUE = "dmp.queue";
  25. @Bean
  26. public DirectExchange demoExchange(){
  27. return new DirectExchange(DEMO_EXCHANGE,true,false);
  28. }
  29. @Bean
  30. public Queue demoQueue(){
  31. //只需要在声明业务队列时添加x-dead-letter-exchange,值为死信交换机
  32. Map<String,Object> map = new HashMap<>(1);
  33. map.put("x-dead-letter-exchange",DLK_EXCHANGE);
  34. //该参数x-dead-letter-routing-key可以修改该死信的路由key,不设置则使用原消息的路由key
  35. map.put("x-dead-letter-routing-key",DLK_ROUTEKEY);
  36. return new Queue(DEMO_QUEUE,true,false,false,map);
  37. }
  38. @Bean
  39. public Binding demoBind(){
  40. return BindingBuilder.bind(demoQueue()).to(demoExchange()).with(DEMO_ROUTEKEY);
  41. }
  42. @Bean
  43. public DirectExchange dlkExchange(){
  44. return new DirectExchange(DLK_EXCHANGE,true,false);
  45. }
  46. @Bean
  47. public Queue dlkQueue(){
  48. return new Queue(DLK_QUEUE,true,false,false);
  49. }
  50. @Bean
  51. public Binding dlkBind(){
  52. return BindingBuilder.bind(dlkQueue()).to(dlkExchange()).with(DLK_ROUTEKEY);
  53. }
  54. //延迟插件使用
  55. //1、声明一个类型为x-delayed-message的交换机
  56. //2、参数添加一个x-delayed-type值为交换机的类型用于路由key的映射
  57. @Bean
  58. public CustomExchange dmpExchange(){
  59. Map<String, Object> arguments = new HashMap<>(1);
  60. arguments.put("x-delayed-type", "direct");
  61. return new CustomExchange(DMP_EXCHANGE,"x-delayed-message",true,false,arguments);
  62. }
  63. @Bean
  64. public Queue dmpQueue(){
  65. return new Queue(DMP_QUEUE,true,false,false);
  66. }
  67. @Bean
  68. public Binding dmpBind(){
  69. return BindingBuilder.bind(dmpQueue()).to(dmpExchange()).with(DMP_ROUTEKEY).noargs();
  70. }
  71. }
  1. 声明一个类用于发送带过期时间的消息
  1. package com.lank.demo.rabbitmq;
  2. import com.lank.demo.config.RabbitmqConfig;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.amqp.AmqpException;
  5. import org.springframework.amqp.core.Message;
  6. import org.springframework.amqp.core.MessagePostProcessor;
  7. import org.springframework.amqp.rabbit.core.RabbitTemplate;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Component;
  10. /**
  11. * @author lank
  12. * @since 2020/12/14 10:33
  13. */
  14. @Component
  15. @Slf4j
  16. public class MessageSender {
  17. @Autowired
  18. private RabbitTemplate rabbitTemplate;
  19. //使用死信队列发送消息方法封装
  20. public void send(String message,Integer time){
  21. String ttl = String.valueOf(time*1000);
  22. //exchange和routingKey都为业务的就可以,只需要设置消息的过期时间
  23. rabbitTemplate.convertAndSend(RabbitmqConfig.DEMO_EXCHANGE, RabbitmqConfig.DEMO_ROUTEKEY,message, new MessagePostProcessor() {
  24. @Override
  25. public Message postProcessMessage(Message message) throws AmqpException {
  26. //设置消息的过期时间,是以毫秒为单位的
  27. message.getMessageProperties().setExpiration(ttl);
  28. return message;
  29. }
  30. });
  31. log.info("使用死信队列消息:{}发送成功,过期时间:{}秒。",message,time);
  32. }
  33. //使用延迟插件发送消息方法封装
  34. public void send2(String message,Integer time){
  35. rabbitTemplate.convertAndSend(RabbitmqConfig.DMP_EXCHANGE, RabbitmqConfig.DMP_ROUTEKEY,message, new MessagePostProcessor() {
  36. @Override
  37. public Message postProcessMessage(Message message) throws AmqpException {
  38. //使用延迟插件只需要在消息的header中添加x-delay属性,值为过期时间,单位毫秒
  39. message.getMessageProperties().setHeader("x-delay",time*1000);
  40. return message;
  41. }
  42. });
  43. log.info("使用延迟插件发送消息:{}发送成功,过期时间:{}秒。",message,time);
  44. }
  45. }
  1. 编写一个类用于消费消息
  1. package com.lank.demo.rabbitmq;
  2. import com.lank.demo.config.RabbitmqConfig;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.amqp.core.Message;
  5. import org.springframework.amqp.rabbit.annotation.RabbitHandler;
  6. import org.springframework.amqp.rabbit.annotation.RabbitListener;
  7. import org.springframework.stereotype.Component;
  8. /**
  9. * @author lank
  10. * @since 2020/12/15 15:57
  11. */
  12. @Component
  13. @Slf4j
  14. public class MessageReceiver {
  15. @RabbitHandler
  16. @RabbitListener(queues = RabbitmqConfig.DLK_QUEUE)
  17. public void onMessage(Message message){
  18. log.info("使用死信队列,收到消息:{}",new String(message.getBody()));
  19. }
  20. @RabbitHandler
  21. @RabbitListener(queues = RabbitmqConfig.DMP_QUEUE)
  22. public void onMessage2(Message message){
  23. log.info("使用延迟插件,收到消息:{}",new String(message.getBody()));
  24. }
  25. }
  1. 编写Controller调用发送消息方法测试结果
  1. package com.lank.demo.controller;
  2. import com.lank.demo.rabbitmq.MessageSender;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RequestParam;
  6. import org.springframework.web.bind.annotation.RestController;
  7. /**
  8. * @author lank
  9. * @since 2020/12/14 17:05
  10. */
  11. @RestController
  12. public class MessageController {
  13. @Autowired
  14. public MessageSender messageSender;
  15. //死信队列controller
  16. @GetMapping("/send")
  17. public String send(@RequestParam String msg,Integer time){
  18. messageSender.send(msg,time);
  19. return "ok";
  20. }
  21. //延迟插件controller
  22. @GetMapping("/send2")
  23. public String sendByPlugin(@RequestParam String msg,Integer time){
  24. messageSender.send2(msg,time);
  25. return "ok";
  26. }
  27. }
  1. 配置文件application.properties
  1. server.port=4399
  2. #virtual-host使用默认的/就好,如果需要/demo需自己在控制台添加
  3. spring.rabbitmq.virtual-host=/demo
  4. spring.rabbitmq.host=localhost
  5. spring.rabbitmq.port=5672
  6. spring.rabbitmq.username=guest
  7. spring.rabbitmq.password=guest
  1. 启动项目,打开rabbitmq控制台,可以看到交换机和队列已经创建好。



  2. 在浏览器中请求http://localhost:4399/send?msg=hello&time=5,从控制台的输出来看,刚好5s后接收到消息。
  1. 2020-12-16 22:47:28.071 INFO 13304 --- [nio-4399-exec-1] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:hello发送成功,过期时间:5秒。
  2. 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,但结果并不是,控制台输出如下:

  1. 2020-12-16 22:54:47.339 INFO 13304 --- [nio-4399-exec-5] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:消息A发送成功,过期时间:30秒。
  2. 2020-12-16 22:54:54.278 INFO 13304 --- [nio-4399-exec-6] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用死信队列消息:消息B发送成功,过期时间:10秒。
  3. 2020-12-16 22:55:17.356 INFO 13304 --- [ntContainer#0-1] c.l.r.rabbitmq.MessageReceiver : 使用死信队列,收到消息:消息A
  4. 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,控制台输出如下,不会出现死信队列出现的问题:
  1. 2020-12-16 23:31:19.819 INFO 13304 --- [nio-4399-exec-9] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用延迟插件发送消息:消息A发送成功,过期时间:30秒。
  2. 2020-12-16 23:31:27.673 INFO 13304 --- [io-4399-exec-10] c.l.rabbitmqdlk.rabbitmq.MessageSender : 使用延迟插件发送消息:消息B发送成功,过期时间:10秒。
  3. 2020-12-16 23:31:37.833 INFO 13304 --- [ntContainer#1-1] c.l.r.rabbitmq.MessageReceiver : 使用延迟插件,收到消息:消息B
  4. 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. js 获取树结构的节点深度

    需求:获取树结构的节点深度. 实现util.js: // 获取节点深度 参数为树结构array function getMaxFloor(treeData){ let deep = 0; functi ...

  2. python stats画正态分布、指数分布、对数正态分布的QQ图

    stats.probplot(grade, dist=stats.norm, plot=plt) #正态分布 # stats.probplot(grade, dist=stats.expon, plo ...

  3. java中==和equals()方法

    java 程序中测试两个变量是否相等有两种方法: == equals()方法 当使用==判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同),则只要两个变量 ...

  4. C# 获取Word文本高亮和背景(附vb.net代码)

    Word中的文本高亮和背景是通过不同方法来设置的.文本高亮(Text Highlight Color)是通过[字体]中的快速工具栏设置:文本背景(Text Background/Shading)是通过 ...

  5. P1200_你的飞碟在这儿(JAVA语言)

    题目描述 众所周知,在每一个彗星后都有一只UFO.这些UFO时常来收集地球上的忠诚支持者. 不幸的是,他们的飞碟每次出行都只能带上一组支持者.因此,他们要用一种聪明的方案让这些小组提前知道谁会被彗星带 ...

  6. upx 手动脱壳

    查壳 UPX 0.89.6 - 1.02 / 1.05 - 2.90 (Delphi) stub -> Markus & Laszlo upx这类压缩壳手动脱壳非常简单. 一.查找oep ...

  7. IOC容器模拟实现

    运用反射机制和自定义注解模拟实现IOC容器,使其具有自动加载.自动装配和根据全限定类名获取Bean的功能. 一. 实现原理 1-1 IOC容器的本质 IOC容器可理解为是一个map,其中的一个entr ...

  8. 学习C#第一天

    学习C#第一天 先是了解了VS 2019编辑器的基本使用 安装Visual Studio https://mp.weixin.qq.com/s?__biz=MzU0MTg5NDkzNA==&m ...

  9. 删除文件--rm

    rm file         删除文件 rm -r dir      删除指定文件夹及文件夹下的所有内容 rm -rf dir     强制删除指定文件夹及文件夹下的所有内容

  10. ASP.NET Core中使用令牌桶限流

    在限流时一般会限制每秒或每分钟的请求数,简单点一般会采用计数器算法,这种算法实现相对简单,也很高效,但是无法应对瞬时的突发流量. 比如限流每秒100次请求,绝大多数的时间里都不会超过这个数,但是偶尔某 ...