天天看點

RabbitMQ 延遲隊列,太實用了!

應用場景

目前常見的應用軟體都有消息的延遲推送的影子,應用也極為廣泛,例如:

淘寶七天自動确認收貨。在我們簽收商品後,物流系統會在七天後延時發送一個消息給支付系統,通知支付系統将款打給商家,這個過程持續七天,就是使用了消息中間件的延遲推送功能。

12306 購票支付确認頁面。我們在選好票點選确定跳轉的頁面中往往都會有倒計時,代表着 30 分鐘内訂單不确認的話将會自動取消訂單。其實在下訂單那一刻開始購票業務系統就會發送一個延時消息給訂單系統,延時30分鐘,告訴訂單系統訂單未完成,如果我們在30分鐘内完成了訂單,則可以通過邏輯代碼判斷來忽略掉收到的消息。

在上面兩種場景中,如果我們使用下面兩種傳統解決方案無疑大大降低了系統的整體性能和吞吐量:

使用 redis 給訂單設定過期時間,最後通過判斷 redis 中是否還有該訂單來決定訂單是否已經完成。這種解決方案相較于消息的延遲推送性能較低,因為我們知道 redis 都是存儲于記憶體中,我們遇到惡意下單或者刷單的将會給記憶體帶來巨大壓力。

使用傳統的資料庫輪詢來判斷資料庫表中訂單的狀态,這無疑增加了IO次數,性能極低。

使用 jvm 原生的 DelayQueue ,也是大量占用記憶體,而且沒有持久化政策,系統當機或者重新開機都會丢失訂單資訊。

消息延遲推送的實作

在 RabbitMQ 3.6.x 之前我們一般采用死信隊列+TTL過期時間來實作延遲隊列,我們這裡不做過多介紹,可以參考之前文章來了解:TTL、死信隊列。

在 RabbitMQ 3.6.x 開始,RabbitMQ 官方提供了延遲隊列的插件,可以下載下傳放置到 RabbitMQ 根目錄下的 plugins 下。

RabbitMQ 延遲隊列,太實用了!
首先我們建立交換機和消息隊列,application.properties 中配置與上一篇文章相同。

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;

@Configuration
public class MQConfig {

    public static final String LAZY_EXCHANGE = "Ex.LazyExchange";
    public static final String LAZY_QUEUE = "MQ.LazyQueue";
    public static final String LAZY_KEY = "lazy.#";

    @Bean
    public TopicExchange lazyExchange(){
        //Map<String, Object> pros = new HashMap<>();
        //設定交換機支援延遲消息推送
        //pros.put("x-delayed-message", "topic");
        TopicExchange exchange = new TopicExchange(LAZY_EXCHANGE, true, false, pros);
        exchange.setDelayed(true);
        return exchange;
    }

    @Bean
    public Queue lazyQueue(){
        return new Queue(LAZY_QUEUE, true);
    }

    @Bean
    public Binding lazyBinding(){
        return BindingBuilder.bind(lazyQueue()).to(lazyExchange()).with(LAZY_KEY);
    }
}      

我們在 Exchange 的聲明中可以設定

exchange.setDelayed(true)

來開啟延遲隊列,也可以設定為以下内容傳入交換機聲明的方法中,因為第一種方式的底層就是通過這種方式來實作的。

//Map<String, Object> pros = new HashMap<>();
//設定交換機支援延遲消息推送
//pros.put("x-delayed-message", "topic");
TopicExchange exchange = new TopicExchange(LAZY_EXCHANGE, true, false, pros);      

發送消息時我們需要指定延遲推送的時間,我們這裡在發送消息的方法中傳入參數 

new MessagePostProcessor()

 是為了獲得 

Message

對象,因為需要借助 

Message

對象的api 來設定延遲時間。

import com.anqi.mq.config.MQConfig;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class MQSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //confirmCallback returnCallback 代碼省略,請參照上一篇

    public void sendLazy(Object message){
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback(confirmCallback);
        rabbitTemplate.setReturnCallback(returnCallback);
        //id + 時間戳 全局唯一
        CorrelationData correlationData = new CorrelationData("12345678909"+new Date());

        //發送消息時指定 header 延遲時間
        rabbitTemplate.convertAndSend(MQConfig.LAZY_EXCHANGE, "lazy.boot", message,
                new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //設定消息持久化
                message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                //message.getMessageProperties().setHeader("x-delay", "6000");
                message.getMessageProperties().setDelay(6000);
                return message;
            }
        }, correlationData);
    }
}      

我們可以觀察 

setDelay(Integer i)

底層代碼,也是在 header 中設定 x-delay。等同于我們手動設定 header

message.getMessageProperties().setHeader("x-delay", "6000");
/**
 * Set the x-delay header.
 * @param delay the delay.
 * @since 1.6
 */
public void setDelay(Integer delay) {
    if (delay == null || delay < 0) {
        this.headers.remove(X_DELAY);
    }
    else {
        this.headers.put(X_DELAY, delay);
    }
}      

消費端進行消費

import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

@Component
public class MQReceiver {

    @RabbitListener(queues = "MQ.LazyQueue")
    @RabbitHandler
    public void onLazyMessage(Message msg, Channel channel) throws IOException{
        long deliveryTag = msg.getMessageProperties().getDeliveryTag();
        channel.basicAck(deliveryTag, true);
        System.out.println("lazy receive " + new String(msg.getBody()));

    }      

測試結果

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
public class MQSenderTest {

    @Autowired
    private MQSender mqSender;

    @Test
    public void sendLazy() throws  Exception {
        String msg = "hello spring boot";

        mqSender.sendLazy(msg + ":");
    }
}