目標:
從宏觀上掌握RabbitMq這個消息中間件的基本原理。同時讓閱讀者掌握一些基本的使用方法。
大致原理介紹
爲了實現解耦或者實現異步,將消息先發往獨立於應用服務以外的一箇中間服務(也就是mq)存儲,其他服務在從這個中間服務獲取消息,進行接下來的業務邏輯處理。整體流程如下:
消息中間件的作用
市場上包括各種中間件Kafka、RabbitMq、ActiveMq、RocketMq等。作用其實都是類似
- 解耦
- 流量削峯
- 異步通信
- 冗餘、擴展、緩衝等
中間件的安裝
RabbitMq的整體架構分析
相關名詞介紹:
- producer :生產者,可以理解爲發送消息的一方
- consumer:消費者,可以理解爲處理消息的一方
- broker:消息中間件服務,消息的中間方。安裝mq服務的節點。
消息的流轉過程
其他關鍵名詞介紹:
- 交換機:可以理解爲一個路由器,整個消息進度broker的第一個處理者。根據消息的不同,將消息放入不同的隊列。
- 路由鍵:標誌消息屬於哪個隊列(某些情況下該參數失效)
- 綁定: 將交換機和隊列進行綁定,
- 隊列:整個mq服務端(發送消息和消費消息的是客戶端)用於存儲消息的對象;
交換機類型
其實就是介紹交換機的常見模式。有一對一,也有一對多
1、fanout:可以理解爲組播
凡是綁定在交換機下的隊列都能收到消息。一個交換機會綁定很多個隊列,這種情況下路由鍵會失效。
2、direct:完全根據路由key進行路由
可以看到路由鍵爲warning的話,消息會被推到兩個隊列;路由key是info的話只會進入一個隊列。這個就是direct類型的交互器的特徵。
3、topic:綁定的key帶有通配符
路由鍵帶有通配符
4、head
實際使用介紹
實際使用介紹之前,我們先介紹下“連接”和“信道”的概念
生產者在和mq服務通信過程中是通過TCP協議,那個二者之間就會建立tcp連接,這種連接的建立通常是非常耗費時間,所以mq的設計者就使用了複用tcp連接的思路。那channel又是什麼呢?他的中文翻譯是信道,這個信道我們可以理解爲完成一次邏輯通信的對象。比如我們可以是整個生產者服務和mq之間只有一條TCP鏈路,但是生產者可以是多線程的,多線程各自維護了一個和Mq服務通信的信道,也就是這裏的channl ,chanel是一條邏輯上的通信鏈路。Connection是一條物理上的通信鏈路。
那爲什麼不直接使用Connection呢?主要是考慮各個線程之間的數據隔離
是不是無論多少個信道都可以共用一個Connection呢?不是,當信道數量越來越多的時候,一個Tcp連接可能不夠用,我們應該適當的增加物理連接的數量。
最簡單使用
默認交換機的使用
# 配置類
@Configuration
//@ConditionalOnProperty(prefix = SystemProperties.PREFIX, name = "openRabbitMq", havingValue = "true", matchIfMissing = true)
public class RabbitMqConfiguration {
@Bean
CommonConsumer commonConsumer(){
return new CommonConsumer(); //申明一個默認消費者,
}
@Bean //定義一個普通隊列,並沒有給整個隊列綁定交換機哦!
public Queue commonQueue() {
return new Queue(QueueEnum.COMMON_QUEUE.getQueueName());
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory connectionFactory, MessageConverter messageConverter) {
connectionFactory.setPublisherConfirms(true);
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(messageConverter);
return factory;
}
@Bean
public MessageConverter messageConverter() {//申明對象序列化類
return new ContentTypeDelegatingMessageConverter(new Jackson2JsonMessageConverter());
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(messageConverter);
template.setMandatory(true);
return template;
}
}
## 生產者
package com.defire.provider;
// 實現了 InitializingBean的對象在bean初始化時會調用afterPropertiesSet方法。
//ConfirmCallback & 和ReturnCallback 是爲了實現消息確認機制,保證整個中間件的高可用。我們後文還會深入探討
@Component
public class CommonProvider implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback, InitializingBean {
static Logger logger = LoggerFactory.getLogger(CommonProvider.class);
protected RabbitTemplate rabbitTemplate;
public CommonProvider(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
/**
* 發送消息
*
* @param messageContent
*/
public void sendMessage(MessageContent messageContent, QueueEnum queueEnum) {
if (messageContent != null ) {
messageContent.setExchange(queueEnum.getExchange());//當使用默認隊列的時候,交換機的名字是空
messageContent.setQueueName(queueEnum.getQueueName());
messageContent.setRouteKey(queueEnum.getRouteKey());
MyCorrelationData correlationData = new MyCorrelationData(messageContent.getMessageId(), messageContent);
correlationData.setExchange(queueEnum.getExchange());
correlationData.setRoutingKey(queueEnum.getRouteKey());
// 執行發送消息到指定隊列
rabbitTemplate.convertAndSend(queueEnum.getExchange(), queueEnum.getRouteKey(), messageContent, correlationData);
logger.debug("CommonProvider新增消息內容:{}", JSON.toJSONString(messageContent));
} else {
logger.warn("消息內容爲空或未開啓隊列!!!!!");
}
}
/**
* 用於實現消息發送到RabbitMQ交換器後接收ack回調,
* 如果消息已經到到中間件,則會回調該方法。該方法在afterPropertiesSet中已經配置到rabbitTemplate對象上。
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.debug("CommonProvider消息到達exchange成功,{}", correlationData == null ? cause : correlationData);
} else {
logger.debug("CommonProvider消息到達exchange失敗,{}", correlationData == null ? cause : correlationData);
}
}
/**
* 用於實現消息發送到RabbitMQ交換器,但無相應隊列與交換器綁定時的回調。
* 如果消息已經到到中間件,但是中間件沒有知道到對應的隊列,則會回調該方法。
* 該方法在afterPropertiesSet中已經配置到rabbitTemplate對象上。
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
if (!QueueEnum.MESSAGE_DELAY_QUEUE.getExchange().equals(exchange)) {
logger.error("CommonProvider發送失敗,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息體:{}",
replyCode, replyText, exchange, exchange, routingKey, JSON.toJSONString(message));
}
}
@Override
public void afterPropertiesSet() throws Exception {
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
}
}
上文簡單的介紹了僅僅申明一個隊列,未申明交互機,未進行綁定的默認情況。
如果使用默認交換機,則消息一定會被投遞到和路由鍵一致的隊列中。下面介紹其他類型的用法
其他特殊用法(兩種實現延遲隊列的方式)
所謂延遲隊列,就是生產者將消息發送到隊列後,隊列不是立即消費,而是等待一段時間後纔開始消費。常見的使用場景是交易系統中下單超時未支付的訂單需要讓其失效。爲啥實現延時隊列有兩種方式呢?又是哪兩種呢?
第一種是TTL隊列+死信隊列,第二種就是叫延遲隊列。爲啥有了第二種還需要第一種,我猜想是因爲一開始並沒有第二種,當延遲隊列的需求確實很多了,官方纔提供了延遲隊列的插件。默認的rabbitmq是沒有延遲隊列的。需要爲其單獨安裝插件。
上文剛剛提到了TTL隊列,
什麼是ttl呢?
其全稱是time to live ,也就是隊列裏面的數據存在生存週期,如果生存週期內數據未被消費掉,那麼,消息將被自動刪除,當然如果ttl隊列關聯死信隊列,則可以將ttl隊列的消息轉移至死信隊列。
什麼又是死信隊列?
死信隊列專門用於作爲其他隊列的協助隊列,當主隊列的消息不能被正常消費,或者主隊列的消息過期了,則消息自動轉到死信隊列。
如何實現第一種延遲隊列呢?設置兩個隊列,隊列1(TTL隊列)先收到生產者發來的消息,然而並沒有消費者會消費隊列1的消息,直到隊列1消息過期,消息被轉移到隊列1關聯的隊列2(死信隊列)而這個死信隊列是指定了消費者的,所以消息一旦轉移到死信隊列,則立即被消費。如下圖
代碼上如何實現?
/**
* TTL交換機配置
*/
@Bean
DirectExchange ttlDirectExchange() {
return (DirectExchange) ExchangeBuilder
.directExchange(QueueEnum.MESSAGE_TTL_QUEUE.getExchange())
.durable(true)
.build();
}
/**
* 定義TTL隊列
* 注意這個ttl隊列申明的時候指定了死信交換機
*/
@Bean
Queue ttlQueue() {
return QueueBuilder
.durable(QueueEnum.MESSAGE_TTL_QUEUE.getQueueName())
// 配置到期後轉發的交換
.withArgument("x-dead-letter-exchange", QueueEnum.MESSAGE_DEAD_QUEUE.getExchange())//注意這裏綁定了死信交換機
// 配置到期後轉發的路由鍵
.withArgument("x-dead-letter-routing-key", QueueEnum.MESSAGE_DEAD_QUEUE.getRouteKey())
//注意這裏綁定了死信隊列
.build();
}
// 死信交換機和死信隊列
/**
* 死信消息交換機配置
*/
@Bean
DirectExchange deadDirectExchange() {
return (DirectExchange) ExchangeBuilder
.directExchange(QueueEnum.MESSAGE_DEAD_QUEUE.getExchange())
.durable(true)
.build();
}
/**
* 定義死信隊列
*/
@Bean
public Queue deadQueue() {
return new Queue(QueueEnum.MESSAGE_DEAD_QUEUE.getQueueName());
}
/**
* 死信隊列和死信交換機的綁定-routekey
* @param deadDirectExchange 消息中心交換配置
* @param deadQueue 消息中心隊列
*/
@Bean
Binding messageBinding(DirectExchange deadDirectExchange, Queue deadQueue) {
return BindingBuilder
.bind(deadQueue)
.to(deadDirectExchange)
.with(QueueEnum.MESSAGE_DEAD_QUEUE.getRouteKey());
}
/**
* ttl隊列和ttl交換機的綁定-routekey
* @param ttlQueue
* @param ttlDirectExchange
*/
@Bean
public Binding messageTtlBinding(Queue ttlQueue, DirectExchange ttlDirectExchange) {
return BindingBuilder
.bind(ttlQueue)
.to(ttlDirectExchange)
.with(QueueEnum.MESSAGE_TTL_QUEUE.getRouteKey());
}
上文提到的都是如何生產消息,還沒有提到如何消費消息。
@RabbitListener(queues = QueueName.COMMON_QUEUE_NAME)//指定隊列名字即可,如果是延遲隊列,這裏應該指定死信隊列的名字
public void handler(MessageContent messageContent, Channel channel, Message message) throws IOException {
log.debug("BaseConsumer,消息內容:{}", JSON.toJSONString(messageContent));
if (messageContent != null) {
//做業務邏輯
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//消費完後回覆mq,mq則會從待確認隊列中刪除這個消息。如此來保證整體的可靠性。
log.debug("BaseConsumer,消息內容:{}", JSON.toJSONString(messageContent));
}
}
以上我們分析完了第一種延遲隊列的實現,現在我們看看第二種,第二種就更簡單了。
/**
* 定義延遲隊列
* @return
*/
@Bean
public Queue delayQueue(){
return new Queue(QueueEnum.MESSAGE_DELAY_QUEUE.getQueueName());
}
/**
* 延遲消息交換機配置
* @return
*/
@Bean
CustomExchange delayExchange(){
Map<String,Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(QueueEnum.MESSAGE_DELAY_QUEUE.getExchange(), "x-delayed-message", true, false, args);
}
//綁定
@Bean
public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
return BindingBuilder.bind(delayQueue).to(delayExchange).with(QueueEnum.MESSAGE_DELAY_QUEUE.getRouteKey()).noargs();
}
至此延遲隊列申明就完成,接下來的消費,就和普通隊列完全一致了。同時我們對於延遲隊列這種特殊隊列的介紹也暫時完成,爲此我們引入了TTL隊列,死信隊列。其實這倆隊列都是可以單獨使用的,並不是完全爲了延遲隊列而生。
作爲一箇中間件,我們總是要充分考慮其可用性,可靠性。那麼整個過程中,是如何保證生產的消息一定會被消費呢?
其實rabbitmq提供了事務的方式和確認機制,兩種方式來保證消費的可靠性。第一個由於性能太低我們就不介紹了。我們主要介紹確認機制。確認機制其實是包括好幾種的,第一種是同步確認機制,第二種是異步確認機制。
首先是生產者,發送出去的消息會有一個回調通知,通知生產者消息是否被mq接收。
/**
* 用於實現消息發送到RabbitMQ交換器後接收ack回調
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
logger.debug("CommonProvider消息到達exchange成功,{}", correlationData == null ? cause : correlationData);
} else {
logger.debug("CommonProvider消息到達exchange失敗,{}", correlationData == null ? cause : correlationData);
}
}
/**
* 用於實現消息發送到RabbitMQ交換器,但無相應隊列與交換器綁定時的回調。
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
if (!QueueEnum.MESSAGE_DELAY_QUEUE.getExchange().equals(exchange)) {
logger.error("CommonProvider發送失敗,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息體:{}",
replyCode, replyText, exchange, exchange, routingKey, JSON.toJSONString(message));
}
}
@Override
public void afterPropertiesSet() throws Exception {
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this); //配置回調
this.rabbitTemplate.setReturnCallback(this);//配置回調
}
現在生產者已經放心了,自己發出去的消息有保證了。那麼mq和消費者之間有哪些操作呢?
@RabbitListener(queues = QueueName.COMMON_QUEUE_NAME)
public void handler(MessageContent messageContent, Channel channel, Message message) throws IOException {
log.debug("BaseConsumer,消息內容:{}", JSON.toJSONString(messageContent));
if (messageContent != null) {
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//通知mq,消費者已經拿到消息。
log.debug("BaseConsumer,消息內容:{}", JSON.toJSONString(messageContent));
}
}
channel.basicAck 通知mq,消費者已經拿到消息。
當然我們上面提到的僅是一種確認機制,能輔助三方之間溝通消息的發送,接收,消費狀態。要提高整個系統的高可用,還得考慮很多其他方面。比如mq本身需要滿足高可用(集羣方式),還有消息&隊列&交換機等實例在mq中要考慮將其持久化。這裏就不深入討論。
本文涉及到的代碼可以在git上查看