本节将会给大家介绍一下什么是延迟队列,延迟队列的使用场景,RabbitMQ实现延迟队列的两种方式:TTL+DLX(即消息有效期+死信交换机/队列),rabbitmq-delayed-message-exchange插件。
延迟队列是存储延迟消息的队列,延迟消息就是生产者发送了一条消息,但是不希望该消息不要被立即消费,而是设置一个延迟时间,等过了这个时间再消费消息。
延迟队列常见的使用场景如下:
。。。。。。
解决上述场景的方法,除了使用延迟队列之外,一般还有如下两种:
项目GitHub地址 https://github.com/RookieMember/RabbitMQ-Learning.git。这种方案就是给消息设置有效期,并给消息所在队列设置DLX(死信交换机),当消息过期后变为死信消息然后被发送到DLX中,然后根据死信的路由键路由到DLX上绑定的死信队列上面。关于TTL和DLX的具体使用方式。可以参考一下上个博客《(12)RabbitMQ的TTL(消息有效期)和DLX(死信交换机/队列)》。
实际应用中,我们都会按照时间段划分等级,例如为5 秒、30 秒、5 分钟、30 分钟、1 小时。。。。。。如下图,我们只画了三个队列,中间的时间省略了。生产者发消息时携带有时间标识的路由键,然后交换机将消息路由到对应的消息过期时间的队列上,这些队列分别绑定了死信交换机,这些死信交换机又分别绑定了死信队列,消息过期后就通过死信交换机路由到了死信队列上面,然后我们的消费者就可以监听消费死信队列上面的消息了。
注意:下面图示中多个死信交换机分别绑定了队列,这样是为了更好的区分不同时间的消息,方便处理、查看、统计、监控等,当然也可以多个死信交换机绑定同一个队列。
package cn.wkp.rabbitmq.newest.delayqueue.ttldlx;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.MessageProperties;
import cn.wkp.rabbitmq.util.ConnectionUtil;
public class Send {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明一个交换机,做死信交换机用
channel.exchangeDeclare("delay_exchange_5s", "direct", true, false, null);
//声明一个队列,做死信队列用
channel.queueDeclare("delay_queue_5s", true, false, false, null);
//队列绑定到交换机上
channel.queueBind("delay_queue_5s", "delay_exchange_5s", "q5s");
channel.exchangeDeclare("work_exchange", "direct", true, false, null);
Map arguments=new HashMap();
arguments.put("x-message-ttl" , 5000);//设置消息有效期1秒,过期后变成私信消息,然后进入DLX
arguments.put("x-dead-letter-exchange" , "delay_exchange_5s");//设置DLX
//为队列normal_queue 添加DLX
channel.queueDeclare("work_queue_5s", true, false, false, arguments);
channel.queueBind("work_queue_5s", "work_exchange", "q5s");
String message = ConnectionUtil.formatDate(new Date())+",延迟五秒的消息";
channel.basicPublish("work_exchange", "q5s", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println("发送消:"+message);
channel.close();
connection.close();
}
}
package cn.wkp.rabbitmq.newest.delayqueue.ttldlx;
import java.io.IOException;
import java.util.Date;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import cn.wkp.rabbitmq.util.ConnectionUtil;
public class Recv {
public static void main(String[] argv) throws Exception {
Connection connection = ConnectionUtil.getConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("delay_exchange_5s", "direct", true, false, null);
channel.queueDeclare("delay_queue_5s", true, false, false, null);
channel.queueBind("delay_queue_5s", "delay_exchange_5s", "q5s");
// 指该消费者在接收到队列里的消息但没有返回确认结果之前,它不会将新的消息分发给它。
channel.basicQos(1);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
System.out.println("消费者收到消息:" + new String(body)+",当前时间:"+ConnectionUtil.formatDate(new Date()));
// 消费者手动发送ack应答
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
System.out.println("消费延迟5秒队列中的消息======================");
// 监听队列
channel.basicConsume("delay_queue_5s", false, consumer);
}
}
运行后消费者控制台输出如下:
消费延迟5秒队列中的消息======================
消费者收到消息:2019-04-21 21:50:41:358,延迟五秒的消息,当前时间:2019-04-21 21:50:46:447
上面演示了通过TTL+DLX实现的延迟队列,这样分的时间段越多,需要的交换机及队列也越多。前面的方式实现起来有点复杂,其实有更简单的实现方式,在RabbitMQ 3.5.7及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。同时插件依赖Erlang/OPT 18.0及以上。
注意:使用rabbitmq-delayed-message-exchange插件时发送到队列的消息数量在web管理界面不可见,不影响正常功能使用
插件源码地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange,插件下载地址为:https://bintray.com/rabbitmq/community-plugins/rabbitmq_delayed_message_exchange,进去之后可以看到如下图的界面,可以选择对应的版本进行下载
我选的v3.6.x,地址为https://bintray.com/rabbitmq/community-plugins/rabbitmq_delayed_message_exchange/v3.6.x,点进去之后可以看到如下所示,点击下载即可。
下载完成后,进入到我们的RabbitMQ的安装目录,把插件放入plugins目录下面, 然后可以通过如下命令管理该插件
启用插件:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
关闭插件:rabbitmq-plugins disable rabbitmq_delayed_message_exchange
我们启用该插件之后,可以登上web管理台,在添加交换机时可以看到多了一个x-delayed-message类型,而这个类型是rabbitmq_delayed_message_exchange插件提供的,RabbitMQ自己是没有的,可以通过x-delayed-type参数设置fanout /direct / topic / header 类型。
声明x-delayed-message类型的交换机代码如下所示:
Map args = new HashMap();
args.put("x-delayed-type", "direct");
channel.exchangeDeclare("delay_plugin_exchange", "x-delayed-message", true, false, args);
发送消息的时候通过header添加"x-delay"参数来设置消息的延时时间,其单位为毫秒
Map headers = new HashMap();
headers.put("x-delay", 5000);
BasicProperties props = new AMQP.BasicProperties.Builder()
.headers(headers).build();
channel.basicPublish("delay_plugin_exchange", "delay", props , "延迟消息".getBytes());
下面是完整的代码示例(偶数序号消息延迟五秒,奇数序号延迟两秒):
package cn.wkp.rabbitmq.newest.delayqueue.plugin;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import cn.wkp.rabbitmq.util.ConnectionUtil;
public class Send {
private final static String EXCHANGE_NAME = "delay_plugin_exchange";
private final static String ROUTING_KEY = "delay";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
//声明x-delayed-type类型的exchange
Map args = new HashMap();
args.put("x-delayed-type", "direct");
channel.exchangeDeclare(EXCHANGE_NAME, "x-delayed-message", true, false, args);
// 消息内容
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
Map headers= new HashMap();
long delayTime=2000;
if(i%2==0) {
delayTime=5000;
}
headers.put("x-delay",delayTime);//消息延迟时间
BasicProperties props = new AMQP.BasicProperties.Builder()
.headers(headers).build();
String message = "序号:" + i + ",时间:" + ConnectionUtil.formatDate(new Date());
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, props, message.getBytes());
System.out.println("Sent message:" + message);
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
package cn.wkp.rabbitmq.newest.delayqueue.plugin;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import cn.wkp.rabbitmq.util.ConnectionUtil;
public class Recv {
private final static String QUEUE_NAME = "delay_plugin_queue";
private final static String EXCHANGE_NAME = "delay_plugin_exchange";
private final static String ROUTING_KEY = "delay";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
final Channel channel = connection.createChannel();
//声明x-delayed-type类型的exchange
Map args = new HashMap();
args.put("x-delayed-type", "direct");
channel.exchangeDeclare(EXCHANGE_NAME, "x-delayed-message", true, false, args);
// 声明队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 指该消费者在接收到队列里的消息但没有返回确认结果之前,它不会将新的消息分发给它。
channel.basicQos(1);
// 定义队列的消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
System.out.println("收到消息:" + new String(body)+",当前时间:"+ConnectionUtil.formatDate(new Date()));
// 消费者手动发送ack应答
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
然后运行结果如下:我们看到延迟一般只有一二十毫秒,效率还是可以的。
Sent message:序号:0,时间:2019-04-21 21:19:10:922
Sent message:序号:1,时间:2019-04-21 21:19:11:934
Sent message:序号:2,时间:2019-04-21 21:19:12:935
Sent message:序号:3,时间:2019-04-21 21:19:13:936
Sent message:序号:4,时间:2019-04-21 21:19:14:939
收到消息:序号:1,时间:2019-04-21 21:19:11:934,当前时间:2019-04-21 21:19:13:959
收到消息:序号:0,时间:2019-04-21 21:19:10:922,当前时间:2019-04-21 21:19:15:936
收到消息:序号:3,时间:2019-04-21 21:19:13:936,当前时间:2019-04-21 21:19:15:953
收到消息:序号:2,时间:2019-04-21 21:19:12:935,当前时间:2019-04-21 21:19:17:942
收到消息:序号:4,时间:2019-04-21 21:19:14:939,当前时间:2019-04-21 21:19:19:946
运行过程中,我们可以查看一下web管理台的队列信息:该队列上显示的消息数量一直是0,印证了我们上面的说法,不过不影响使用。
然后可以通过控制台查看一下我们添加的交换机:其类型为x-delayed-message,这个是插件提供的,RabbitMQ本身并不提供。
好了,关于RabbitMQ实现延迟队列就先介绍到这里,下一节将会介绍与Spring的整合使用。