springboot整合rabbitmq發送延時消息


gitee demo地址:

一、Rabbit MQ 實現延時消息

1.死信隊列

通過死信隊列來間接實現延時消息,如果延時時間,不是一個固定的時間,則不建議使用這種方式。

(1) 具體實現

  1. 引入依賴
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>
  1. 聲明相關 exchange route-key queue
package com.lfg.message.delay.message.demo.delay.plugin;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @author lfg
 * @version v1.0
 */
@Configuration
public class RabbitMqConfig {
    /**
     * 流程:
     * 1.消費者:重發隊列跟 重發交換機 重發路由綁定
     * 2.死信交換機 死信路由不綁定消費者
     * 3.死信消息找不到消費者,有效時間到,自動轉發到重發交換機
     * 4.重發隊列消費成功
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 死信交換機
     */
    private String dlExchange = "dead-letter-exchange-name";
    /**
     * 死信 路由key
     */
    private String dlRoutingKey = "dead-letter-routing-key-name";
    /**
     * 死信隊列
     */
    private String dlQueue = "dead-letter-queue-name";
    /**
     * 重發交換機
     */
    private String reExchange = "exchange-name";
    /**
     * 重發路由key
     */
    private String reRoutingKey = "routing-key-name";
    /**
     * 重發隊列
     */
    private String reQueue = "queue-name";


    /**
     * 聲明死信交換機
     */
    @Bean("dlExchange")
    public Exchange dlExchange() {
        return ExchangeBuilder.directExchange(dlExchange).durable(true).build();
    }

    /**
     * 聲明重發交換機
     */
    @Bean("reExchange")
    public Exchange reExchange() {
        return ExchangeBuilder.directExchange(reExchange).durable(true).build();
    }

    /**
     * 聲明死信隊列,指定死信交換機,死信路由
     */
    @Bean("dlQueue")
    public Queue dlQueue() {
        Map<String, Object> args = new HashMap<>(4);
        //指定交換機爲死信交換機
        args.put("x-dead-letter-exchange", dlExchange);
        //指定路由爲死信路由,值爲重發路由
        args.put("x-dead-letter-routing-key", reRoutingKey);
        return QueueBuilder.durable(dlQueue).withArguments(args).build();
    }

    /**
     * 聲明重發隊列
     */
    @Bean("reQueue")
    public Queue reQueue() {
        return QueueBuilder.durable(reQueue).build();
    }

    /**
     * 死信隊列綁定 死信交換機 死信路由
     */
    @Bean
    public Binding deadLetterPushBinding() {
        return new Binding(dlQueue, Binding.DestinationType.QUEUE, dlExchange, dlRoutingKey, null);
    }

    /**
     * 重發隊列 綁定死信交換機 死信路由
     */
    @Bean
    public Binding redirectPushBinding() {
        return new Binding(reQueue, Binding.DestinationType.QUEUE, dlExchange, reRoutingKey, null);
    }

    /**
     * 重發隊列 綁定重發交換機 重發路由
     */
    @Bean
    public Binding reExchangeBinding() {
        return new Binding(reQueue, Binding.DestinationType.QUEUE, reExchange, reRoutingKey, null);
    }
}


  1. 生產者
package com.lfg.message.delay.message.demo.delay.dl;

import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lfg
 * @version v1.0
 */
@RestController
public class Product {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RequestMapping("/product/message")
    public String sendMessage() {
        rabbitTemplate.convertAndSend("dead-letter-exchange-name",
                "dead-letter-routing-key-name", "測試消息:Test message !", message -> {
                    //設定編碼
                    message.getMessageProperties().setContentEncoding("utf-8");
                    //永久有效
                    message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                    //延時時間10s
                    message.getMessageProperties().setExpiration("10000");
                    return message;
                });
        return "已發送延時消息!";
    }
}

  1. 消費者
package com.lfg.message.delay.message.demo.delay.dl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
* @author lfg
* @version v1.0
*/
@Component
public class Consumer {

   private static final Logger log = LoggerFactory.getLogger(Consumer.class);

   @RabbitListener(queues = "queue-name")
   public void consume(Message message) {
       try {
           log.info("正在消費消息---{}", new String(message.getBody()));
       } catch (Exception e) {
           //注意進行異常處理,默認當出現異常時,消息將會被 再次立即重新消費
       }
   }
}

(2) 潛在的坑

通過死信隊列來間接實現延時消息,如果延時時間,不是一個固定的時間,則不建議使用這種方式。

例如:一個請求推送一條消息兩次,第一次延時時間1小時,第二次延時時間2小時。
這時候有兩次請求過來:
第一個請求完成後,產生兩條延時消息 A 和 B ,過期時間是 1 和 2 。
第二個請求完成後,產生兩條延時消息 C 和 D ,過期時間是 1 和 2 。

理想情況:當消息過期時,則直接消費。消費順序爲 ACBD
實際情況:根據消息的生成順序延時消費。消費順序爲ABCD

結論:當消息過期時間一致時則沒有問題,但是當過期時間不一致,則會有大坑。

ps: 再消息B-C之間,B過期時間2小時,C過期時間1小時,C先過期,B後過期。
但消費順序像隊列一樣 先進先出,B先消費,C後消費,並且B消費後,立即消費C

通過建多個死信隊列可以解決這個問題,過期時間類別少還好,多的話,非常麻煩。
例如微信JSAPI支付成功,間隔時間回調通知便不適合建多個隊列。

2.延時插件

通過安裝延時插件來實現延時消息,此插件只適用於磁盤節點,內存節點會安裝失敗。

(1) 具體實現

  1. rabbitmq安裝延時插件,這裏不做過多贅述,百度/google相關教程。
    官方插件鏈接
    插件下載地址:rabbitmq_delayed_message_exchange

(2) 潛在的坑

此插件只能安裝在硬盤存儲中,無法安裝在內存存儲。
rabbitmq集羣,一個內存存儲,一個硬盤存儲。
對於生產環境不推薦使用此插件。
插件github地址

引用插件官方的一句話:

This plugin was created with disk nodes in mind. RAM nodes are currently unsupported and adding support for them is not a priority (if you aren’t sure what RAM nodes are and whether you need to use them, you almost certainly don’t).

二、Redis 實現延時消息

1.具體實現

推薦使用這種方式,但是需注意redis如果掛了,要保證存儲數據的完整性。

  1. 生產者
package com.lfg.message.delay.message.demo.delay.redis;

import org.redisson.api.RDelayedQueue;
import org.redisson.api.RQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author lfg
 * @version v1.0
 */
@RestController
public class Product3 {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 頻率爲10s/20s/30s
     */
    private static final Long[] PUSH_DELAY = {10L, 20L, 30L};

    @RequestMapping("/product/message3")
    public String sendMessage() {
        for (int i = 0; i < PUSH_DELAY.length; i++) {
            //推送到隊列
            RQueue<String> queue = redissonClient.getBlockingQueue("redis-key-name");
            RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(queue);
            delayedQueue.offer("測試消息-Test Message-" + PUSH_DELAY[i], PUSH_DELAY[i], TimeUnit.SECONDS);
            delayedQueue.destroy();
        }
        return "已發送延時消息!";
    }

}

  1. 消費者
package com.lfg.message.delay.message.demo.delay.redis;

import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @author lfg
 * @version v1.0
 */
@Component
@EnableScheduling
public class Consumer3 {

    private static final Logger log = LoggerFactory.getLogger(Consumer3.class);

    @Autowired
    private RedissonClient redissonClient;

    @Scheduled(fixedDelay = 1000)
    public void consume() throws InterruptedException {
        RBlockingQueue<String> queue = redissonClient.getBlockingQueue("redis-key-name");
        String message = queue.take();
        log.info("正在消費推送信息:" + message);
        //此種方式拋異常也會正常消費掉消息
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章