延時隊列的概念
場景一:物聯網系統經常會遇到向終端下發命令,如果命令一段時間沒有應答,就需要設置成超時。
場景二:訂單下單之後30分鐘後,如果用戶沒有付錢,則系統自動取消訂單。
上述場景都是有一個共同的地方,在物聯網在給終端下發命令時,在用戶訂單下單時,會發送一個消息到隊列鍾,同時設定改條消息的過期時間,如果終端在過期時間之內響應了物聯網的命令或者用戶在過期時間之內進行了付款,則會將改條消息刪除,否則改條消息機會被消費,告知物聯網超時或者用戶未在規定時間內付款,自動取消訂單。
使用RabbitMQ來實現延遲任務必須先了解RabbitMQ的兩個概念:消息的TTL和死信Exchange,通過這兩者的組合來實現上述需求。
消息的TTL就是消息的存活時間。RabbitMQ可以對隊列和消息分別設置TTL。對隊列設置就是隊列沒有消費者連着的保留時間,也可以對每一個單獨的消息做單獨的設置。超過了這個時間,我們認爲這個消息就死了,稱之爲死信。如果隊列設置了,消息也設置了,那麼會取小的。所以一個消息如果被路由到不同的隊列中,這個消息死亡的時間有可能不一樣(不同的隊列設置)。這裏單講單個消息的TTL,因爲它纔是實現延遲任務的關鍵。
可以通過設置消息的expiration字段或者x-message-ttl屬性來設置時間,兩者是一樣的效果。只是expiration字段是字符串參數,所以要寫個int類型的字符串
當消息扔到隊列中後,過了60秒,如果沒有被消費,它就死了。不會被消費者消費到。這個消息後面的,沒有“死掉”的消息對頂上來,被消費者消費。死信在隊列中並不會被刪除和釋放,它會被統計到隊列的消息數中去。單靠死信還不能實現延遲任務,還要靠Dead Letter Exchange。
Dead Letter Exchanges
Exchage的概念在這裏就不在贅述,可以從這裏進行了解。一個消息在滿足如下條件下,會進死信路由,記住這裏是路由而不是隊列,一個路由可以對應很多隊列。
1. 一個消息被Consumer拒收了,並且reject方法的參數裏requeue是false。也就是說不會被再次放在隊列裏,被其他消費者使用。
2. 上面的消息的TTL到了,消息過期了。
3. 隊列的長度限制滿了。排在前面的消息會被丟棄或者扔到死信路由上。
Dead Letter Exchange其實就是一種普通的exchange,和創建其他exchange沒有兩樣。只是在某一個設置Dead Letter Exchange的隊列中有消息過期了,會自動觸發消息的轉發,發送到Dead Letter Exchange中去。
接下來是demo實現:
package com.ucar.rabbitmq.config;
import com.rabbitmq.client.AMQP;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author
* @since 2018/8/27 16:42
*/
@Configuration
public class MyRabbitMQConfig {
//延時隊列
public static final String delayQueue = "delayQueue";
//正常隊列
public static final String normalQueue = "normalQueue";
//延時交換機
public static final String delayExchange = "delayExchange";
//正常交換機
public static final String normalExchange = "normalExchange";
//延時隊列路由鍵
public static final String DELAY_ROUTING_KEY = "delay-routing-key";
//正常隊列路由鍵
public static final String NORMAL_ROUTING_KEY = "normal-routing-key";
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public Queue getDelayQueue() {
Map args = new HashMap();
/**
* 消息發送給延時隊列
* 設置延時隊列的過期時間爲5秒鐘
* 5秒之後,延時隊列將消息發送給普通隊列
*/
args.put("x-dead-letter-exchange", normalExchange);
args.put("x-dead-letter-routing-key", NORMAL_ROUTING_KEY);
args.put("x-message-ttl", 5000);
return QueueBuilder.durable(delayQueue).withArguments(args).build();
}
//創建延時交換機
@Bean
public Exchange getDelayExchange() {
return ExchangeBuilder.directExchange(delayExchange).durable(true).build();
}
//延時與延時交換機進行綁定
@Bean
public Binding bindDelay() {
return BindingBuilder.bind(getDelayQueue()).to(getDelayExchange()).with(DELAY_ROUTING_KEY).noargs();
}
//創建延時交換機
@Bean
public Queue getNormalQueue() {
return new Queue(normalQueue);
}
//創建普通交換機
@Bean
public Exchange getNormalExchange() {
return ExchangeBuilder.directExchange(normalExchange).durable(true).build();
}
//普通隊列與普通交換機進行綁定
@Bean
public Binding bindNormal() {
return BindingBuilder.bind(getNormalQueue()).to(getNormalExchange()).with(NORMAL_ROUTING_KEY).noargs();
}
}
@PostMapping("/test00")
public void sendDelayMessage() {
logger.info("開始發送消息: " + "延時5秒消費");
rabbitTemplate.convertAndSend(MyRabbitMQConfig.delayExchange, MyRabbitMQConfig.DELAY_ROUTING_KEY, "延時5秒消費");
}
spring:
rabbitmq:
virtual-host: /
host: localhost
username: guest
password: guest