什么是延迟队列
通俗一点说,延迟队列和我们生活中常用的定时器有点像,定时器会在指定的时间后响起,延迟队列则会在指定的时间后处理消息。延迟队列主要的应用场景有订单超时取消、超时自动评价等等。
实现原理
RabbitMQ给我们提供了TTL(Time-To-Live)和DLX (Dead-Letter-Exchange)这两个特性,(队列的TTL和消息的TTL如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为dead letter)使用RabbitMQ实现延迟队列利用的正是这两个特性。TTL用于控制消息的生存时间,如果超时,消息将变成Dead Letter。DLX用于配置死信队列,可以通过配置x-dead-letter-exchange和x-dead-letter-routing-key这两个参数来指定消息变成死信后需要路由到的交换器和队列。
图例
首先看看死信队列的图
然后就是延迟队列,延迟队列本身就是死信队列的扩展
代码实例
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--创建连接工厂-->
<rabbit:connection-factory id="mqConnectionFactory"
host="***.***.**.**"
port="****"
username="***"
password="***"
publisher-confirms="true"
virtual-host="*****"/>
<!--通过指定下面的admin信息,当前producer中的exchange和queue会在rabbitmq服务器上自动生成 -->
<rabbit:admin id="orderConnectAdmin" connection-factory="mqConnectionFactory"/>
<!-- <rabbit:admin id="delayConnectAdmin" connection-factory="delayConnectionFactory"/>-->
<!-- 说明:将需要关闭的订单的消息发送到此队列
durable: 为 true 则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。
autoDelete: 设置是否自动删除。为 true 则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到
这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除
-->
<rabbit:queue name="shanreal_order_shutdown_queue" durable="false" auto-declare="true">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value="10000" value-type="java.lang.Long" />
<entry key="x-dead-letter-exchange" value="shanreal_exchange_delay" />
<entry key="x-dead-letter-routing-key" value="shanreal_delay_key" />
</rabbit:queue-arguments>
</rabbit:queue>
<!--正常交换机
durable:true 持久化。可以将交换器存盘,在服务器重启 的时候不会丢失相关信息。false 反之
auto-declare:true 自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定 ,
之后所有与这个交换器绑定的队列或者交换器都与此解绑
-->
<rabbit:fanout-exchange name="shanreal_exchange_normal" durable="false" auto-delete="true" id="shanreal_exchange_normal">
<rabbit:bindings>
<rabbit:binding queue="shanreal_order_shutdown_queue" />
</rabbit:bindings>
</rabbit:fanout-exchange>
<!--死亡队列,用于存储超时消息-->
<rabbit:queue name="shanreal_delay_queue" durable="false" auto-declare="true"/>
<!--死亡交换机,消息过时后会由此交换机转发到对应的队列-->
<rabbit:direct-exchange name="shanreal_exchange_delay" durable="false" auto-delete="true" id="shanreal_exchange_delay">
<rabbit:bindings>
<rabbit:binding queue="shanreal_delay_queue" key="shanreal_delay_key"/>
</rabbit:bindings>
</rabbit:direct-exchange>
<!--定义rabbit orderAmqpTemplate用于数据的生产 -->
<rabbit:template id="orderAmqpTemplate" connection-factory="mqConnectionFactory" exchange="shanreal_exchange_normal"/>
<!--定义rabbit delayAmqpTemplate用于死信队列数据的消费 -->
<rabbit:template
id="delayAmqpTemplate"
connection-factory="mqConnectionFactory"
exchange="shanreal_exchange_delay"
queue="shanreal_delay_queue"/>
<!-- 配置线程池 -->
<bean id ="taskExecutor" class ="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor" >
<!-- 线程池维护线程的最少数量 -->
<property name ="corePoolSize" value ="5" />
<!-- 线程池维护线程所允许的空闲时间 -->
<property name ="keepAliveSeconds" value ="30000" />
<!-- 线程池维护线程的最大数量 -->
<property name ="maxPoolSize" value ="1000" />
<!-- 线程池所使用的缓冲队列 -->
<property name ="queueCapacity" value ="200" />
</bean>
<bean id="delayTask" class="com.shanreal.controller.gb.rubbitMq.DelayTask" />
<!-- Queue Listener 当有消息到达时会通知监听在对应的队列上的监听对象-->
<rabbit:listener-container connection-factory="mqConnectionFactory" acknowledge="auto" task-executor="taskExecutor">
<rabbit:listener queues="shanreal_delay_queue" ref="delayTask"/>
</rabbit:listener-container>
</beans>
生产者:
这里可以单独对每个消息设置TTL(消息生存时间)
import com.rabbitmq.client.ConnectionFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class ProducerService {
/* @Resource(name="mqConnectionFactory")
private ConnectionFactory mqConnectionFactory;*/
@Resource(name="orderAmqpTemplate")
private AmqpTemplate orderAmqpTemplate;
public void send(String msg) {
orderAmqpTemplate.convertAndSend((Object) msg, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
System.out.println("Sent: " + msg);
}
}
setExpiration是基于单个消息的超时设置,但是当我们先增加一条过期时间大(10000)的A消息进入,之后再增加一个过期时间小的(1000)消息B,并没有出现想象中的B消息先被消费,A消息后被消费,而是出现了当10000过去的时候,AB消息同时被消费,也就是B消息的消费被阻塞了。
为什么会出现这样的现象呢?
我们知道利用TTL DLX特性实现的方式,实际上在第一个延时队列C里面设置了dlx,生产者生产了一条带ttl的消息放入了延时队列C中,等到延时时间到了,延时队列C中的消息变成了死信,根据延时队列C中设置的dlx的exchange的转发规则,转发到了实际消费队列D中,当该队列中的监听器监听到消息时就会正式开始消费。那么实际上延时队列中的消息也是放入队列中的,队列满足先进先出,而延时大的消息A还没出队,所以B消息也不能顺利出队。
利用Rabbitmq的插件x-delay-message实现
为了解决上面的问题,Rabbitmq实现了一个插件x-delay-message来实现延时队列。
消费者:
这里可以手动消费
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class ConsumerService {
@Resource(name="delayAmqpTemplate")
private AmqpTemplate delayAmqpTemplate;
public void recive() {
System.out.println("Received: " + delayAmqpTemplate.receiveAndConvert());
}
}
执行生产者操作
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
@Controller
@RequestMapping(value="/mqTest")
public class TestController {
@Resource(name = "producerService")
private ProducerService producerService;
@Resource(name = "consumerService")
private ConsumerService consumerService;
@ResponseBody
@RequestMapping(value="/producerTest")
public void producerTest() throws Exception {
String s = "我是生产者测试";
producerService.send(s);
}
@ResponseBody
@RequestMapping(value="/consumerTest")
public void consumerTest() throws Exception {
consumerService.recive();
}
}
通过监听死信队列的消息来进行消费
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
public class DelayTask implements MessageListener {
@Override
public void onMessage(Message message) {
try {
String receivedMsg = new String(message.getBody(), "UTF-8");
System.out.println("Received : " + receivedMsg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
参考:rubbitMq实战指南