延迟队列
延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。
场景:
- 延迟消费。比如:
- 用户生成订单之后,需要过一段时间校验订单的支付状态,如果订单仍未支付则需要及时地关闭订单。
- 用户注册成功之后,需要过一段时间比如一周后校验用户的使用情况,如果发现用户活跃度较低,则发送邮件或者短信来提醒用户使用。
- 延迟重试。比如消费者从队列里消费消息时失败,但是想要延迟一段时间后自动重试。
介绍一下RabbitMQ的两个特性,一个是Time-To-Live Extensions,另一个是Dead Letter Exchanges。
Time-To-Live Extensions
RabbitMQ允许为消息或者队列设置TTL(time to live),也就是过期时间。TTL表明一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置TTL或者当某条消息进入设置了TTL的队列时,这条消息会在经过TTL秒后“死亡”,成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。
Dead Letter Exchange
被设置TTL的消息在过期后会成为Dead Letter。其实在RabbitMQ中,一共有三种消息的“死亡”形式:
- 消息被拒绝。通过调用basic.reject或者basic.nack并且设置的requeue参数为false。
- 消息因为设置TTL而过期。
- 消息进入一条已经达到最大长度的队列。
如果队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新publish到Dead Letter Exchange,通过Dead Letter Exchange路由到其他队列。
生产者产生的消息首先会进入缓冲队列(图中红色队列)。通过RabbitMQ提供的TTL扩展,这些消息会被设置过期时间,也就是延迟消费的时间。等消息过期之后,这些消息会通过配置好的DLX转发到实际消费队列(图中蓝色队列),以此达到延迟消费的效果。

延迟重试
消费者发现该消息处理出现异常,比如是因为网络波动引起的异常。那么如果不等待一段时间,直接就重试的话,很可能会导致在这期间内一直无法成功,造成一定的资源浪费。那么可以将其先放在缓冲队列中(图中红色队列),等消息经过一段的延迟时间后再次进入实际消费队列中(图中蓝色队列),此时由于已经过了“较长”的时间,异常的一些波动通常已经恢复,这些消息可以被正常地消费。

示例:
配置队列
一个延迟队列的实现,需要一个缓冲队列以及一个实际的消费队列。由于在RabbitMQ中,有两种消息过期的配置方式,所以在代码中,共配置了三条队列:
- delay_queue_per_message_ttl:TTL配置在消息上的缓冲队列。
- delay_queue_per_queue_ttl:TTL配置在队列上的缓冲队列。
- delay_process_queue:实际消费队列。
将delay_queue_per_message_ttl以及delay_queue_per_queue_ttl的DLX配置为同一个,且过期的消息都会通过DLX转发到delay_process_queue。
配置文件
@Slf4j @Configuration public class RabbitConfig { /** * 发送到该队列的message会在一段时间后过期进入到delay_process_queue * 每个message可以控制自己的失效时间 */ private final static String DELAY_QUEUE_PER_MESSAGE_TTL_NAME = "delay_queue_per_message_ttl"; /** * 发送到该队列的message会在一段时间后过期进入到delay_process_queue * 队列里所有的message都有统一的失效时间 */ private final static String DELAY_QUEUE_PER_QUEUE_TTL_NAME = "delay_queue_per_queue_ttl"; private static int QUEUE_EXPIRATION = 4000; /** * message失效后进入的队列,也就是实际的消费队列 */ private final static String DELAY_PROCESS_QUEUE_NAME = "delay_process_queue"; /** * DLX */ private final static String DELAY_EXCHANGE_NAME = "delay_exchange"; /** * 路由到delay_queue_per_queue_ttl的exchange */ private static String PER_QUEUE_TTL_EXCHANGE_NAME = "per_queue_ttl_exchange"; /** * 创建DLX exchange */ @Bean public DirectExchange delayExchange() { return new DirectExchange(DELAY_EXCHANGE_NAME); } /** * 创建per_queue_ttl_exchange */ @Bean public DirectExchange perQueueTTLExchange() { return new DirectExchange(PER_QUEUE_TTL_EXCHANGE_NAME); } /** * 创建delay_queue_per_message_ttl队列 * * @return */ @Bean public Queue delayQueuePerMessage() { return QueueBuilder.durable(DELAY_QUEUE_PER_MESSAGE_TTL_NAME) .withArgument("x-dead-letter-exchange", DELAY_EXCHANGE_NAME) //DLX,dead letter发送到的exchange .withArgument("x-dead-letter-routing-key", DELAY_PROCESS_QUEUE_NAME) //dead letter携带的routing key .build(); } @Bean public Queue delayQueuePerQueueTTL() { return QueueBuilder.durable(DELAY_QUEUE_PER_QUEUE_TTL_NAME) .withArgument("x-dead-letter-exchange", DELAY_EXCHANGE_NAME) .withArgument("x-dead-letter-routing-key", DELAY_PROCESS_QUEUE_NAME) .withArgument("x-message-ttl", QUEUE_EXPIRATION) //设置队列的过期时间 .build(); } /** * 创建delay_process_queue队列,也就是实际消费队列 * * @return */ @Bean public Queue delayProcessQueue() { return QueueBuilder.durable(DELAY_PROCESS_QUEUE_NAME).build(); } /** * 将DLX绑定到实际消费队列 * * @param delayProcessQueue * @param delayExchange * @return */ @Bean public Binding dlxBinding(Queue delayProcessQueue, DirectExchange delayExchange) { return BindingBuilder.bind(delayProcessQueue) .to(delayExchange).with(DELAY_PROCESS_QUEUE_NAME); } @Bean public Binding queueTTLBinding(Queue delayQueuePerQueueTTL, DirectExchange perQueueTTLExchange) { return BindingBuilder.bind(delayQueuePerQueueTTL).to(perQueueTTLExchange) .with(DELAY_QUEUE_PER_QUEUE_TTL_NAME); } @Bean public Binding messageTTLBinding(Queue delayQueuePerMessage, DirectExchange perQueueTTLExchange) { return BindingBuilder.bind(delayQueuePerMessage).to(perQueueTTLExchange) .with(DELAY_QUEUE_PER_MESSAGE_TTL_NAME); } /** * 定义delay_process_queue队列的Listener Container * * @param connectionFactory * @return */ @Bean public SimpleMessageListenerContainer processContainer(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); listenerContainer.setConnectionFactory(connectionFactory); listenerContainer.setQueueNames(DELAY_PROCESS_QUEUE_NAME); listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL); listenerContainer.setMessageListener((ChannelAwareMessageListener) (message, channel) -> { try { long deliveryTag = message.getMessageProperties().getDeliveryTag(); String msg = new String(message.getBody()); log.info("接收到消息:{}", msg); channel.basicAck(deliveryTag, false); } catch (Exception e) {
//异常重试 log.info("接收消息处理异常:{}重新进入延迟队列", e.getMessage()); channel.basicPublish(PER_QUEUE_TTL_EXCHANGE_NAME, DELAY_QUEUE_PER_QUEUE_TTL_NAME, null, message.getBody()); } }); return listenerContainer; } }
如何为每个消息设置TTL呢?需要借助MessagePostProcessor。MessagePostProcessor通常用来设置消息的Header以及消息的属性。新建一个ExpirationMessagePostProcessor类来负责设置消息的TTL属性:
public class ExpirationMessagePostProcessor implements MessagePostProcessor { private final Long ttl; public ExpirationMessagePostProcessor(Long ttl) { this.ttl = ttl; } @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().setExpiration(ttl.toString()); return message; } }
向缓冲队列中发送3条消息,过期时间依次为1秒,2秒和3秒。具体的代码如下所示:
@Test public void testDelayQueuePerMessageTTL() throws InterruptedException { for (int i = 1; i <= 3; i++) { long expiration = i * 1000; Object message = "message from delay_queue_per_message_ttl with expiration " + expiration; rabbitTemplate.convertAndSend(DELAY_QUEUE_PER_MESSAGE_TTL_NAME, message, new ExpirationMessagePostProcessor(expiration)); } TimeUnit.SECONDS.sleep(10); }
TTL设置在队列上的代码如下:
@Test public void testDelayQueuePerQueueTTL() throws InterruptedException { for (int i = 1; i <= 3; i++) { Object message = "message from delay_queue_per__ttl with num " + i; rabbitTemplate.convertAndSend(DELAY_QUEUE_PER_QUEUE_TTL_NAME, message); } TimeUnit.SECONDS.sleep(20); }
浙公网安备 33010602011771号