Java中動態聲明與綁定Rabbit MQ隊列以及延遲隊列的實現與使用

一 概述

通常,我們會通過Spring schemal來配置隊列基礎設施、隊列聲明以及綁定等功能,這樣讓我們能夠很方便的通過Spring注入對象去使用它,但有不足之處就是,在項目中如果我們想要多個隊列去分擔不同的任務,此時我們不得不創建很多不同的Rabbit MQ Spring schemal,那麼這種做法就顯得太過繁瑣與笨重了。反之,在Java代碼裏動態的去聲明和綁定隊列,就會方便很多了,而在schemal中我們只需引入Rabbit MQ的相關配置即可。本篇博客會講解如何在Java代碼中動態的聲明與綁定隊列以及延遲隊列的實現。

注意:

  • 本篇博客介紹的是Java語言下的使用,客戶端使用的是Spring AMQP,版本爲1.7.7(詳情見pom.xml);
  • 本篇博客不使用Rabbit MQ的數據對象轉化,如有需要須自行實現;
  • 代碼中聲明的Exchange都爲D型的,如果需要別的類型,可自行抽取代碼。

二 配置Rabbit MQ

  • pom.xm 引入Spring AMQP
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    <version>1.7.7.RELEASE</version>
</dependency>
  • 在 config.properties(一個自定義的屬性配置文件)中配置Rabbit MQ相關,需要在Spring的schemal 中導入
# Rabbit MQ
rabbit.username=wltask
rabbit.password=123123
rabbit.port=5672
rabbit.host=192.168.30.218
rabbit.virtual.host=/rabbit
  • 編寫applicationContext-rabbitmq.xml schemal

其實不需要定義consumer 和 producer, 項目中爲了更好的區分類型來源,才這麼定義,可根據自己的需求定義一個或者是多個

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/rabbit
           http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
           
    <!-- Consumer -->
    <rabbit:connection-factory id="consumerConnectionFactory"
                               host="${rabbit.host}" port="${rabbit.port}"
                               username="${rabbit.username}"
                               password="${rabbit.password}"
                               virtual-host="${rabbit.virtual.host}"
                               channel-cache-size="300"
                               publisher-confirms="true"/>

    <rabbit:template id="consumerAmqpTemplate" connection-factory="consumerConnectionFactory"/>

    <rabbit:admin id="consumerRabbitAdmin" connection-factory="consumerConnectionFactory"/>

    <!-- Producer -->

    <rabbit:connection-factory id="producerConnectionFactory"
                               host="${rabbit.host}" port="${rabbit.port}"
                               username="${rabbit.username}"
                               password="${rabbit.password}"
                               virtual-host="${rabbit.virtual.host}"
                               channel-cache-size="300"
                               publisher-confirms="true"/>

    <rabbit:template id="producerAmqpTemplate" connection-factory="producerConnectionFactory"/>

    <rabbit:admin id="producerRabbitAdmin" connection-factory="producerConnectionFactory"/>

</beans>
  • 抽取生產者和消費者公共配置接口IRabbitMqConfig
package com.bell.rabbitmq;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 18:33
 * Copyright © Bell All Rights Reserved.
 */
public interface IRabbitMqConfig {

    /**
     * queue name
     *
     * @return
     */
    String queueName();

    /**
     * queue exchange name
     *
     * @return
     */
    String queueExchangeName();

    /**
     * queue route key
     *
     * @return
     */
    String queueRouteKey();

}
  • 抽取生產者和消費者公共配置抽象類AbstractRabbitMqBase並實現IRabbitMqConfig接口,但在抽象類型不實現IRabbitMqConfig接口
package com.bell.rabbitmq;

import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 18:37
 * Copyright © Bell All Rights Reserved.
 */
public abstract class AbstractRabbitMqBase implements IRabbitMqConfig {

    @Resource
    private RabbitAdmin producerRabbitAdmin;

    @Resource
    private RabbitTemplate producerAmqpTemplate;

    @PostConstruct
    private void init() {

        Queue queue = new Queue(queueName());
        DirectExchange exchange = new DirectExchange(queueExchangeName());

        producerRabbitAdmin.declareQueue(queue);
        producerRabbitAdmin.declareExchange(exchange);
        producerRabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(exchange).with(queueRouteKey()));

    }

    /**
     * 發佈字符串信息到隊列中
     *
     * @param
     */
    protected void publishMessage(String message) {
        producerAmqpTemplate.convertAndSend(queueExchangeName(), queueRouteKey(), message);
    }

    /**
     * 發佈Message對象信息到隊列中
     *
     * @param message
     */
    protected void publishMessage(Message message) {
        producerAmqpTemplate.send(queueExchangeName(), queueRouteKey(), message);
    }

}
  • 抽取消費者公共抽象類並繼承AbstractRabbitMqBase,同時實現ChannelAwareMessageListener,此處我們使用ChannelAwareMessageListener去接收消息,除此之外,我們需要在增加一個抽象方法getConsumerCount(),用於配置要啓用多少個消費者,同時需要實現onDestroy()方法,在類銷燬時去斷開與MQ服務器的鏈接,而不是異常退出,保證消息不丟失或被正常ACK
package com.bell.rabbitmq;

import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.Objects;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 18:50
 * Copyright © Bell All Rights Reserved.
 */
public abstract class AbstractRabbitMqConsumer extends AbstractRabbitMqBase implements ChannelAwareMessageListener {

    @Resource
    private ConfigService configService;

    @Resource
    private ConnectionFactory consumerConnectionFactory;

    private SimpleMessageListenerContainer[] rabbitMqListener = null;

    @PostConstruct
    private void init() {
    
        rabbitMqListener = new SimpleMessageListenerContainer[getConsumerCount()];

        for (int i = 0; i < getConsumerCount(); ++i) {
            rabbitMqListener[i] = new SimpleMessageListenerContainer(consumerConnectionFactory);
            rabbitMqListener[i].setMessageListener(this);
            rabbitMqListener[i].setAcknowledgeMode(AcknowledgeMode.MANUAL);
            rabbitMqListener[i].setQueueNames(queueName());
            rabbitMqListener[i].start();
        }
    }

    /**
     * 需要創建多少個消費者
     *
     * @return
     */
    protected abstract int getConsumerCount();

    @PreDestroy
    private void onDestroy() {

        if (Objects.isNull(rabbitMqListener) || rabbitMqListener.length <= 0) {
            return;
        }

        for (int i = 0; i < getConsumerCount(); ++i) {
            rabbitMqListener[i].destroy();
        }
    }
}

三 使用Rabbit MQ

完成了Rabbit MQ的基本引入與配置後,就可以去使用它了

  • 生產者的使用。定義一個生產者類TestRabbitMqProducer繼承AbstractRabbitMqBase,然後在需要使用的地方通過Spring注入此類,就可以發送消息到隊列裏了
package com.bell.rabbitmq.test;

import org.springframework.stereotype.Service;
import bell.util.JSONUtil;

import java.util.Objects;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 16:25
 * Copyright © Bell All Rights Reserved.
 */
@Service
public class TestRabbitMqProducer extends AbstractRabbitMqBase {

    public Boolean publish(String data) {

        // data 字符串我使用json格式的,這樣方便反序列化,當然可以使用Rabbit MQ的convert,由於篇幅有限,不做介紹,請自行實現
        this.publishMessage(data);

        return true;
    }

    @Override
    public String queueName() {
        return "rabbitmq.test.queue";
    }

    @Override
    public String queueExchangeName() {
        return "rabbitmq.test.exchange";
    }

    @Override
    public String queueRouteKey() {
        return "rabbitmq.test.route.key";
    }

    // 對於 queue,exchange,route key等參數可以放到一個常量類中,一處定義,多處可用,還能保證生產者與消費者不一致
}
  • 消費者的使用。創建消費者類TestRabbitMqConsumer繼承AbstractRabbitMqConsumer類,並實現所有的方法纔可使用,此類只要使用@Service標註後,就可開始消費,不用做其他的操作
package com.bell.rabbitmq.test;

import org.springframework.stereotype.Service;
import bell.util.JSONUtil;

import java.util.Objects;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 16:25
 * Copyright © Bell All Rights Reserved.
 */
@Service
public class TestRabbitMqConsumer extends AbstractRabbitMqConsumer {


    /**
     *   onMessage方法中不要拋出異常,否則會阻塞此消費者,導致服務端不在向此消費者推送消息
     */  
    @Override
    public void onMessage(Message message, Channel channel) throws IOException {

        // 我們發送的消息類型是已bytes數組的形式存在的
        String eventMessage = new String(message.getBody());

        System.out.println(eventMessage);

        // 確認收到消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

        // 確認收到消息,但是業務異常了,需要重回隊列
        // channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            
    }

    @Override
    protected int getConsumerCount() {
        return 1;
    }

    @Override
    public String queueName() {
        return "rabbitmq.test.queue";
    }

    @Override
    public String queueExchangeName() {
        return "rabbitmq.test.exchange";
    }

    @Override
    public String queueRouteKey() {
        return "rabbitmq.test.route.key";
    }

    // 對於 queue,exchange,route key等參數可以放到一個常量類中,一處定義,多處可用,還能保證生產者與消費者不一致
}

到這裏,動態的聲明和綁定隊列就完了, 接下來,我將繼續講解如何配置與使用延遲隊列。

四 延遲隊列的配置與使用須知

延遲隊列的實現有兩種方式,第一種是使用插件的方式(詳情見官網),第一種方式是通過插件的方式去啓用延遲隊列,由於插件是官方Rabbit MQ不自帶的,所以在Rabbit MQ的後臺管理中心是看不到延遲隊列信息的,不太利於觀察和維護,但使用與否,取決於個人。第二種方式是使用兩個隊列去實現,一個隊列充當消息計時隊列,當這些消息變成死信之後,如果不配置死信轉發機制,那麼這些死信將會被丟棄,反之,要想這些消息被消費,就需要另一個隊列接收這些死信,從而讓它在次被消費,從而實現延遲的功能。使用本篇博客所配置的延遲隊列需要注意以下事項:

1)不對隊列設置TTL。

2)不對單個消息設置TTL。也就是說所有的消息得過期時間都是一樣的,過期從進入隊列開始計算,因消息到達隊列有先後順序,如果同一個隊列中的每個消息時間都不一樣,那麼隊頭的消息時間還沒有過期,而隊列中間的消息過期了,隊列中間的消息不會立馬被轉發,只有當他到達隊頭後才能被轉發,因此,爲保證過期消息的過期時間不遠大於設定的時間,本博客講解的配置不對隊列和單個消息設置不同的時間。

3)因採用的是使用兩個隊列實現延遲隊列,因此需要結合本篇博客前部分的配置,請熟知。

4)延遲隊列不得有消費,否則就無法實現延遲功能。

五 延遲隊列的配置與使用

  • 隊列配置參數解釋

1)參數x-dead-letter-exchange爲死信轉發Exchange;

2)參數x-dead-letter-routing-key爲死信轉發Route key;

3)參數x-message-ttl爲消息在隊列裏的生存時間;

  • 創建延遲隊列配置抽象類AbstractRabbitDelayMqBase並實現IRabbitMqConfig接口,接口在抽象類中不實現。同時創建messageTtl()deadLetterRoutingKey()deadLetterExchange()三個抽象方法,方法作用描述見代碼
package com.bell.rabbitmq;

import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 18:37
 * Copyright © Bell All Rights Reserved.
 */
public abstract class AbstractRabbitDelayMqBase implements IRabbitMqConfig {

    @Resource
    private RabbitAdmin producerRabbitAdmin;

    @Resource
    private RabbitTemplate producerAmqpTemplate;

    @PostConstruct
    private void init() {

		// 此處配置延遲隊列相關參數
        Map<String, Object> args = new HashMap<>(3);
        args.put("x-dead-letter-exchange", deadLetterExchange());
        args.put("x-dead-letter-routing-key", deadLetterRoutingKey());
        args.put("x-message-ttl", messageTtl());

        DirectExchange exchange = new DirectExchange(queueExchangeName());
        Queue queue = new Queue(queueName(), true, false, false, args);

        producerRabbitAdmin.declareQueue(queue);
        producerRabbitAdmin.declareExchange(exchange);
        producerRabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(exchange).with(queueRouteKey()));
    }

    /**
     * 消息生存時間 (單位毫秒)
     *
     * @return
     */
    protected abstract Integer messageTtl();

    /**
     * 死信交Routing
     *
     * @return
     */
    protected abstract String deadLetterRoutingKey();

    /**
     * 死信Exchange
     *
     * @return
     */
    protected abstract String deadLetterExchange();

    /**
     * 發佈消息到隊列中
     *
     * @param
     */
    protected void publishMessage(String message) {
        producerAmqpTemplate.convertAndSend(queueExchangeName(), queueRouteKey(), message);
    }

    /**
     * 發佈消息隊列中
     *
     * @param message
     */
    protected void publishMessage(Message message) {
        producerAmqpTemplate.send(queueExchangeName(), queueRouteKey(), message);
    }
}

  • 創建延遲隊列生產者類TestRabbitMqDelayProducer繼承AbstractRabbitDelayMqBase類,並實現所有方法,在實現方法時需要注意,延遲隊列死信轉發的Exchange和Route key必須同接收死信隊列的Exchange和Route key保持一致,且延遲隊列不需要消費者,接收死信的隊列的生產者可根據業務需求可有可無
package com.bell.rabbitmq.test;

import org.springframework.amqp.core.MessageProperties;
import org.springframework.stereotype.Service;
import bell.util.JSONUtil;

import javax.annotation.PostConstruct;
import java.util.Objects;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 14:53
 * Copyright © Bell All Rights Reserved.
 */
@Service
public class TestRabbitMqDelayProducer extends AbstractRabbitDelayMqBase {

    /**
     * 消息過期時間 (一天)
     */
    private static final int MESSAGE_DELAY_TIME = 24 * 3600 * 1000;

    public Boolean publishMessage(String data) {

        // data爲json字符串,同樣,可以使用Rabbit MQ的convert,自行實現
        publishMessage(data);
        
        return true;
    }

    @Override
    protected Integer messageTtl() {
        return MESSAGE_DELAY_TIME;
    }


    @Override
    public String queueName() {
        return "rabbitmq.test.delay.queue";
    }

    @Override
    public String queueExchangeName() {
        return "rabbitmq.test.delay.exchange";
    }

    @Override
    public String queueRouteKey() {
        return "rabbitmq.test.delay.route.key";
    }

    @Override
    protected String deadLetterRoutingKey() {
        return "rabbitmq.test.delay.receive.route.key";
    }

    @Override
    protected String deadLetterExchange() {
        return "rabbitmq.test.delay.receive.exchange";
    }

    // 對於 queue,exchange,route key等參數可以放到一個常量類中,一處定義,多處可用,還能保證生產者與消費者不一致

}
  • 創建延遲隊列消費者類TestRabbitMqDelayConsumer並繼承AbstractRabbitMqConsumer抽象類
package com.bell.rabbitmq.test;

import org.springframework.stereotype.Service;
import bell.util.JSONUtil;

import java.util.Objects;

/**
 * @Author: yqs
 * @Date: 2019/1/25
 * @Time: 16:25
 * Copyright © Bell All Rights Reserved.
 */
@Service
public class TestRabbitMqDelayConsumer extends AbstractRabbitMqConsumer {


    /**
     *   onMessage方法中不要拋出異常,否則會阻塞此消費者,導致服務端不在向此消費者推送消息
     */  
    @Override
    public void onMessage(Message message, Channel channel) throws IOException {

        // 我們發送的消息類型是已bytes數組的形式存在的
        String eventMessage = new String(message.getBody());

        System.out.println(eventMessage);

        // 確認收到消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

        // 確認收到消息,但是業務異常了,需要重回隊列
        // channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
            
    }

    @Override
    protected int getConsumerCount() {
        return 1;
    }

    @Override
    public String queueName() {
        return "rabbitmq.test.delay.receive.queue";
    }

    // 此處的 Exchange 和 Route key必須同延遲隊列裏聲明的死信轉發保持一致

    @Override
    public String queueExchangeName() {
        return "rabbitmq.test.delay.receive.exchange";
    }

    @Override
    public String queueRouteKey() {
        return "rabbitmq.test.delay.receive.route.key";
    }

    // 對於 queue,exchange,route key等參數可以放到一個常量類中,一處定義,多處可用,還能保證生產者與消費者不一致
}
  • 到此步,我們便配置完了延遲隊列相關,此時,可編寫單元測試去查看戰果了,由於篇幅有限,不在展示測試類。當我們跑通程序後,就會發現在Rabbit MQ的管理後臺有兩個隊列,一個是延遲隊列rabbitmq.test.delay.queue,一個是延遲隊列消費者rabbitmq.test.delay.receive.queue,並且rabbitmq.test.delay.queue在Features那一列會出現D TTL DLX DLK這幾個大寫字母,這個便是我們配置的死信轉發Exchange,Route key,以及消息TTL。

本篇博客所有的代碼都是全的,可直接拷貝使用,對Rabbit MQ的安裝和測試類須自行實現,由於小編能力有限,文中如有錯誤,還望指正,謝謝合作!

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