什么是延时队列
延时队列:顾名思义,是一个用于作消息延时消费的队列。可是它也是一个普通队列,因此它具有普通队列的特性,相比之下,延时的特性就是它最大的特色。所谓的延时就是将咱们须要的消息,延迟多久以后被消费。普通队列是即时消费的,延时队列是根据延时时间,多久以后才能消费的。java
延时队列使用场景
- 订单在十分钟以内未支付则自动取消。
- 会员续费的定时推送
- 用户注册成功后,若是三天内没有登录则进行短信提醒。
- 预约会议后,须要在预约的时间点前十分钟通知各个与会人员参加会议。
- 优惠券过时提醒
核心的应用内容基本都是基于须要设定过时时间的ui
RabbitMQ如何实现延时队列
- 方式一、经过RabbitMQ的高级特性TTL和配合死信队列
- 方式二、安装rabbitmq_delayed_message_exchange插件
RabbitMQ中的高级特性TTL
TTL是什么呢?TTL是RabbitMQ中一个消息或者队列的属性,代表一条消息或者该队列中的全部消息的最大存活时间,单位是毫秒,为何延时队列要介绍它?TTL就是一种消息过时策略。给咱们的消息作过时处理,当消息在队列中存活了指定时候以后,改队列就会将这个消息直接丢弃。在RabbitMQ中并无直接实现好的延时队列,咱们可使用TTL这种高级特性,而后配合死信队列,便可实现延时队列的功能。插件
那么,如何设置这个TTL值呢?有两种方式,第一种是在建立队列的时候设置队列的“x-message-ttl”属性,以下: 方式一:3d
Map<String, Object> args = new HashMap<String, Object>(); args.put("x-message-ttl", 6000); channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
使用这种方式,消息被设定TTL,一旦消息过时,就会被队列丢弃code
方式二:blog
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder(); builder.expiration("6000"); AMQP.BasicProperties properties = builder.build(); channel.basicPublish(exchangeName, routingKey, mandatory, properties, "msg body".getBytes());
使用这种方式,消息即便过时,也不必定会被立刻丢弃,由于消息是否过时是在即将投递到消费者以前断定的,若是当前队列有严重的消息积压状况,则已过时的消息也许还能存活较长时间。rabbitmq
另外,还须要注意的一点是,若是不设置TTL,表示消息永远不会过时,若是将TTL设置为0,则表示除非此时能够直接投递该消息到消费者,不然该消息将会被丢弃。队列
RabbitMQ到底怎么实现延时队列
- 步骤一:建立一个正常的队列,指定消息过时时间,而且指定消息过时后须要投递的死信交换器和死信交换队列。
- 步骤二:建立死信队列和死信交换器
RabbitMQ实现延时队列实例
package com.example.demo; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; /** * @author echo * @date 2021-01-14 14:35 */ public class TopicDealProductTest { /** * 延时队列交换机 */ private static final String DIRECT_EXCHANGE_DELAY = "dir_exchange_delay"; /** * 死信队列交换机 */ private static final String DIRECT_EXCHANGE_DEAD = "dir_exchange_dead"; /** * 延时队列 */ private static final String DIRECT_QUEUE_DELAY = "dir.queue.delay"; /** * 死信队列 */ private static final String DIRECT_QUEUE_DEAD = "dir.queue.dead"; /** * 延时队列ROUTING_KEY */ private static final String DIRECT_DELAY_ROUTING_KEY = "delay.queue.routingKey"; /** * 延时队列ROUTING_KEY */ private static final String DIRECT_DEAD_ROUTING_KEY = "dead.queue.routingKey"; private static final String IP_ADDRESS = "192.168.230.131"; private static final int PORT = 5672; public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { Connection connection = createConnection(); // 建立一个频道 Channel channel = connection.createChannel(); sendMsg(channel); Thread.sleep(10000); closeConnection(connection, channel); } private static void sendMsg(Channel channel) throws IOException { // 建立延时队列和延时交换器 channel.exchangeDeclare(DIRECT_EXCHANGE_DELAY, BuiltinExchangeType.DIRECT); Map<String, Object> map = new HashMap<>(16); // 在延时交换器上指定死信交换器 map.put("x-dead-letter-exchange", DIRECT_EXCHANGE_DEAD); // 在延时交换器上指定死信队列的routing-key map.put("x-dead-letter-routing-key", DIRECT_DEAD_ROUTING_KEY); // 设定延时队列的延长时长 10s map.put("x-message-ttl", 10000); // 建立延时队列 channel.queueDeclare(DIRECT_QUEUE_DELAY, true, false, false, map); // 在延时交换器上绑定延时队列 channel.queueBind(DIRECT_QUEUE_DELAY, DIRECT_EXCHANGE_DELAY, DIRECT_DELAY_ROUTING_KEY); // 建立死信队列和死信交换器 channel.exchangeDeclare(DIRECT_EXCHANGE_DEAD, BuiltinExchangeType.TOPIC, true, false, null); // 建立死信队列 channel.queueDeclare(DIRECT_QUEUE_DEAD, true, false, false, null); // 在死信交换器上绑定死信队列 channel.queueBind(DIRECT_QUEUE_DEAD, DIRECT_EXCHANGE_DEAD, DIRECT_DEAD_ROUTING_KEY); channel.basicPublish(DIRECT_EXCHANGE_DELAY, DIRECT_DELAY_ROUTING_KEY, null, "hello world".getBytes()); } private static void closeConnection(Connection connection, Channel channel) throws IOException, TimeoutException { // 关闭资源 channel.close(); connection.close(); } private static Connection createConnection() throws IOException, TimeoutException { // 建立链接工厂 ConnectionFactory factory = new ConnectionFactory(); // 设置RabbitMQ的连接参数 factory.setHost(IP_ADDRESS); factory.setPort(PORT); factory.setUsername("echo"); factory.setPassword("123456"); // 和RabbitMQ创建一个连接 return factory.newConnection(); } }
到这里,其实咱们不难发现,咱们无非是利用了TTL这个特性,让消息在过时的时候丢弃到指定队列,死信队列其实也是一个普通队列。图片
执行以后,咱们来看看结果,在Exchange里面,咱们建立了两个交换器和两个队列,可是两个队列和交换器仍是有区别的,咱们来看图片资源
咱们能够看到两个队列的Features标志是不同的
- TTL: 消息在队列中的过时时间
- DLX: 该队列绑定了死信交换器
- DLK: 该队列绑定的死信队列的ROUTING_KEY
在咱们执行完成只有,咱们能够看到,消息先被投递到了delay,该队列里面的消息,到达过时时间以后就被投递到了dead队列中去了。
那么咱们上面介绍了TTL和设置AMQP.BasicProperties,这两种有必定的区别,前一个是设置队列消息过时时间,后一个是设定每条消息的过时时间。那他们的区别在哪里?
设置每条消息和设置TTL的区别
其实这两种方式的区别就在于怎么判断该消息是否要被丢弃。TTL设定的队列,只要消息到达过时时间,立马就会将消息丢弃。若是是后者,可能咱们队列里面有不少的消息,而后每条消息的过时时间又不一致,这个时候,若是队列出口处堵了不少没有设定过时时间的消息又不被消费的时候,队列后面的消息及时设定了过时时间也不会被丢弃,只有在设定了过时时间的消息到了队列该消费的位置,才会断定
怎么使用AMQP.BasicProperties?
package com.example.demo; import com.rabbitmq.client.*; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeoutException; /** * @author echo * @date 2021-01-14 14:35 */ public class TopicDealProductTest { /** * 延时队列交换机 */ private static final String DIRECT_EXCHANGE_DELAY = "dir_exchange_delay"; /** * 死信队列交换机 */ private static final String DIRECT_EXCHANGE_DEAD = "dir_exchange_dead"; /** * 延时队列 */ private static final String DIRECT_QUEUE_DELAY = "dir.queue.delay"; /** * 死信队列 */ private static final String DIRECT_QUEUE_DEAD = "dir.queue.dead"; /** * 延时队列ROUTING_KEY */ private static final String DIRECT_DELAY_ROUTING_KEY = "delay.queue.routingKey"; /** * 延时队列ROUTING_KEY */ private static final String DIRECT_DEAD_ROUTING_KEY = "dead.queue.routingKey"; private static final String IP_ADDRESS = "192.168.230.131"; private static final int PORT = 5672; public static void main(String[] args) throws IOException, TimeoutException, InterruptedException { Connection connection = createConnection(); // 建立一个频道 Channel channel = connection.createChannel(); sendMsg(channel); Thread.sleep(10000); closeConnection(connection, channel); } private static void sendMsg(Channel channel) throws IOException { // 建立延时队列和延时交换器 channel.exchangeDeclare(DIRECT_EXCHANGE_DELAY, BuiltinExchangeType.DIRECT); Map<String, Object> map = new HashMap<>(16); // 在延时交换器上指定死信交换器 map.put("x-dead-letter-exchange", DIRECT_EXCHANGE_DEAD); map.put("x-dead-letter-routing-key", DIRECT_DEAD_ROUTING_KEY); // 设定延时队列的延长时长 10s // map.put("x-message-ttl", 10000); // 建立延时队列 channel.queueDeclare(DIRECT_QUEUE_DELAY, true, false, false, map); // 在延时交换器上绑定延时队列 channel.queueBind(DIRECT_QUEUE_DELAY, DIRECT_EXCHANGE_DELAY, DIRECT_DELAY_ROUTING_KEY); // 建立死信队列和死信交换器 channel.exchangeDeclare(DIRECT_EXCHANGE_DEAD, BuiltinExchangeType.TOPIC, true, false, null); // 建立死信队列 channel.queueDeclare(DIRECT_QUEUE_DEAD, true, false, false, null); // 在死信交换器上绑定死信队列 channel.queueBind(DIRECT_QUEUE_DEAD, DIRECT_EXCHANGE_DEAD, DIRECT_DEAD_ROUTING_KEY); AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder(); builder.expiration("10000"); AMQP.BasicProperties properties = builder.build(); channel.basicPublish(DIRECT_EXCHANGE_DELAY, DIRECT_DELAY_ROUTING_KEY, false, properties, "hello world".getBytes()); } private static void closeConnection(Connection connection, Channel channel) throws IOException, TimeoutException { // 关闭资源 channel.close(); connection.close(); } private static Connection createConnection() throws IOException, TimeoutException { // 建立链接工厂 ConnectionFactory factory = new ConnectionFactory(); // 设置RabbitMQ的连接参数 factory.setHost(IP_ADDRESS); factory.setPort(PORT); factory.setUsername("echo"); factory.setPassword("123456"); // 和RabbitMQ创建一个连接 return factory.newConnection(); } }
咱们运行完成成以后,能够看到和咱们以前那一种方式的效果是同样的
两种设定过时时间的方式其实区别就在于一个统一设定了过时时间,一个指定每条过时时间。可是并不影响咱们延时队列的实现,那咱们怎么选择呢?
怎么选择TTL设定方式?
根据两种方式的特性来选定使用场景才是最合理的。咱们若是用来作延时队列的,想将延时队列的特性应用到实际场景的,而且对时时性要求比较高的,建议选择第一种方式。
总结
延时队列的实现并不难,关键是咱们要知道他的一个原理,了解RabbitMQ他的TTL和死信对了。掌握了它的这些特性以后,咱们就能够很好的应用延时队列。延时队列在工做中对咱们的帮组也很是大,不过RabbiTMQ没有原生延时队列,咱们用这种方式实现了它并不意味着咱们必定要选择它。其实还有不少的方式,好比Java中的DelayQueu、kafka的时间轮等。