第四十六章:SpringBoot & RabbitMQ完成訊息延遲消費

恆宇少年發表於2018-06-23

2018-3-1SpringBoot官方發版了2.0.0.RELEASE最新版本,新版本完全基於Spring5.0來構建,JDK最低支援也從原來的1.6也改成了1.8,不再相容1.8以下的版本,更多新特性請檢視官方文件

本章目標

基於SpringBoot整合RabbitMQ完成訊息延遲消費。

構建專案

注意前言

由於SpringBoot的內建掃描機制,我們如果不自動配置掃描路徑,請保持下面rabbitmq-common模組內的配置可以被SpringBoot掃描到,否則不會自動建立佇列,控制檯會輸出404的錯誤資訊。

SpringBoot 企業級核心技術學習專題


專題 專題名稱 專題描述
001 Spring Boot 核心技術 講解SpringBoot一些企業級層面的核心元件
002 Spring Boot 核心技術章節原始碼 Spring Boot 核心技術簡書每一篇文章碼雲對應原始碼
003 Spring Cloud 核心技術 對Spring Cloud核心技術全面講解
004 Spring Cloud 核心技術章節原始碼 Spring Cloud 核心技術簡書每一篇文章對應原始碼
005 QueryDSL 核心技術 全面講解QueryDSL核心技術以及基於SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術 全面講解SpringDataJPA核心技術
007 SpringBoot核心技術學習目錄 SpringBoot系統的學習目錄,敬請關注點贊!!!

我們本章採用2.0.0.RELEASE版本的SpringBoot,新增相關的依賴如下所示:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
</parent>
......
<dependencies>
        <!--rabbbitMQ相關依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!--web相關依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--lombok依賴-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--spring boot tester-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--fast json依賴-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.40</version>
        </dependency>
    </dependencies>
......
複製程式碼

我們仍然採用多模組的方式來測試佇列的Provider以及Consumer

佇列公共模組

我們先來建立一個名為rabbitmq-common公共依賴模組(Create New Maven Module) 在公共模組內新增一個QueueEnum佇列列舉配置,該列舉內配置佇列的ExchangeQueueNameRouteKey等相關內容,如下所示:

package com.hengyu.rabbitmq.lazy.enums;

import lombok.Getter;

/**
 * 訊息佇列列舉配置
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/3
 * Time:下午4:33
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Getter
public enum QueueEnum {
    /**
     * 訊息通知佇列
     */
    MESSAGE_QUEUE("message.center.direct", "message.center.create", "message.center.create"),
    /**
     * 訊息通知ttl佇列
     */
    MESSAGE_TTL_QUEUE("message.center.topic.ttl", "message.center.create.ttl", "message.center.create.ttl");
    /**
     * 交換名稱
     */
    private String exchange;
    /**
     * 佇列名稱
     */
    private String name;
    /**
     * 路由鍵
     */
    private String routeKey;

    QueueEnum(String exchange, String name, String routeKey) {
        this.exchange = exchange;
        this.name = name;
        this.routeKey = routeKey;
    }
}
複製程式碼

可以看到MESSAGE_QUEUE佇列配置跟我們之前章節的配置一樣,而我們另外新建立了一個字尾為ttl的訊息佇列配置。我們採用的這種方式是RabbitMQ訊息佇列其中一種的延遲消費模組,通過配置佇列訊息過期後轉發的形式。

這種模式比較簡單,我們需要將訊息先傳送到ttl延遲佇列內,當訊息到達過期時間後會自動轉發到ttl佇列內配置的轉發Exchange以及RouteKey繫結的佇列內完成訊息消費。

下面我們來模擬訊息通知的延遲消費場景,先來建立一個名為MessageRabbitMqConfiguration的佇列配置類,該配置類內新增訊息通知佇列配置以及訊息通過延遲佇列配置,如下所示:

/**
 * 訊息通知 - 訊息佇列配置資訊
 *
 * @author:恆宇少年 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/3
 * Time:下午4:32
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Configuration
public class MessageRabbitMqConfiguration {
    /**
     * 訊息中心實際消費佇列交換配置
     *
     * @return
     */
    @Bean
    DirectExchange messageDirect() {
        return (DirectExchange) ExchangeBuilder
                .directExchange(QueueEnum.MESSAGE_QUEUE.getExchange())
                .durable(true)
                .build();
    }

    /**
     * 訊息中心延遲消費交換配置
     *
     * @return
     */
    @Bean
    DirectExchange messageTtlDirect() {
        return (DirectExchange) ExchangeBuilder
                .directExchange(QueueEnum.MESSAGE_TTL_QUEUE.getExchange())
                .durable(true)
                .build();
    }

    /**
     * 訊息中心實際消費佇列配置
     *
     * @return
     */
    @Bean
    public Queue messageQueue() {
        return new Queue(QueueEnum.MESSAGE_QUEUE.getName());
    }


    /**
     * 訊息中心TTL佇列
     *
     * @return
     */
    @Bean
    Queue messageTtlQueue() {
        return QueueBuilder
                .durable(QueueEnum.MESSAGE_TTL_QUEUE.getName())
                // 配置到期後轉發的交換
                .withArgument("x-dead-letter-exchange", QueueEnum.MESSAGE_QUEUE.getExchange())
                // 配置到期後轉發的路由鍵
                .withArgument("x-dead-letter-routing-key", QueueEnum.MESSAGE_QUEUE.getRouteKey())
                .build();
    }

    /**
     * 訊息中心實際訊息交換與佇列繫結
     *
     * @param messageDirect 訊息中心交換配置
     * @param messageQueue  訊息中心佇列
     * @return
     */
    @Bean
    Binding messageBinding(DirectExchange messageDirect, Queue messageQueue) {
        return BindingBuilder
                .bind(messageQueue)
                .to(messageDirect)
                .with(QueueEnum.MESSAGE_QUEUE.getRouteKey());
    }

    /**
     * 訊息中心TTL繫結實際訊息中心實際消費交換機
     *
     * @param messageTtlQueue
     * @param messageTtlDirect
     * @return
     */
    @Bean
    public Binding messageTtlBinding(Queue messageTtlQueue, DirectExchange messageTtlDirect) {
        return BindingBuilder
                .bind(messageTtlQueue)
                .to(messageTtlDirect)
                .with(QueueEnum.MESSAGE_TTL_QUEUE.getRouteKey());
    }
}
複製程式碼

我們宣告瞭訊息通知佇列的相關ExchangeQueueBinding等配置,將message.center.create佇列通過路由鍵message.center.create繫結到了message.center.direct交換上。

除此之外,我們還新增了訊息通知延遲佇列ExchangeQueueBinding等配置,將message.center.create.ttl佇列通過message.center.create.ttl路由鍵繫結到了message.center.topic.ttl交換上。

我們仔細來看看messageTtlQueue延遲佇列的配置,跟messageQueue佇列配置不同的地方這裡多出了x-dead-letter-exchangex-dead-letter-routing-key兩個引數,而這兩個引數就是配置延遲佇列過期後轉發的ExchangeRouteKey,只要在建立佇列時對應新增了這兩個引數,在RabbitMQ管理平臺看到的佇列配置就不僅是單純的Direct型別的佇列型別,如下圖所示:

佇列型別差異

在上圖內我們可以看到message.center.create.ttl佇列多出了DLXDLK的配置,這就是RabbitMQ死信交換的標誌。 滿足死信交換的條件,在官方文件中表示:

Messages from a queue can be 'dead-lettered'; that is, republished to another exchange when any of the following events occur:

The message is rejected (basic.reject or basic.nack) with requeue=false, The TTL for the message expires; or The queue length limit is exceeded.

  • 該訊息被拒絕(basic.reject或 basic.nack),requeue = false
  • 訊息的TTL過期
  • 佇列長度限制已超出 官方文件地址

我們需要滿足上面的其中一種方式就可以了,我們採用滿足第二個條件,採用過期的方式。

佇列訊息提供者

我們再來建立一個名為rabbitmq-lazy-provider的模組(Create New Maven Module),並且在pom.xml配置檔案內新增rabbitmq-common模組的依賴,如下所示:

<!--新增公共模組依賴-->
<dependency>
      <groupId>com.hengyu</groupId>
      <artifactId>rabbitmq-common</artifactId>
      <version>0.0.1-SNAPSHOT</version>
</dependency>
複製程式碼

配置佇列

resource下建立一個名為application.yml的配置檔案,在該配置檔案內新增如下配置資訊:

spring:
  #rabbitmq訊息佇列配置資訊
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /hengboy
    publisher-confirms: true
複製程式碼

訊息提供者類

接下來我們來建立名為MessageProvider訊息提供者類,用來傳送訊息內容到訊息通知延遲佇列,程式碼如下所示:

/**
 * 訊息通知 - 提供者
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/3
 * Time:下午4:40
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Component
public class MessageProvider {
    /**
     * logger instance
     */
    static Logger logger = LoggerFactory.getLogger(MessageProvider.class);
    /**
     * RabbitMQ 模版訊息實現類
     */
    @Autowired
    private AmqpTemplate rabbitMqTemplate;

    /**
     * 傳送延遲訊息
     *
     * @param messageContent 訊息內容
     * @param exchange       佇列交換
     * @param routerKey      佇列交換繫結的路由鍵
     * @param delayTimes     延遲時長,單位:毫秒
     */
    public void sendMessage(Object messageContent, String exchange, String routerKey, final long delayTimes) {
        if (!StringUtils.isEmpty(exchange)) {
            logger.info("延遲:{}毫秒寫入訊息佇列:{},訊息內容:{}", delayTimes, routerKey, JSON.toJSONString(messageContent));
            // 執行傳送訊息到指定佇列
            rabbitMqTemplate.convertAndSend(exchange, routerKey, messageContent, message -> {
                // 設定延遲毫秒值
                message.getMessageProperties().setExpiration(String.valueOf(delayTimes));
                return message;
            });
        } else {
            logger.error("未找到佇列訊息:{},所屬的交換機", exchange);
        }
    }
}
複製程式碼

由於我們在 pom.xml配置檔案內新增了RabbitMQ相關的依賴並且在上面application.yml檔案內新增了對應的配置,SpringBoot為我們自動例項化了AmqpTemplate,該例項可以傳送任何型別的訊息到指定佇列。 我們採用convertAndSend方法,將訊息內容傳送到指定ExchangeRouterKey佇列,並且通過setExpiration方法設定過期時間,單位:毫秒。

編寫傳送測試

我們在test目錄下建立一個測試類,如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMqLazyProviderApplication.class)
public class RabbitMqLazyProviderApplicationTests {
    /**
     * 訊息佇列提供者
     */
    @Autowired
    private MessageProvider messageProvider;

    /**
     * 測試延遲訊息消費
     */
    @Test
    public void testLazy() {
        // 測試延遲10秒
        messageProvider.sendMessage("測試延遲消費,寫入時間:" + new Date(),
                QueueEnum.MESSAGE_TTL_QUEUE.getExchange(),
                QueueEnum.MESSAGE_TTL_QUEUE.getRouteKey(),
                10000);
    }
}
複製程式碼

注意:@SpringBootTest註解內新增了classes入口類的配置,因為我們是模組建立的專案並不是預設建立的SpringBoot專案,這裡需要配置入口程式類才可以執行測試。

在測試類我們注入了MessageProvider訊息提供者,呼叫sendMessage方法傳送訊息到訊息通知延遲佇列,並且設定延遲的時間為10秒,這裡衡量傳送到指定佇列的標準是要看MessageRabbitMqConfiguration配置類內的相關Binding配置,通過ExchangeRouterKey值進行傳送到指定的佇列。

到目前為止我們的rabbitmq-lazy-provider訊息提供模組已經編寫完成了,下面我們來看看訊息消費者模組。

佇列訊息消費者

我們再來建立一個名為rabbitmq-lazy-consumer的模組(Create New Maven Module),同樣需要在pom.xml配置檔案內新增rabbitmq-common模組的依賴,如下所示:

<!--新增公共模組依賴-->
<dependency>
      <groupId>com.hengyu</groupId>
      <artifactId>rabbitmq-common</artifactId>
      <version>0.0.1-SNAPSHOT</version>
</dependency>
複製程式碼

當然同樣需要在resource下建立application.yml並新增訊息佇列的相關配置,程式碼就不貼出來了,可以直接從rabbitmq-lazy-provider模組中複製application.yml檔案到當前模組內。

訊息消費者類

接下來建立一個名為MessageConsumer的消費者類,該類需要監聽訊息通知佇列,程式碼如下所示:

/**
 * 訊息通知 - 消費者
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/3
 * Time:下午5:00
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@Component
@RabbitListener(queues = "message.center.create")
public class MessageConsumer {
    /**
     * logger instance
     */
    static Logger logger = LoggerFactory.getLogger(MessageConsumer.class);

    @RabbitHandler
    public void handler(String content) {
        logger.info("消費內容:{}", content);
    }
}
複製程式碼

@RabbitListener註解內配置了監聽的佇列,這裡配置內容是QueueEnum列舉內的queueName屬性值,當然如果你採用常量的方式在註解屬性上是直接可以使用的,列舉不支援這種配置,這裡只能把QueueName字串配置到queues屬性上了。 由於我們在訊息傳送時採用字串的形式傳送訊息內容,這裡在@RabbitHandler處理方法的引數內要保持資料型別一致!

消費者入口類

我們為消費者模組新增一個入口程式類,用於啟動消費者,程式碼如下所示:

/**
 * 【第四十六章:SpringBoot & RabbitMQ完成訊息延遲消費】
 * 佇列消費者模組 - 入口程式類
 *
 * @author:於起宇 <br/>
 * ===============================
 * Created with IDEA.
 * Date:2018/3/3
 * Time:下午4:55
 * 簡書:http://www.jianshu.com/u/092df3f77bca
 * ================================
 */
@SpringBootApplication
public class RabbitMqLazyConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(RabbitMqLazyConsumerApplication.class, args);
    }
}
複製程式碼

測試

我們的程式碼已經編寫完畢,下面來測試下是否完成了我們預想的效果,步驟如下所示:

1. 啟動消費者模組
2. 執行RabbitMqLazyProviderApplicationTests.testLazy()方法進行傳送訊息到通知延遲佇列
3. 檢視消費者模組控制檯輸出內容
複製程式碼

我們可以在消費者模組控制檯看到輸出內容:

2018-03-04 10:10:34.765  INFO 70486 --- [cTaskExecutor-1] c.h.r.lazy.consumer.MessageConsumer      : 消費內容:測試延遲消費,寫入時間:Sun Mar 04 10:10:24 CST 2018
複製程式碼

我們在提供者測試方法傳送訊息的時間為10:10:24,而真正消費的時間則為10:10:34,與我們預計的一樣,訊息延遲了10秒後去執行消費。

總結

終上所述我們完成了訊息佇列的延遲消費,採用死信方式,通過訊息過期方式觸發,在實際專案研發過程中,延遲消費還是很有必要的,可以省去一些定時任務的配置。

本章原始碼已經上傳到碼雲: SpringBoot配套原始碼地址:gitee.com/hengboy/spr… SpringCloud配套原始碼地址:gitee.com/hengboy/spr… SpringBoot相關係列文章請訪問:目錄:SpringBoot學習目錄 QueryDSL相關係列文章請訪問:QueryDSL通用查詢框架學習目錄 SpringDataJPA相關係列文章請訪問:目錄:SpringDataJPA學習目錄,感謝閱讀!

微信掃碼關注 - 專注分享

相關文章