SpringBoot + RabbitMQ 做延時隊列
一、前言
延遲隊列的使用場景:
1.未按時支付的訂單,30分鐘過期之後取消訂單;
2.給活躍度比較低的用戶間隔N天之後推送消息,提高活躍度;
3.過1分鐘給新註冊會員的用戶,發送註冊郵件等。
實現延遲隊列的方式有兩種:
通過消息過期後進入死信交換器,再由交換器轉發到延遲消費隊列,實現延遲功能;
使用rabbitmq-delayed-message-exchange插件實現延遲功能;
注意: 延遲插件rabbitmq-delayed-message-exchange是在RabbitMQ 3.5.7及以上的版本才支持的,依賴Erlang/OPT 18.0及以上運行環境。
由於使用死信交換器相對曲折,本文重點介紹第二種方式,使用rabbitmq-delayed-message-exchange插件完成延遲隊列的功能。
AMQP協議和RabbitMQ隊列本身沒有直接支持延遲隊列功能,但是我們可以通過RabbitMQ的兩個特性來曲線實現延遲隊列:
特性一:Time To Live(TTL)
RabbitMQ可以針對Queue設置x-expires 或者 針對Message設置 x-message-ttl,來控制消息的生存時間,如果超時(兩者同時設置以最先到期的時間爲準),則消息變爲dead letter(死信)
RabbitMQ針對隊列中的消息過期時間有兩種方法可以設置。
A: 通過隊列屬性設置,隊列中所有消息都有相同的過期時間。
B: 對消息進行單獨設置,每條消息TTL可以不同。
如果同時使用,則消息的過期時間以兩者之間TTL較小的那個數值爲準。消息在隊列的生存時間一旦超過設置的TTL值,就成爲dead letter
特性二:Dead Letter Exchanges(DLX)
RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可選)兩個參數,如果隊列內出現了dead letter,則按照這兩個參數重新路由轉發到指定的隊列。
x-dead-letter-exchange:出現dead letter之後將dead letter重新發送到指定exchange
x-dead-letter-routing-key:出現dead letter之後將dead letter重新按照指定的routing-key發送
隊列出現dead letter的情況有:
消息或者隊列的TTL過期
隊列達到最大長度
消息被消費端拒絕(basic.reject or basic.nack)並且requeue=false
二、安裝延遲插件
1.1 下載插件
SpringBoot整合RabbitMQ
創建一個springBoot項目
在 pom.xml 中添加 spring-boot-starter-amqp的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
在 application.yml文件中配置rabbitmq相關內容
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
listener:
direct:
acknowledge-mode: manual
simple:
acknowledge-mode: manual
具體編碼實現
1.配置隊列
package com.hmg.rabbitmq.config;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @title rabbitmq配置類
* @auther 吊炸天
* @date 2019/12/18 21:28
*/
@Configuration
@Log4j2
public class DelayRabbitConfig {
/**
* 延遲隊列 TTL 名稱
*/
private static final String ORDER_DELAY_QUEUE = "user.order.delay.queue";
/**
* DLX,dead letter發送到的 exchange
* 延時消息就是發送到該交換機的
*/
public static final String ORDER_DELAY_EXCHANGE = "user.order.delay.exchange";
/**
* routing key 名稱
* 具體消息發送在該 routingKey 的
*/
public static final String ORDER_DELAY_ROUTING_KEY = "order_delay";
public static final String ORDER_QUEUE_NAME = "user.order.queue";
public static final String ORDER_EXCHANGE_NAME = "user.order.exchange";
public static final String ORDER_ROUTING_KEY = "order";
/**
* 延遲隊列配置
* <p>
* 1、params.put("x-message-ttl", 5 * 1000);
* 第一種方式是直接設置 Queue 延遲時間 但如果直接給隊列設置過期時間,這種做法不是很靈活,(當然二者是兼容的,默認是時間小的優先)
* 2、rabbitTemplate.convertAndSend(book, message -> {
* message.getMessageProperties().setExpiration(2 * 1000 + "");
* return message;
* });
* 第二種就是每次發送消息動態設置延遲時間,這樣我們可以靈活控制
**/
@Bean
public Queue delayOrderQueue() {
Map<String, Object> params = new HashMap<>();
// x-dead-letter-exchange 聲明瞭隊列裏的死信轉發到的DLX名稱,
params.put("x-dead-letter-exchange", ORDER_EXCHANGE_NAME);
// x-dead-letter-routing-key 聲明瞭這些死信在轉發時攜帶的 routing-key 名稱。
params.put("x-dead-letter-routing-key", ORDER_ROUTING_KEY);
return new Queue(ORDER_DELAY_QUEUE, true, false, false, params);
}
/**
* 需要將一個隊列綁定到交換機上,要求該消息與一個特定的路由鍵完全匹配。
* 這是一個完整的匹配。如果一個隊列綁定到該交換機上要求路由鍵 “dog”,則只有被標記爲“dog”的消息才被轉發,
* 不會轉發dog.puppy,也不會轉發dog.guard,只會轉發dog。
*
* @return DirectExchange
*/
@Bean
public DirectExchange orderDelayExchange() {
return new DirectExchange(ORDER_DELAY_EXCHANGE);
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(delayOrderQueue()).to(orderDelayExchange()).with(ORDER_DELAY_ROUTING_KEY);
}
@Bean
public Queue orderQueue() {
return new Queue(ORDER_QUEUE_NAME, true);
}
/**
* 將路由鍵和某模式進行匹配。此時隊列需要綁定要一個模式上。
* 符號“#”匹配一個或多個詞,符號“*”匹配不多不少一個詞。因此“audit.#”能夠匹配到“audit.irs.corporate”,但是“audit.*” 只會匹配到“audit.irs”。
**/
@Bean
public TopicExchange orderTopicExchange() {
return new TopicExchange(ORDER_EXCHANGE_NAME);
}
@Bean
public Binding orderBinding() {
// TODO 如果要讓延遲隊列之間有關聯,這裏的 routingKey 和 綁定的交換機很關鍵
return BindingBuilder.bind(orderQueue()).to(orderTopicExchange()).with(ORDER_ROUTING_KEY);
}
}
2.創建一個Order實體類
package com.hmg.rabbitmq.entity;
import lombok.Data;
import java.io.Serializable;
/**
* @auther 吊炸天
* @date 2019/12/18 21:43
*/
@Data
public class Order implements Serializable {
private static final long serialVersionUID = -2221214252163879885L;
/**
* 訂單id
*/
private String orderId;
/**
* 訂單狀態 0:未支付,1:已支付,2:訂單已取消
*/
private Integer orderStatus;
/**
* 訂單名字
*/
private String orderName;
}
3.接收者
package com.hmg.rabbitmq.config;
import com.hmg.rabbitmq.entity.Order;
import com.rabbitmq.client.Channel;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @auther 吊炸天
* @date 2019/12/18 21:46
*/
@Component
@Log4j2
public class DelayReceiver {
@RabbitListener(queues = {DelayRabbitConfig.ORDER_QUEUE_NAME})
public void orderDelayQueue(Order order, Message message, Channel channel) {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
log.info("【orderDelayQueue 監聽的消息】 - 【消費時間】 - [{}]- 【訂單內容】 - [{}]", new Date(), order.toString());
if(order.getOrderStatus() == 0) {
order.setOrderStatus(2);
log.info("【該訂單未支付,取消訂單】" + order.toString());
} else if(order.getOrderStatus() == 1) {
log.info("【該訂單已完成支付】");
} else if(order.getOrderStatus() == 2) {
log.info("【該訂單已取消】");
}
log.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
}
}
4.發送者
package com.hmg.rabbitmq.config;
import com.hmg.rabbitmq.entity.Order;
import lombok.extern.log4j.Log4j2;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @auther 吊炸天
* @date 2019/12/18 21:48
*/
@Component
@Log4j2
public class DelaySender {
@Autowired
private AmqpTemplate amqpTemplate;
public void sendDelay(Order order) {
log.info("【訂單生成時間】" + new Date().toString() + "【1分鐘後檢查訂單是否已經支付】" + order.toString());
this.amqpTemplate.convertAndSend(DelayRabbitConfig.ORDER_DELAY_EXCHANGE,
DelayRabbitConfig.ORDER_DELAY_ROUTING_KEY, order, message -> {
/**
* 如果配置了 params.put("x-message-ttl", 5 * 1000);
* 那麼這一句也可以省略,
* 具體根據業務需要是聲明 Queue 的時候就指定好延遲時間還是在發送自己控制時間
*/
message.getMessageProperties().setExpiration(1 * 1000 * 60 + "");
return message;
});
}
}
5.測試,訪問http://localhost:8080/sendDelay,查看日誌輸出
package com.hmg.rabbitmq.controller;
import com.hmg.rabbitmq.config.DelaySender;
import com.hmg.rabbitmq.entity.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @auther 吊炸天
* @date 2019/12/18 21:56
*/
@RestController
public class TestController {
@Autowired
private DelaySender delaySender;
@GetMapping("/sendDelay")
public Object sendDelay() {
Order order1 = new Order();
order1.setOrderStatus(0);
order1.setOrderId("13147747");
order1.setOrderName("魅族16plus");
Order order2 = new Order();
order2.setOrderStatus(1);
order2.setOrderId("68363685");
order2.setOrderName("魅族16s");
delaySender.sendDelay(order1);
delaySender.sendDelay(order2);
return "ok";
}
}
6.測試效果