前言

jdkjuc 工具包中,提供了一种延迟队列 DelayQueue。延迟队列用处非常广泛,比如我们最常见的场景就是在网购或者外卖平台中发起一个订单,如果不付款,一般 15 分钟后就会被关闭,这个直接用定时任务是不好实现的,因为每个用户下单的时间并不确定,所以这时候就需要用到延迟队列。

什么是延迟队列

延迟队列本身也是队列,只不过这个队列是延迟的,意思就是说当我们把一条消息放入延迟队列,消息并不会立刻出队,而是会在到达指定时间之后(或者说过了指定时间)才会出队,从而被消费者消费。

利用死信队列实现延迟队列

RabbitMQ 中的死信队列就是用来存储特定条件下的消息,那么假如我们把这个条件设定为指定时间过期(设定带TTL 的消息或者队列),就可以用来实现延迟队列的功能。

  1. 新建一个 TtlDelayRabbitConfig 配置类(省略了包名和导入),消息最开始发送至 ttl 消息队列,这个队列中所有的消息在 5 秒后过期,后期后会进入死信队列:
@Configuration
public class TtlDelayRabbitConfig { //路由ttl消息交换机
@Bean("ttlDelayFanoutExchange")
public FanoutExchange fanoutExchange(){
return new FanoutExchange("TTL_DELAY_FANOUT_EXCHANGE");
} //ttl消息队列
@Bean("ttlDelayQueue")
public Queue ttlQueue(){
Map<String, Object> map = new HashMap<String, Object>();
map.put("x-message-ttl", 5000);//队列中所有消息5秒后过期
map.put("x-dead-letter-exchange", "TTL_DELAY_DEAD_LETTER_FANOUT_EXCHANGE");//过期后进入死信队列
return new Queue("TTL_QUEUE",false,false,false,map);
} //Fanout交换机和productQueue绑定
@Bean
public Binding bindTtlFanoutExchange(@Qualifier("ttlDelayQueue") Queue queue, @Qualifier("ttlDelayFanoutExchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
} //fanout死信交换机
@Bean("ttlDelayDeadLetterExchange")
public FanoutExchange deadLetterExchange(){
return new FanoutExchange("TTL_DELAY_DEAD_LETTER_FANOUT_EXCHANGE");
} //死信队列
@Bean("ttlDelayDeadLetterQueue")
public Queue ttlDelayDeadLetterQueue(){
return new Queue("TTL_DELAY_DEAD_LETTER_FANOUT_QUEUE");
} //死信队列和死信交换机绑定
@Bean
public Binding deadLetterQueueBindExchange(@Qualifier("ttlDelayDeadLetterQueue") Queue queue, @Qualifier("ttlDelayDeadLetterExchange") FanoutExchange fanoutExchange){
return BindingBuilder.bind(queue).to(fanoutExchange);
}
}
  1. 新建一个消费者 TtlDelayConsumer 类,监听死信队列,这里收到的消息都是生产者生产消息之后的 5 秒,也就是延迟了 5 秒的消息:
@Component
public class TtlDelayConsumer { @RabbitHandler
@RabbitListener(queues = "TTL_DELAY_DEAD_LETTER_FANOUT_QUEUE")
public void fanoutConsumer(String msg){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("【延迟队列】【" + sdf.format(new Date()) + "】收到死信队列消息:" + msg);
}
}
  1. 新建一个 DelayQueueController 类做生产者来发送消息:
@RestController
@RequestMapping("/delay")
public class DelayQueueController {
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping(value="/ttl/send")
public String clearVipInfo(@RequestParam(value = "msg",defaultValue = "no message") String msg){
rabbitTemplate.convertAndSend("TTL_DELAY_FANOUT_EXCHANGE","",msg);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("消息发送成功【" + sdf.format(new Date()) + "】");
return "succ";
}
}
  1. 最后我们在浏览器输入地址 http://localhost:8080/delay/ttl/send?msg=测试ttl延迟队列 进行测试,可以看到每条消息都是在发送 5 秒之后才能收到消息:

TTL 延迟队列的问题

假如我们实际中,有的消息是 10 分钟过期,有的是 20 分钟过期,这时候我们就需要建立多个队列,一旦时间维度非常庞大,那么就需要维护非常多的队列。说到这里,可能很多人会有疑问,我们可以针对单条信息设置过期时间,大可不必去定义多个队列?

然而事实真的是如此吗?接下来我们通过一个例子来验证下。

  1. 把上面示例中 TtlDelayRabbitConfig 类中的队列定义函数 x-message-ttl 属性去掉,不过需要注意的是我们需要先把这个队列后台删除掉,否则同名队列重复创建无效:
@Bean("ttlDelayQueue")
public Queue ttlQueue(){
Map<String, Object> map = new HashMap<String, Object>();
// map.put("x-message-ttl", 5000);//注释掉这个属性,队列不设置过期时间
map.put("x-dead-letter-exchange", "TTL_DELAY_DEAD_LETTER_FANOUT_EXCHANGE");//过期后进入死信队列
return new Queue("TTL_QUEUE",false,false,false,map);
}
  1. 然后将 DelayQueueController 类中的发送消息方法修改一下,对每条信息设置过期时间:
@GetMapping(value="/ttl/send")
public String ttlMsgSend(@RequestParam(value = "msg",defaultValue = "no message") String msg,
@RequestParam(value = "time") String millTimes){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration(millTimes);//单条消息设置过期时间,单位:毫秒
Message message = new Message(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend("TTL_DELAY_FANOUT_EXCHANGE","",message);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("消息发送成功【" + sdf.format(new Date()) + "】");
return "succ";
}
  1. 然后执行 2 条消息发送,一条 10 秒过期,一条 5 秒过期,先发送 10 秒的:
http://localhost:8080/delay/ttl/send?msg=10秒过期消息&time=10000
http://localhost:8080/delay/ttl/send?msg=5秒过期消息&time=5000
  1. 执行之后得到如下结果:

我们看到,两条消息都是 10 秒后过期,这是巧合吗?并不是,这是因为 RabbitMQ 中的机制就是如果前一条消息没有出队,那么即使后一条消息已经失效,也必须要等前一条消息出队之后才能出队,所以这就是为什么一般都尽量避免同一个队列单条消息设置不同过期时间的做法。

死信队列实现的延迟队列缺点

通过以上两个例子,使用死信队列来实现延迟队列,我们可以得到几个很明显的缺点:

  • 如果有非常多的时间点(比如有的 10 分钟过期,有的 20 分钟过期等),则需要创建不同的交换机和队列来实现消息的路由。
  • 单独设置消息的 TTL 时可能会造成消息的阻塞。因为当前一条消息没有出队,后一条消息即使到期了也不能出队。
  • 消息可能会有一定的延迟(上面的示例中就可以看到有一点延迟)。

为了避免 TTL 和死信队列可能造成的问题,所以就非常有必要用一种新的更好的方案来替代实现延迟队列,这就是延时队列插件。

利用插件实现延迟队列

RabbitMQ3.5.7 版本之后,提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列 ,同时需保证 Erlang/OPT 版本为 18.0 之后。

安装延迟队列插件

  1. RabbitMQ版本在 3.5.7-3.7.x 的可以执行以下命令进行下载(也可以直接通过浏览器下载):
wget https://bintray.com/rabbitmq/community-plugins/download_file?file_path=rabbitmq_delayed_message_exchange-0.0.1.ez

如果 RabbitMQ3.8 之后的版本,可以点击这里,找到延迟队列对应版本的插件,然后下载。

  1. 下载好之后,将插件上传到 plugins 目录下,执行 rabbitmq-plugins enable rabbitmq_delayed_message_exchange 命令启动插件。如果要禁止该插件,则可以执行命令 rabbitmq-plugins disable rabbitmq_delayed_message_exchange(启用插件后需要重启 RabbitMQ 才会生效)。

延迟队列插件示例

  1. 新建一个 PluginDelayRabbitConfig 配置类:
@Configuration
public class PluginDelayRabbitConfig {
@Bean("pluginDelayExchange")
public CustomExchange pluginDelayExchange() {
Map<String, Object> argMap = new HashMap<>();
argMap.put("x-delayed-type", "direct");//必须要配置这个类型,可以是direct,topic和fanout
//第二个参数必须为x-delayed-message
return new CustomExchange("PLUGIN_DELAY_EXCHANGE","x-delayed-message",false, false, argMap);
} @Bean("pluginDelayQueue")
public Queue pluginDelayQueue(){
return new Queue("PLUGIN_DELAY_QUEUE");
} @Bean
public Binding pluginDelayBinding(@Qualifier("pluginDelayQueue") Queue queue,@Qualifier("pluginDelayExchange") CustomExchange customExchange){
return BindingBuilder.bind(queue).to(customExchange).with("delay").noargs();
}
}
  1. 新建一个消费者类 PluginDelayConsumer
@Component
public class PluginDelayConsumer { @RabbitHandler
@RabbitListener(queues = "PLUGIN_DELAY_QUEUE")//监听延时队列
public void fanoutConsumer(String msg){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("【插件延迟队列】【" + sdf.format(new Date()) + "】收到消息:" + msg);
}
}
  1. 在上面示例中的 DelayQueueController 类,新增一个方法:
@GetMapping(value="/plugin/send")
public String pluginMsgSend(@RequestParam(value = "msg",defaultValue = "no message") String msg){
MessageProperties messageProperties = new MessageProperties();
messageProperties.setHeader("x-delay",5000);//延迟5秒被删除
Message message = new Message(msg.getBytes(), messageProperties);
amqpTemplate.convertAndSend("PLUGIN_DELAY_EXCHANGE","delay",message);//交换机和路由键必须和配置文件类中保持一致
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("消息发送成功【" + sdf.format(new Date()) + "】");
return "succ";
}
  1. 接下来就可以访问地址 http://localhost:8080/delay/plugin/send?msg=插件延迟队列消息 进行测试,可以看到,消息在延时 5 秒之后被消费:

总结

延迟队列的使用非常广泛,如果是单机部署,可以考虑使用 jdk 自带的 DelayQueue,分布式部署可以采用 RabbitMQRedis 等中间件来实现延迟队列。本文主要介绍了如何利用 RabbitMQ 实现两种延迟队列的两种方案,当然本文的例子只是引导,并没有开启回调等消息确认模式,如果想了解 RabbitMQ 消息的可靠性传输的,可以点击这里

如何用RabbitMQ实现延迟队列的更多相关文章

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

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

  2. RabbitMQ延迟队列

    rabbitmq延迟队列 rabbitmq实现延迟队列用了rabbitmq-delayed-message-exchange插件,需要提前安装,并启用. 原理 其原理是通过Exchange来实现延迟功 ...

  3. Spring Boot (26) RabbitMQ延迟队列

    延迟消息就是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费. 延迟队列 订单业务: 在电商/点餐中,都有下单后30分钟内没有付款,就自动取消订单. 短 ...

  4. RabbitMQ如何实现延迟队列?(转)

    什么是延迟队列 延迟队列存储的对象肯定是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费. 场景一 ...

  5. C#实现rabbitmq 延迟队列功能

    最近在研究rabbitmq,项目中有这样一个场景:在用户要支付订单的时候,如果超过30分钟未支付,会把订单关掉.当然我们可以做一个定时任务,每个一段时间来扫描未支付的订单,如果该订单超过支付时间就关闭 ...

  6. 【RabbitMQ】一文带你搞定RabbitMQ延迟队列

    本文口味:鱼香肉丝   预计阅读:10分钟 一.说明 在上一篇中,介绍了RabbitMQ中的死信队列是什么,何时使用以及如何使用RabbitMQ的死信队列.相信通过上一篇的学习,对于死信队列已经有了更 ...

  7. Spring Boot(十四)RabbitMQ延迟队列

    一.前言 延迟队列的使用场景:1.未按时支付的订单,30分钟过期之后取消订单:2.给活跃度比较低的用户间隔N天之后推送消息,提高活跃度:3.过1分钟给新注册会员的用户,发送注册邮件等. 实现延迟队列的 ...

  8. C# RabbitMQ延迟队列功能实战项目演练

    一.需求背景 当用户在商城上进行下单支付,我们假设如果8小时没有进行支付,那么就后台自动对该笔交易的状态修改为订单关闭取消,同时给用户发送一份邮件提醒.那么我们应用程序如何实现这样的需求场景呢?在之前 ...

  9. rabbitmq延迟队列demo

    1. demo详解 1.1 工程结构: 1.2 pom 定义jar包依赖的版本.版本很重要,rabbit依赖spring,两者必须相一致,否则报错: <properties> <sp ...

随机推荐

  1. springboot源码解析-管中窥豹系列之总体结构(一)

    一.简介 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...

  2. 新建虚拟机ping不通windows主机,windows主机ping不通虚拟机解决办法(图文)

    说明: 新建虚拟机和主机互ping不通,因此使用xhell等远程连接工具连接不上 解决办法:配置的时候注意网段 2.修改   /etc/sysconfig/network-scripts/ifcfg- ...

  3. Hbase之过滤器的使用

    一.过滤器概念 基础API中的查询操作在面对大量数据的时候是非常物无力的,这里Hbase提供了高级的查询方法:Filter(过滤器).过滤器可以根据簇.列.版本等更多的条件来对数据进行过滤,基于Hba ...

  4. vue的favicon.ico的不能修改替换问题解决。

    vue的favicon.ico解决办法: 暴力替换图片: <link rel="icon" href="favicon.ico" type="i ...

  5. 使用正则表达式和urllib模块爬取最好大学排名信息

    题目 使用urllib模块编程实现爬取网站的大学排名. (网址:http://www.zuihaodaxue.cn/zuihaodaxuepaiming2016.html) (1)获取网站页面,分析代 ...

  6. 【ORA】ORA-27125:unable to create shared memory segment

    在安装Oracle 10g的时候出现一个了错误,在网上总结了一下大牛写的文章 ORA-27125:unable to create shared memory segment 安装时出现这个错误安装会 ...

  7. kubernets之向外部应用暴露应用

    一  通过NodePort来暴露服务 前面已经介绍的服务的一些作用,例如将集群内部的应用暴露给集群内部的pod使用,将外部的应用通过服务暴露给内部应用使用,但是服务最大的作用不仅仅是这些 而是将集群内 ...

  8. LeetCode617. 合并二叉树

    题目 1 class Solution { 2 public: 3 TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { 4 if(!t1 && ...

  9. BAPI_SALESORDER_CREATEFROMDAT2 条件 定价元素

    用函数 BAPI_SALESORDER_CREATEFROMDAT2创建SO的时候,遇见个问题,就是如图: 会多出来一个类型,搞了半天,发现是一个函数里的一个参数,我没有设置: LOGIC_SWITC ...

  10. JD6621快速充电协议芯片,带有PPS 控制器的USB-PD3.0

    描述 JD6621是高度集成的USB供电(PD)控制器,支持USB PD 3.0 ,该USB PD 3.0 具有针对USBType-C下游接口(源)设计的可编程电源(PPS)规范.它监视CC引脚以检测 ...