目錄
前言
由於工作中沒有用到RabbitMQ,所以只能本地謝謝HelloWorld這樣的代碼,自己主動去了解一下,學習RabbitMQ先從這一章開始了
概念介紹
VirtualHost
RabbitMq的VirtualHost(虛擬消息服務器),每個VirtualHost相當於一個相對獨立的RabbitMQ服務器;每個VirtualHost之間是相互隔離的,exchange、queue、message不能互通。 拿數據庫(用MySQL)來類比:RabbitMq相當於MySQL,RabbitMq中的VirtualHost就相當於MySQL中的一個庫。
Connection
是RabbitMQ的socket鏈接,它封裝了socket協議相關部分邏輯。生產者和消費者都需要和RabbitMQ相連接,所以生產者和消費者不是直連的。
ConnectionFactory
ConnectionFactory爲Connection的製造工廠。
Channel
Channel是我們與RabbitMQ打交道的最重要的一個接口,我們大部分的業務操作是在Channel這個接口中完成的,包括定義Queue、定義Exchange、綁定Queue與Exchange、發佈消息等。
Queue
Queue(隊列)是RabbitMQ的內部對象,用於存儲消息。多個消費者可以訂閱同一個Queue,這時Queue中的消息會被平均分攤給多個消費者進行處理,而不是每個消費者都收到所有的消息並處理。
Message幾個關鍵字段
Message acknowledgment
消息回執確認,當消費者收到消息後可以發送一個確認消息給生產者,這個是爲了防止消息丟失。在實際應用中,可能會發生消費者收到Queue中的消息,但沒有處理完成就宕機(或出現其他意外)的情況,這種情況下就可能會導致消息丟失。爲了避免這種情況發生,我們可以要求消費者在消費完消息後發送一個回執給RabbitMQ,RabbitMQ收到消息回執(Message acknowledgment)後纔將該消息從Queue中移除;如果RabbitMQ沒有收到回執並檢測到消費者的RabbitMQ連接斷開,則RabbitMQ會將該消息發送給其他消費者(如果存在多個消費者)進行處理。這裏不存在timeout概念,一個消費者處理消息時間再長也不會導致該消息被髮送給其他消費者,除非它的RabbitMQ連接斷開。 這裏會產生另外一個問題,如果我們的開發人員在處理完業務邏輯後,忘記發送回執給RabbitMQ,這將會導致嚴重的bug——Queue中堆積的消息會越來越多;消費者重啓後會重複消費這些消息並重復執行業務邏輯。
channel.basicAck(envelope.getDeliveryTag(),false);
durable
如果我們希望即使在RabbitMQ服務重啓的情況下,也不會丟失消息,我們可以將Queue與Message都設置爲可持久化的(durable),這樣可以保證絕大部分情況下我們的RabbitMQ消息不會丟失。但依然解決不了小概率丟失事件的發生(比如RabbitMQ服務器已經接收到生產者的消息,但還沒來得及持久化該消息時RabbitMQ服務器就斷電了),如果我們需要對這種小概率事件也要管理起來,那麼我們要用到事務。
1、exchange持久化,在聲明時指定durable => true
2、queue持久化,在聲明時指定durable => true
3、消息持久化,在投遞時指定delivery_mode=> 2(1是非持久化)
如果exchange和queue都是持久化的,那麼它們之間的binding也是持久化的。如果exchange和queue兩者之間有一個持久化,一個非持久化,就不允許建立綁定。
prefetchCount
prefetchCount這個值是在消費者端收集的。如果有多個消費者同時訂閱同一個Queue中的消息,Queue中的消息會被平攤給多個消費者。這時如果每個消息的處理時間不同,就有可能會導致某些消費者一直在忙,而另外一些消費者很快就處理完手頭工作並一直空閒的情況。我們可以通過設置prefetchCount來限制Queue每次發送給每個消費者的消息數,比如我們設置prefetchCount=1,則Queue每次給每個消費者發送一條消息;消費者處理完這條消息後Queue會再給該消費者發送一條消息。這樣就會當快速處理完消息的消費者能很快再次取得消息。
// 同一時刻服務器只會發一條消息給消費者
channel.basicQos(1)
Exchange
Exchange是一個交換機,負責連接生產者和隊列,並能根據不同規則來選擇傳給不同的隊列。
Binding
RabbitMQ中通過Binding將Exchange與Queue關聯起來,這樣RabbitMQ就知道如何正確地將消息路由到指定的Queue了。
routing key
在生產者將消息傳遞給Exchange的時候,也會傳遞一個routingkey,這個routingkey來指定路由選擇。當然它只是消息路由選擇的一部分,還不完整。routing key需要與Exchange Type及binding key聯合使用才能最終生效。 在Exchange Type與binding key固定的情況下(在正常使用時一般這些內容都是固定配置好的),我們的生產者就可以在發送消息給Exchange時,通過指定routing key來決定消息流向哪裏。 RabbitMQ爲routing key設定的長度限制爲255 bytes。
binding key
在綁定(Binding)Exchange與Queue的同時,一般會指定一個binding key;消費者將消息發送給Exchange時,一般會指定一個routing key;當binding key與routing key相匹配時,消息將會被路由到對應的Queue中。
Exchange Type
Exchange Type是指定routingkey和bindingkey以何種規則進行匹配。
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers這四種
fanout類型的Exchange路由規則非常簡單,它會把所有發送到該Exchange的消息路由到所有與它綁定的Queue中。 相當於忽略了routingkey和bindingkey的匹配,可以看作是不設防。
direct類型的Exchange路由規則也很簡單,它會把消息路由到那些binding key與routing key完全匹配的Queue中。
topic是一種模糊匹配,裏面有兩個關鍵符號:#和*,#代表匹配零個字符或多個字符,*代表匹配一個字符。
headers類型的Exchange不依賴於routing key與binding key的匹配規則來路由消息,而是根據發送的消息內容中的headers屬性進行匹配。 在綁定Queue與Exchange時指定一組鍵值對;當消息發送到Exchange時,RabbitMQ會取到該消息的headers(也是一個鍵值對的形式),對比其中的鍵值對是否完全匹配Queue與Exchange綁定時指定的鍵值對;如果完全匹配則消息會路由到該Queue,否則不會路由到該Queue。
RPC
MQ本身是基於異步的消息處理,前面的示例中所有的生產者(P)將消息發送到RabbitMQ後不會知道消費者(C)處理成功或者失敗(甚至連有沒有消費者來處理這條消息都不知道)。 但實際的應用場景中,我們很可能需要一些同步處理,需要同步等待服務端將我的消息處理完成後再進行下一步處理。這相當於RPC(Remote Procedure Call,遠程過程調用)。在RabbitMQ中也支持RPC。
RabbitMQ 中實現RPC 的機制是:
-
客戶端發送請求(消息)時,在消息的屬性(MessageProperties ,在AMQP 協議中定義了14中properties ,這些屬性會隨着消息一起發送)中設置兩個值replyTo (一個Queue 名稱,用於告訴服務器處理完成後將通知我的消息發送到這個Queue 中)和correlationId (此次請求的標識號,服務器處理完成後需要將此屬性返還,客戶端將根據這個id瞭解哪條請求被成功執行了或執行失敗)
-
服務器端收到消息並處理
-
服務器端處理完消息後,將生成一條應答消息到replyTo 指定的Queue ,同時帶上correlationId 屬性
-
客戶端之前已訂閱replyTo 指定的Queue ,從中收到服務器的應答消息後,根據其中的correlationId 屬性分析哪條請求被執行了,根據執行結果進行後續業務處理
自己的理解
生產者,消費者,RabbitMQ服務器中的VirtualHost分爲這三部分。
生產者和服務器,可以直接連接,也可以由exchange相連,但是exchange需要和queue綁定。
消費者和服務器之間是服務器將消息從queue裏面取出來後傳給了消費者
routing key binding key ExchangeType三者之間的關係
routing key是生產者發送消息的時候帶上的,相當於通行證,
binding key 是exhcange和queue綁定的時候設置的,這相當於一個門,
ExhcangeType相當於是門檢查通行證的一種規則,常見的由鬆到嚴分爲三種
fanout,直接忽略routing key和binding key,消息會分發給所有和exchange相連的duilie
direct,完全匹配,要routing key 和binding key完全一樣,纔給分發
topic,模糊匹配,關鍵字#和*,#匹配零到多個,*是匹配一個字符。
代碼介紹
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.findjob</groupId>
<artifactId>RabbitMQTest</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.2.0</version>
</dependency>
</dependencies>
</project>
ConnectionUtil
package com.wangbiao.rabbitmq.helloworld;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
*連接工具
*/
public class ConnectionUtil {
private static final String HOST="127.0.0.1";
private static final int PORT=5672;
private static final String USER_NAME="test";
private static final String PASSWORD="test";
private static final String VIRHOST="/test";
private static ConnectionFactory connectionFactory = new ConnectionFactory();
public static Connection getConnection(){
connectionFactory.setHost(HOST);
connectionFactory.setPort(PORT);
connectionFactory.setUsername(USER_NAME);
connectionFactory.setPassword(PASSWORD);
connectionFactory.setVirtualHost(VIRHOST);
try {
Connection connection = connectionFactory.newConnection();
return connection;
}catch (Exception exception){
exception.printStackTrace();
}
return null;
}
}
生產者
package com.wangbiao.rabbitmq.exchangetest;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wangbiao.rabbitmq.helloworld.ConnectionUtil;
/**
* @Author wangbiao
* @Date 2019-01-19 17:24
* @Decripition TODO
**/
public class ProductorTwo {
private static Connection connection = ConnectionUtil.getConnection();
private static final String EXCHANGE_NAME = "exchangeTest";
private static final String ROUTING_KEY = "fighting";
public static void main(String[] args) throws Exception{
Channel channel = connection.createChannel();
/**
* exchange: 交換器名稱
* type : 交換器類型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
* durable: 是否持久化,durable設置爲true表示持久化,反之是非持久化,持久化的可以將交換器存盤,在服務器重啓的時候不會丟失信息.
* autoDelete是否自動刪除,設置爲TRUE則表是自動刪除,自刪除的前提是至少有一個隊列或者交換器與這交換器綁定,之後所有與這個交換器綁定的隊列或者交換器都與此解綁,一般都設置爲fase
* 交換機在不被使用時是否刪除
* arguments:其它一些結構化參數比如:alternate-exchange
*/
channel.exchangeDeclare(EXCHANGE_NAME,"topic",false,false,null);
for(int i=0;i<10;i++){
String message = "Hello World"+i;
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,null,message.getBytes());
}
System.out.println("生產者發送消息");
channel.close();
connection.close();
}
}
消費者二
package com.wangbiao.rabbitmq.exchangetest;
import com.rabbitmq.client.*;
import com.wangbiao.rabbitmq.helloworld.ConnectionUtil;
import java.io.IOException;
/**
* @Author wangbiao
* @Date 2019-01-19 18:49
* @Decripition TODO
**/
public class CustomerTwo {
private static Connection connection = ConnectionUtil.getConnection();
private static final String QUEUE_NAME = "queueTest";
private static final String EXCHANGE_NAME = "exchangeTest";
private static final String BINDING_KEY = "fighting";
public static void main(String[] args) throws Exception{
final Channel channel = connection.createChannel();
/**
* 隊列不存在就創建隊列,如果隊列存在什麼都不做
* 第一個參數:隊列名字
* 第二個參數:是否持久化,隊列裏的消息是保存在內存中的,rabbitmq重啓會丟失,如果是ture,隊列裏的消息會保存到erlang自帶的數據庫中
* 第三個參數:是否排外,有兩個作用,連接關閉是否清除隊列;是否私有當前隊列,如果私有當前隊列,其他通道不可以訪問當前隊列
* 爲ture的話,一般適用於一個隊列對應一個消費者的時候
* 第四個參數:是否自動刪除
* 第五個參數:其他參數
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,BINDING_KEY);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("收到的消息:"+message);
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
channel.basicConsume(QUEUE_NAME,false,consumer);
System.out.println("=============消費者二接受完消息==============");
}
}
消費者三
package com.wangbiao.rabbitmq.exchangetest;
import com.rabbitmq.client.*;
import com.wangbiao.rabbitmq.helloworld.ConnectionUtil;
import java.io.IOException;
/**
* @Author wangbiao
* @Date 2019-01-19 18:49
* @Decripition TODO
**/
public class CustomerThree {
private static Connection connection = ConnectionUtil.getConnection();
private static final String QUEUE_NAME = "queueTestTwo";
private static final String EXCHANGE_NAME = "exchangeTest";
private static final String BINDING_KEY = "fig#";
public static void main(String[] args) throws Exception{
final Channel channel = connection.createChannel();
/**
* 隊列不存在就創建隊列,如果隊列存在什麼都不做
* 第一個參數:隊列名字
* 第二個參數:是否持久化,隊列裏的消息是保存在內存中的,rabbitmq重啓會丟失,如果是ture,隊列裏的消息會保存到erlang自帶的數據庫中
* 第三個參數:是否排外,有兩個作用,連接關閉是否清除隊列;是否私有當前隊列,如果私有當前隊列,其他通道不可以訪問當前隊列
* 爲ture的話,一般適用於一個隊列對應一個消費者的時候
* 第四個參數:是否自動刪除
* 第五個參數:其他參數
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,BINDING_KEY);
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("消費者三收到的消息:"+message);
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
啓動生產者
消費者二和消費者三輸出的結果是
總結
我們可以將隊列服務器Broker看成是mysql服務器,將虛擬主機vhost看成是數據庫,隊列Queue看成是數據庫裏的表,生產者發佈的消息可以看成是表裏的記錄。
RabbitMQ工作的過程是:
1.生產者端連接上隊列服務器上的一個虛擬主機vhost。
2.再創建一個Channel,也可以創建多個Channel,Channel負責定義Exchange,Queue,綁定Exchange和Queue,發佈消息,綁定消費者和隊列。
3.通過Channel聲明Exchange,然後生產者會將消息發佈給Exchange,在聲明Exchange的時候可以設置一個校驗規則。
4.生產者通過Channel在發佈消息的時候會將消息發佈給Exchange,在發佈消息的時候,可以帶上routing key。
5.消費者這邊先連上vhost,然後聲明出一個隊列,再將隊列和Exchange綁定,綁定的時候可以設置校驗routingkey的bindingkey。
6.消費者這邊還得聲明一個消費對象Consumer,這個Consumer還得實現handleDevlivery方法來處理隊列裏面的消息。
7.channel就可以消費對象消費隊列Queue裏面的消息了。