【RabbitMQ】一文帶你搞定RabbitMQ延遲佇列

弗蘭克的貓發表於2019-07-28

本文口味:魚香肉絲   預計閱讀:10分鐘

一、說明

在上一篇中,介紹了RabbitMQ中的死信佇列是什麼,何時使用以及如何使用RabbitMQ的死信佇列。相信通過上一篇的學習,對於死信佇列已經有了更多的瞭解,這一篇的內容也跟死信佇列息息相關,如果你還不瞭解死信佇列,那麼建議你先進行上一篇文章的閱讀。

這一篇裡,我們將繼續介紹RabbitMQ的高階特性,通過本篇的學習,你將收穫:

  1. 什麼是延時佇列
  2. 延時佇列使用場景
  3. RabbitMQ中的TTL
  4. 如何利用RabbitMQ來實現延時佇列

二、本文大綱

以下是本文大綱:

1.png

本文閱讀前,需要對RabbitMQ以及死信佇列有一個簡單的瞭解。

三、什麼是延時佇列

延時佇列,首先,它是一種佇列,佇列意味著內部的元素是有序的,元素出隊和入隊是有方向性的,元素從一端進入,從另一端取出。

其次,延時佇列,最重要的特性就體現在它的延時屬性上,跟普通的佇列不一樣的是,普通佇列中的元素總是等著希望被早點取出處理,而延時佇列中的元素則是希望被在指定時間得到取出和處理,所以延時佇列中的元素是都是帶時間屬性的,通常來說是需要被處理的訊息或者任務。

簡單來說,延時佇列就是用來存放需要在指定時間被處理的元素的佇列。

四、延時佇列使用場景

那麼什麼時候需要用延時佇列呢?考慮一下以下場景:

  1. 訂單在十分鐘之內未支付則自動取消。
  2. 新建立的店鋪,如果在十天內都沒有上傳過商品,則自動傳送訊息提醒。
  3. 賬單在一週內未支付,則自動結算。
  4. 使用者註冊成功後,如果三天內沒有登陸則進行簡訊提醒。
  5. 使用者發起退款,如果三天內沒有得到處理則通知相關運營人員。
  6. 預定會議後,需要在預定的時間點前十分鐘通知各個與會人員參加會議。

這些場景都有一個特點,需要在某個事件發生之後或者之前的指定時間點完成某一項任務,如:發生訂單生成事件,在十分鐘之後檢查該訂單支付狀態,然後將未支付的訂單進行關閉;發生店鋪建立事件,十天後檢查該店鋪上新商品數,然後通知上新數為0的商戶;發生賬單生成事件,檢查賬單支付狀態,然後自動結算未支付的賬單;發生新使用者註冊事件,三天後檢查新註冊使用者的活動資料,然後通知沒有任何活動記錄的使用者;發生退款事件,在三天之後檢查該訂單是否已被處理,如仍未被處理,則傳送訊息給相關運營人員;發生預定會議事件,判斷離會議開始是否只有十分鐘了,如果是,則通知各個與會人員。

看起來似乎使用定時任務,一直輪詢資料,每秒查一次,取出需要被處理的資料,然後處理不就完事了嗎?如果資料量比較少,確實可以這樣做,比如:對於“如果賬單一週內未支付則進行自動結算”這樣的需求,如果對於時間不是嚴格限制,而是寬鬆意義上的一週,那麼每天晚上跑個定時任務檢查一下所有未支付的賬單,確實也是一個可行的方案。但對於資料量比較大,並且時效性較強的場景,如:“訂單十分鐘內未支付則關閉“,短期內未支付的訂單資料可能會有很多,活動期間甚至會達到百萬甚至千萬級別,對這麼龐大的資料量仍舊使用輪詢的方式顯然是不可取的,很可能在一秒內無法完成所有訂單的檢查,同時會給資料庫帶來很大壓力,無法滿足業務要求而且效能低下。

更重要的一點是,不!優!雅!

沒錯,作為一名有追求的程式設計師,始終應該追求更優雅的架構和更優雅的程式碼風格,寫程式碼要像寫詩一樣優美。【滑稽】

這時候,延時佇列就可以閃亮登場了,以上場景,正是延時佇列的用武之地。

既然延時佇列可以解決很多特定場景下,帶時間屬性的任務需求,那麼如何構造一個延時佇列呢?接下來,本文將介紹如何用RabbitMQ來實現延時佇列。

五、RabbitMQ中的TTL

在介紹延時佇列之前,還需要先介紹一下RabbitMQ中的一個高階特性——TTL(Time To Live)

TTL是什麼呢?TTL是RabbitMQ中一個訊息或者佇列的屬性,表明一條訊息或者該佇列中的所有訊息的最大存活時間,單位是毫秒。換句話說,如果一條訊息設定了TTL屬性或者進入了設定TTL屬性的佇列,那麼這條訊息如果在TTL設定的時間內沒有被消費,則會成為“死信”(至於什麼是死信,請翻看上一篇)。如果同時配置了佇列的TTL和訊息的TTL,那麼較小的那個值將會被使用。

那麼,如何設定這個TTL值呢?有兩種方式,第一種是在建立佇列的時候設定佇列的“x-message-ttl”屬性,如下:

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 6000);
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);

這樣所有被投遞到該佇列的訊息都最多不會存活超過6s。

另一種方式便是針對每條訊息設定TTL,程式碼如下:

AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.expiration("6000");
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(exchangeName, routingKey, mandatory, properties, "msg body".getBytes());

這樣這條訊息的過期時間也被設定成了6s。

但這兩種方式是有區別的,如果設定了佇列的TTL屬性,那麼一旦訊息過期,就會被佇列丟棄,而第二種方式,訊息即使過期,也不一定會被馬上丟棄,因為訊息是否過期是在即將投遞到消費者之前判定的,如果當前佇列有嚴重的訊息積壓情況,則已過期的訊息也許還能存活較長時間。

另外,還需要注意的一點是,如果不設定TTL,表示訊息永遠不會過期,如果將TTL設定為0,則表示除非此時可以直接投遞該訊息到消費者,否則該訊息將會被丟棄。

六、如何利用RabbitMQ實現延時佇列

前一篇裡介紹瞭如果設定死信佇列,前文中又介紹了TTL,至此,利用RabbitMQ實現延時佇列的兩大要素已經集齊,接下來只需要將它們進行調和,再加入一點點調味料,延時佇列就可以新鮮出爐了。

想想看,延時佇列,不就是想要訊息延遲多久被處理嗎,TTL則剛好能讓訊息在延遲多久之後成為死信,另一方面,成為死信的訊息都會被投遞到死信佇列裡,這樣只需要消費者一直消費死信佇列裡的訊息就萬事大吉了,因為裡面的訊息都是希望被立即處理的訊息。

從下圖可以大致看出訊息的流向:

23.png

生產者生產一條延時訊息,根據需要延時時間的不同,利用不同的routingkey將訊息路由到不同的延時佇列,每個佇列都設定了不同的TTL屬性,並繫結在同一個死信交換機中,訊息過期後,根據routingkey的不同,又會被路由到不同的死信佇列中,消費者只需要監聽對應的死信佇列進行處理即可。

下面來看程式碼:

先宣告交換機、佇列以及他們的繫結關係:

@Configuration
public class RabbitMQConfig {

    public static final String DELAY_EXCHANGE_NAME = "delay.queue.demo.business.exchange";
    public static final String DELAY_QUEUEA_NAME = "delay.queue.demo.business.queuea";
    public static final String DELAY_QUEUEA_ROUTING_KEY = "delay.queue.demo.business.queuea.routingkey";
    public static final String DELAY_QUEUEB_ROUTING_KEY = "delay.queue.demo.business.queueb.routingkey";
    public static final String DEAD_LETTER_EXCHANGE = "delay.queue.demo.deadletter.exchange";
    public static final String DEAD_LETTER_QUEUEA_ROUTING_KEY = "delay.queue.demo.deadletter.delay_10s.routingkey";
    public static final String DEAD_LETTER_QUEUEB_ROUTING_KEY = "delay.queue.demo.deadletter.delay_60s.routingkey";
    public static final String DEAD_LETTER_QUEUEA_NAME = "delay.queue.demo.deadletter.queuea";
    public static final String DEAD_LETTER_QUEUEB_NAME = "delay.queue.demo.deadletter.queueb";

    // 宣告延時Exchange
    @Bean("delayExchange")
    public DirectExchange delayExchange(){
        return new DirectExchange(DELAY_EXCHANGE_NAME);
    }

    // 宣告死信Exchange
    @Bean("deadLetterExchange")
    public DirectExchange deadLetterExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }

    // 宣告延時佇列A 延時10s
    // 並繫結到對應的死信交換機
    @Bean("delayQueueA")
    public Queue delayQueueA(){
        Map<String, Object> args = new HashMap<>(2);
        // x-dead-letter-exchange    這裡宣告當前佇列繫結的死信交換機
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        // x-dead-letter-routing-key  這裡宣告當前佇列的死信路由key
        args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
        // x-message-ttl  宣告佇列的TTL
        args.put("x-message-ttl", 6000);
        return QueueBuilder.durable(DEAD_LETTER_QUEUEA_NAME).withArguments(args).build();
    }

    // 宣告延時佇列B 延時 60s
    // 並繫結到對應的死信交換機
    @Bean("delayQueueB")
    public Queue delayQueueB(){
        Map<String, Object> args = new HashMap<>(2);
        // x-dead-letter-exchange    這裡宣告當前佇列繫結的死信交換機
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        // x-dead-letter-routing-key  這裡宣告當前佇列的死信路由key
        args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEB_ROUTING_KEY);
        // x-message-ttl  宣告佇列的TTL
        args.put("x-message-ttl", 60000);
        return QueueBuilder.durable(DEAD_LETTER_QUEUEB_NAME).withArguments(args).build();
    }

    // 宣告死信佇列A 用於接收延時10s處理的訊息
    @Bean("deadLetterQueueA")
    public Queue deadLetterQueueA(){
        return new Queue(DEAD_LETTER_QUEUEA_NAME);
    }

    // 宣告死信佇列B 用於接收延時60s處理的訊息
    @Bean("deadLetterQueueB")
    public Queue deadLetterQueueB(){
        return new Queue(DEAD_LETTER_QUEUEB_NAME);
    }

    // 宣告延時佇列A繫結關係
    @Bean
    public Binding delayBindingA(@Qualifier("delayQueueA") Queue queue,
                                    @Qualifier("delayExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUEA_ROUTING_KEY);
    }

    // 宣告業務佇列B繫結關係
    @Bean
    public Binding delayBindingB(@Qualifier("delayQueueB") Queue queue,
                                    @Qualifier("delayExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUEB_ROUTING_KEY);
    }

    // 宣告死信佇列A繫結關係
    @Bean
    public Binding deadLetterBindingA(@Qualifier("deadLetterQueueA") Queue queue,
                                    @Qualifier("deadLetterExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEA_ROUTING_KEY);
    }

    // 宣告死信佇列B繫結關係
    @Bean
    public Binding deadLetterBindingB(@Qualifier("deadLetterQueueB") Queue queue,
                                      @Qualifier("deadLetterExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEB_ROUTING_KEY);
    }
}

接下來,建立兩個消費者,分別對兩個死信佇列的訊息進行消費:

@Slf4j
@Component
public class DeadLetterQueueConsumer {

    @RabbitListener(queues = DEAD_LETTER_QUEUEA_NAME)
    public void receiveA(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        log.info("當前時間:{},死信佇列A收到訊息:{}", new Date().toString(), msg);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

    @RabbitListener(queues = DEAD_LETTER_QUEUEB_NAME)
    public void receiveB(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        log.info("當前時間:{},死信佇列B收到訊息:{}", new Date().toString(), msg);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

然後是訊息的生產者:

@Component
public class DelayMessageSender {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMsg(String msg, DelayTypeEnum type){
        switch (type){
            case DELAY_10s:
                rabbitTemplate.convertAndSend(DELAY_EXCHANGE_NAME, DELAY_QUEUEA_ROUTING_KEY, msg);
                break;
            case DELAY_60s:
                rabbitTemplate.convertAndSend(DELAY_EXCHANGE_NAME, DELAY_QUEUEB_ROUTING_KEY, msg);
                break;
        }
    }
}

接下來,我們暴露一個web介面來生產訊息:

@Slf4j
@RequestMapping("rabbitmq")
@RestController
public class RabbitMQMsgController {

    @Autowired
    private DelayMessageSender sender;

    @RequestMapping("sendmsg")
    public void sendMsg(String msg, Integer delayType){
        log.info("當前時間:{},收到請求,msg:{},delayType:{}", new Date(), msg, delayType);
        sender.sendMsg(msg, Objects.requireNonNull(DelayTypeEnum.getDelayTypeEnumByValue(delayType)));
    }
}

準備就緒,啟動!

開啟rabbitMQ的管理後臺,可以看到我們剛才建立的交換機和佇列資訊:

2.png

4.png

3.png

接下來,我們來傳送幾條訊息,http://localhost:8080/rabbitmq/sendmsg?msg=testMsg1&delayType=1 http://localhost:8080/rabbitmq/sendmsg?msg=testMsg2&delayType=2

日誌如下:

2019-07-28 16:02:19.813  INFO 3860 --- [nio-8080-exec-9] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 16:02:19 CST 2019,收到請求,msg:testMsg1,delayType:1
2019-07-28 16:02:19.815  INFO 3860 --- [nio-8080-exec-9] .l.DirectReplyToMessageListenerContainer : SimpleConsumer [queue=amq.rabbitmq.reply-to, consumerTag=amq.ctag-o-qPpkWIkRm73DIrOIVhig identity=766339] started
2019-07-28 16:02:25.829  INFO 3860 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 16:02:25 CST 2019,死信佇列A收到訊息:testMsg1
2019-07-28 16:02:41.326  INFO 3860 --- [nio-8080-exec-1] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 16:02:41 CST 2019,收到請求,msg:testMsg2,delayType:2
2019-07-28 16:03:41.329  INFO 3860 --- [ntContainer#0-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 16:03:41 CST 2019,死信佇列B收到訊息:testMsg2

第一條訊息在6s後變成了死信訊息,然後被消費者消費掉,第二條訊息在60s之後變成了死信訊息,然後被消費掉,這樣,一個還算ok的延時佇列就打造完成了。

不過,等等,如果這樣使用的話,豈不是每增加一個新的時間需求,就要新增一個佇列,這裡只有6s和60s兩個時間選項,如果需要一個小時後處理,那麼就需要增加TTL為一個小時的佇列,如果是預定會議室然後提前通知這樣的場景,豈不是要增加無數個佇列才能滿足需求??

嗯,仔細想想,事情並不簡單。

七、RabbitMQ延時佇列優化

顯然,需要一種更通用的方案才能滿足需求,那麼就只能將TTL設定在訊息屬性裡了。我們來試一試。

增加一個延時佇列,用於接收設定為任意延時時長的訊息,增加一個相應的死信佇列和routingkey:

@Configuration
public class RabbitMQConfig {

    public static final String DELAY_EXCHANGE_NAME = "delay.queue.demo.business.exchange";
    public static final String DELAY_QUEUEC_NAME = "delay.queue.demo.business.queuec";
    public static final String DELAY_QUEUEC_ROUTING_KEY = "delay.queue.demo.business.queuec.routingkey";
    public static final String DEAD_LETTER_EXCHANGE = "delay.queue.demo.deadletter.exchange";
    public static final String DEAD_LETTER_QUEUEC_ROUTING_KEY = "delay.queue.demo.deadletter.delay_anytime.routingkey";
    public static final String DEAD_LETTER_QUEUEC_NAME = "delay.queue.demo.deadletter.queuec";

    // 宣告延時Exchange
    @Bean("delayExchange")
    public DirectExchange delayExchange(){
        return new DirectExchange(DELAY_EXCHANGE_NAME);
    }

    // 宣告死信Exchange
    @Bean("deadLetterExchange")
    public DirectExchange deadLetterExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }

    // 宣告延時佇列C 不設定TTL
    // 並繫結到對應的死信交換機
    @Bean("delayQueueC")
    public Queue delayQueueC(){
        Map<String, Object> args = new HashMap<>(3);
        // x-dead-letter-exchange    這裡宣告當前佇列繫結的死信交換機
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        // x-dead-letter-routing-key  這裡宣告當前佇列的死信路由key
        args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEC_ROUTING_KEY);
        return QueueBuilder.durable(DELAY_QUEUEC_NAME).withArguments(args).build();
    }

    // 宣告死信佇列C 用於接收延時任意時長處理的訊息
    @Bean("deadLetterQueueC")
    public Queue deadLetterQueueC(){
        return new Queue(DEAD_LETTER_QUEUEC_NAME);
    }

    // 宣告延時列C繫結關係
    @Bean
    public Binding delayBindingC(@Qualifier("delayQueueC") Queue queue,
                                 @Qualifier("delayExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DELAY_QUEUEC_ROUTING_KEY);
    }

    // 宣告死信佇列C繫結關係
    @Bean
    public Binding deadLetterBindingC(@Qualifier("deadLetterQueueC") Queue queue,
                                      @Qualifier("deadLetterExchange") DirectExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_LETTER_QUEUEC_ROUTING_KEY);
    }
}

增加一個死信佇列C的消費者:

@RabbitListener(queues = DEAD_LETTER_QUEUEC_NAME)
public void receiveC(Message message, Channel channel) throws IOException {
    String msg = new String(message.getBody());
    log.info("當前時間:{},死信佇列C收到訊息:{}", new Date().toString(), msg);
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

再次啟動!然後訪問:http://localhost:8080/rabbitmq/delayMsg?msg=testMsg1delayTime=5000 來生產訊息,注意這裡的單位是毫秒。

2019-07-28 16:45:07.033  INFO 31468 --- [nio-8080-exec-4] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 16:45:07 CST 2019,收到請求,msg:testMsg1,delayTime:5000
2019-07-28 16:45:11.694  INFO 31468 --- [nio-8080-exec-5] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 16:45:11 CST 2019,收到請求,msg:testMsg2,delayTime:5000
2019-07-28 16:45:12.048  INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 16:45:12 CST 2019,死信佇列C收到訊息:testMsg1
2019-07-28 16:45:16.709  INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 16:45:16 CST 2019,死信佇列C收到訊息:testMsg2

看起來似乎沒什麼問題,但不要高興的太早,在最開始的時候,就介紹過,如果使用在訊息屬性上設定TTL的方式,訊息可能並不會按時“死亡“,因為RabbitMQ只會檢查第一個訊息是否過期,如果過期則丟到死信佇列,索引如果第一個訊息的延時時長很長,而第二個訊息的延時時長很短,則第二個訊息並不會優先得到執行。

實驗一下:

2019-07-28 16:49:02.957  INFO 31468 --- [nio-8080-exec-8] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 16:49:02 CST 2019,收到請求,msg:longDelayedMsg,delayTime:20000
2019-07-28 16:49:10.671  INFO 31468 --- [nio-8080-exec-9] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 16:49:10 CST 2019,收到請求,msg:shortDelayedMsg,delayTime:2000
2019-07-28 16:49:22.969  INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 16:49:22 CST 2019,死信佇列C收到訊息:longDelayedMsg
2019-07-28 16:49:22.970  INFO 31468 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 16:49:22 CST 2019,死信佇列C收到訊息:shortDelayedMsg

我們先發了一個延時時長為20s的訊息,然後發了一個延時時長為2s的訊息,結果顯示,第二個訊息會在等第一個訊息成為死信後才會“死亡“。

八、利用RabbitMQ外掛實現延遲佇列

上文中提到的問題,確實是一個硬傷,如果不能實現在訊息粒度上新增TTL,並使其在設定的TTL時間及時死亡,就無法設計成一個通用的延時佇列。

那如何解決這個問題呢?不要慌,安裝一個外掛即可:https://www.rabbitmq.com/community-plugins.html ,下載rabbitmq_delayed_message_exchange外掛,然後解壓放置到RabbitMQ的外掛目錄。

接下來,進入RabbitMQ的安裝目錄下的sbin目錄,執行下面命令讓該外掛生效,然後重啟RabbitMQ。

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

然後,我們再宣告幾個Bean:

@Configuration
public class DelayedRabbitMQConfig {
    public static final String DELAYED_QUEUE_NAME = "delay.queue.demo.delay.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delay.queue.demo.delay.exchange";
    public static final String DELAYED_ROUTING_KEY = "delay.queue.demo.delay.routingkey";

    @Bean
    public Queue immediateQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

    @Bean
    public CustomExchange customExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
    }

    @Bean
    public Binding bindingNotify(@Qualifier("immediateQueue") Queue queue,
                                 @Qualifier("customExchange") CustomExchange customExchange) {
        return BindingBuilder.bind(queue).to(customExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

controller層再新增一個入口:

@RequestMapping("delayMsg2")
public void delayMsg2(String msg, Integer delayTime) {
    log.info("當前時間:{},收到請求,msg:{},delayTime:{}", new Date(), msg, delayTime);
    sender.sendDelayMsg(msg, delayTime);
}

訊息生產者的程式碼也需要修改:

public void sendDelayMsg(String msg, Integer delayTime) {
    rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, msg, a ->{
        a.getMessageProperties().setDelay(delayTime);
        return a;
    });
}

最後,再建立一個消費者:

@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
    String msg = new String(message.getBody());
    log.info("當前時間:{},延時佇列收到訊息:{}", new Date().toString(), msg);
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

一切準備就緒,啟動!然後分別訪問以下連結:

http://localhost:8080/rabbitmq/delayMsg2?msg=msg1&delayTime=20000
http://localhost:8080/rabbitmq/delayMsg2?msg=msg2&delayTime=2000

日誌如下:

2019-07-28 17:28:13.729  INFO 25804 --- [nio-8080-exec-2] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 17:28:13 CST 2019,收到請求,msg:msg1,delayTime:20000
2019-07-28 17:28:20.607  INFO 25804 --- [nio-8080-exec-1] c.m.d.controller.RabbitMQMsgController   : 當前時間:Sun Jul 28 17:28:20 CST 2019,收到請求,msg:msg2,delayTime:2000
2019-07-28 17:28:22.624  INFO 25804 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 17:28:22 CST 2019,延時佇列收到訊息:msg2
2019-07-28 17:28:33.751  INFO 25804 --- [ntContainer#1-1] c.m.d.mq.DeadLetterQueueConsumer         : 當前時間:Sun Jul 28 17:28:33 CST 2019,延時佇列收到訊息:msg1

第二個訊息被先消費掉了,符合預期。至此,RabbitMQ實現延時佇列的部分就完結了。

九、總結

延時佇列在需要延時處理的場景下非常有用,使用RabbitMQ來實現延時佇列可以很好的利用RabbitMQ的特性,如:訊息可靠傳送、訊息可靠投遞、死信佇列來保障訊息至少被消費一次以及未被正確處理的訊息不會被丟棄。另外,通過RabbitMQ叢集的特性,可以很好的解決單點故障問題,不會因為單個節點掛掉導致延時佇列不可用或者訊息丟失。

當然,延時佇列還有很多其它選擇,比如利用Java的DelayQueu,利用Redis的zset,利用Quartz或者利用kafka的時間輪,這些方式各有特點,但就像爐石傳說一般,這些知識就好比手裡的卡牌,知道的越多,可以用的卡牌也就越多,遇到問題便能遊刃有餘,所以需要大量的知識儲備和經驗積累才能打造出更出色的卡牌組合,讓自己解決問題的能力得到更好的提升。

但另一方面,隨著時間的流逝和閱歷的增長,越來越感覺到自己的能力有限,無法獨自面對紛繁複雜且多變的業務需求,在很多方面需要其他人的協助才能很好的完成任務。也知道聞道有先後,術業有專攻,不會再狂妄自大,覺得自己能把所有事情都搞定,也將重心慢慢轉移到研究如何有效的進行團隊合作上來,我相信一個高度協調的團隊永遠比一個人戰鬥要更有價值。

花了一個週末的時間完成了這篇文章,文中所有的程式碼都上傳到了github,https://github.com/MFrank2016/delayed-queue-demo如有需要可以自行查閱,希望能對你有幫助,如果有錯誤的地方,歡迎指正,也歡迎關注我的公眾號進行留言交流。

TIM圖片20190714173105.png

相關文章