一 概述
通常,我們會通過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的安裝和測試類須自行實現,由於小編能力有限,文中如有錯誤,還望指正,謝謝合作!