一.延時隊列使用場景
在很多的業務場景中,延時隊列可以實現很多功能,此類業務中,一般上是非實時的,需要延遲處理的,需要進行重試補償的。
- 訂單超時關閉:在支付場景中,一般上訂單在創建後30分鐘或1小時內未支付的,會自動取消訂單。
- 短信或者郵件通知:在一些註冊或者下單業務時,需要在1分鐘或者特定時間後進行短信或者郵件發送相關資料的。
- 重試場景:比如消息通知,在第一次通知出現異常時,會在隔幾分鐘之後進行再次重試發送。
二.RabbitMQ實現延時隊列
本身在RabbitMQ中是未直接提供延時隊列功能的,但可以使用 TTL(Time-To-Live,存活時間) 和 DLX(Dead-Letter-Exchange,死信隊列交換機) 的特性實現延時隊列的功能。
三.存活時間 TTL(Time-To-Live )
RabbitMQ中可以對隊列和消息分別設置TTL,TTL表明了一條消息可在隊列中存活的最大時間。當某條消息被設置了TTL或者當某條消息進入了設置了TTL的隊列時,這條消息會在TTL時間後死亡成爲Dead Letter。如果既配置了消息的TTL,又配置了隊列的TTL,那麼較小的那個值會被取用。
四.死信交換 DLX(Dead Letter Exchanges )
設置了TTL的消息或隊列最終會成爲Dead Letter,當消息在一個隊列中變成死信之後,它能被重新發送到另一個交換機中,這個交換機就是DLX,綁定此DLX的隊列就是死信隊列。
一個消息變成死信一般上是由於以下幾種情況;
- 消息被拒絕
- 消息過期
- 隊列達到了最大長度。
所以,通過TTL和DLX的特性可以模擬實現延時隊列的功能。當隊列中的消息超時成爲死信後,會把消息死信重新發送到配置好的交換機中,然後分發到真實的消費隊列。故簡單來說,我們可以創建2個隊列,一個隊列用於發送消息,一個隊列用於消息過期後的轉發的目標隊列。
五. SpringBoot集成RabbitMQ實現延時隊列實戰
下面是死信隊列在創建、綁定、生產消息、消費消息過程的結構流程圖:
圖中問題的答案爲:當入死信隊列的消息TTL一到,它自然而然的將被路由到 死信交換機綁定的隊列 中被真正消費處理!!!
六.rabbitmq-produce的改動
項目使用上一篇中的項目 rabbitmq-produce、rabbitmq-consumer
2.1 在rabbitmq-produce中,新增一個DealRabbitConfig 配置類
DealRabbitConfig 的代碼如下:
package com.example.rabbitmqproduce.config;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
/**
* 死信隊列
*/
@Configuration
public class DealRabbitConfig {
private Logger logger = LoggerFactory.getLogger(DealRabbitConfig.class);
//死信隊列 模型
@Bean
public Queue dealQueue() {
Map<String, Object> argsMap = Maps.newHashMap();
//當變成死信隊列時,會轉發至 路由爲x-dead-letter-exchange及x-dead-letter-routing-key的隊列中
argsMap.put("x-dead-letter-exchange", "deal.exchange");
argsMap.put("x-dead-letter-routing-key", "deal.routing.key");
//過期時間(單位:毫秒),當過期後 會變成死信隊列,之後進行轉發
//TTL 這邊可以不填,可以動態設置,通過MessageProperies屬性設定
argsMap.put("x-message-ttl", 10000);
return new Queue("deal.queue", true, false, false, argsMap);
}
//生產端的交換機
@Bean
public TopicExchange produceExchange(){
return new TopicExchange("produce.exchange",true,false);
}
//生產端的交換機和路由key 綁定 死信隊列
@Bean
public Binding produceToDeal(){
return BindingBuilder.bind(dealQueue()).to(produceExchange()).with("produce.routing.key");
}
//消費端的隊列
@Bean
public Queue consumerQueue(){
return new Queue("consumer.queue",true);
}
//死信交換機
@Bean
public TopicExchange deadExchange(){
return new TopicExchange("deal.exchange",true,false);
}
//死信交換機+死信路由key->消費端的隊列
@Bean
public Binding dealToConsumer(){
return BindingBuilder.bind(consumerQueue()).to(deadExchange()).with("deal.routing.key");
}
}
6.2 新建消息生產者RabbitDealProduce
RabbitDealProduce 的代碼如下:
package com.example.rabbitmqproduce.produce;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 死信隊列 消息生產者
* @Component 注入到Spring容器中
*/
@Component
public class RabbitDealProduce {
//注入一個AmqpTemplate來發布消息
@Autowired
private AmqpTemplate rabbitTemplate;
private Logger logger = LoggerFactory.getLogger(RabbitDealProduce.class);
/**
* topicA 發送消息
*/
public void send() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "hello!死信隊列";
String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
Map<String,Object> map=new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
logger.info("發送的內容 : " + map.toString());
rabbitTemplate.convertAndSend("produce.exchange", "produce.routing.key", map);
}
}
6.3 寫個測試的TestController類
代碼如下:
package com.example.rabbitmqproduce.controller;
import com.example.rabbitmqproduce.produce.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("TestController")
public class TestController {
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
@Autowired
private RabbitMqProduce rabbitMqProduce;
@Autowired
private DirectExchangeProduce directExchangeProduce;
@Autowired
private FanoutExchangeProduce fanoutExchangeProduce;
@Autowired
private TopicExchangeProduce topicExchangeProduce;
@Autowired
private RabbitAckProduce rabbitAckProduce;
@Autowired
private RabbitDealProduce rabbitDealProduce;
/**
* 測試基本消息模型(簡單隊列)
*/
@RequestMapping(value = "/testSimpleQueue", method = RequestMethod.POST)
public void testSimpleQueue() {
logger.info("測試基本消息模型(簡單隊列)SimpleQueue---開始");
for (int i = 0; i < 10; i++) {
rabbitMqProduce.sendMessage();
}
logger.info("測試基本消息模型(簡單隊列)SimpleQueue---結束");
}
/**
* 測試 Direct-exchange模式
*/
@RequestMapping(value = "/directExchangeTest", method = RequestMethod.POST)
public void directExchangeTest() {
logger.info("測試 Direct-exchange模式 隊列名爲directQueue---開始");
for (int i = 0; i < 10; i++) {
directExchangeProduce.sendMessage();
}
logger.info("測試 Direct-exchange模式 隊列名爲directQueue---結束");
}
/**
* 測試 Fanout-exchange模式
*/
@RequestMapping(value = "/fanoutExchangeTest", method = RequestMethod.POST)
public void fanoutExchangeTest() {
logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---開始");
fanoutExchangeProduce.sendMessage();
logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---結束");
}
/**
* 測試 Topic-exchange模式 topicA 和 topicB
*/
@RequestMapping(value = "/topictExchangeTest", method = RequestMethod.POST)
public void topictExchangeTest() {
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---開始");
topicExchangeProduce.sendMessageTopicA();
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---結束");
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---開始");
topicExchangeProduce.sendMessageTopicB();
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---結束");
}
/**
* 測試 ack模式
*/
@RequestMapping(value = "/ackTest", method = RequestMethod.POST)
public void ackTest() {
logger.info("測試 ack 隊列名爲ackQueue---開始");
rabbitAckProduce.sendMessage();
logger.info("測試 ack 隊列名爲ackQueue---結束");
}
/**
* 測試 死信隊列
*/
@RequestMapping(value = "/dealTest", method = RequestMethod.POST)
public void dealTest() {
logger.info("測試 死信隊列---開始");
rabbitDealProduce.send();
logger.info("測試 死信隊列---結束");
}
}
七.rabbitmq-consumer的改動
7.1 新建消息消費者RabbitDealConsumer類
RabbitDealConsumer代碼如下:
package com.example.rabbitmqconsumer.consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 死信隊列 的消息消費者
* @RabbitListener(queues = "consumer.queue") 監聽名爲consumer.queue的隊列
*/
@Component
@RabbitListener(queues = "consumer.queue")
public class RabbitDealConsumer {
@Autowired
private AmqpTemplate rabbitmqTemplate;
private Logger logger = LoggerFactory.getLogger(RabbitDealConsumer.class);
/**
* 消費消息
* @RabbitHandler 代表此方法爲接受到消息後的處理方法
*/
@RabbitHandler
public void receiveMessage(Map msg){
logger.info("經過死信之後,消費者接收到的消息 :" + msg.toString());
}
}
四.測試
首先啓動生產者rabbitmq-produce項目。在postman或瀏覽器上訪問:
http://localhost:8783/TestController/dealTest POST請求
這時可以在rabbitmq-produce的控制檯可以看到
然後再啓動消費者rabbitmq-consumer工程,在rabbitmq-consumer可以看到: