在RabbitMQ(一)——入門中我們簡單的介紹了一下RabbitMQ的入門知識,這節我們針對RabbitMQ的幾種常見的工作模式展開來講解一下。RabbitMQ有以下幾種工作模式 :
目錄
一、簡單模式
這種模式是RabbitMQ中最簡單的一種工作模式,我們在RabbitMQ(一)——入門中“RabbitMQ入門測試”編寫的案例就是這種模式。它只要有一個消費端、一個生產端和一個指定的隊列。具體的代碼演示可以返回去看上一節的內容,這裏就不在贅述了。
二、工作隊列模式
工作隊列模式與簡單模式相比,僅僅是多了一個(或多個)消費端,多個消費端共同消費同一個隊列中的消息。對於任務過重或任務較多情況使用工作隊列可以提高任務處理的速度,因爲對於多個任務,MQ可以把它們分別交給不同的消費端同時進行處理,這樣就提高了任務處理的效率。
在這種模式下RabbitMQ會採用“輪詢”的方式將消息隊列中的消息平均地發送給每個消費者(假如有n個消費者,則第一條消息發給第一個消費者,第二條消息發給第二個消費者......第n條消息發給第n個消費者,第n+1條消息發給第1個消費者),並且一條消息只會被一個消費者接收,不會存在同一條消息被兩個或以上的消費者都接受到的情況,當消費者在處理完某條消息後,纔會收到下一條消息。
三、發佈/訂閱模式
此種模式下生產者將消息發給broker,broker中的由交換機將消息轉發到綁定到此交換機上的每個隊列中(假如交換機收到10條消息,它會把這10條消息分別發給每個隊列,如下圖兩個隊列中,每個都會收到相同的10條消息),而消費者這邊去監聽自己的隊列。
那發佈/訂閱模式和上面的工作隊列模式有什麼異同嗎?主要有以下幾點
不同點:
- work queues不用定義交換機,而publish/subscribe需要定義交換機。
- publish/subscribe的生產方是面向交換機發送消息,work queues的生產方是面向隊列發送消息(底層使用默認交換機)。
- publish/subscribe需要設置隊列和交換機的綁定,work queues不需要設置,實質上work queues會將隊列綁定到默認的交換機 。
相同點:
- 同工作隊列工作模式一樣,發佈訂閱模式也可以實現多個消費端共同監聽同一個隊列,並且不會重複消費消息的功能。從這一點可以看出發佈訂閱模其實比工作隊列模式功能更強大,因爲工作隊列模式可以實現的功能,發佈訂閱模式也完全可以實現,並且發佈訂閱模式可以指定自己專用的交換機,而工作隊列模式只能使用默認的交換機,所以在實際工作中發佈訂閱模式用的更多一些。
下面通過代碼來具體的看一下這種模式(代碼是在RabbitMQ(一)——入門中的“RabbitMQ入門測試”的工程基礎上進行編寫)。我們設想這樣一種場景:用戶通知,當用戶充值成功或轉賬完成後,系統通過“短信”和“郵件”這兩種方式通知用戶交易完成,這樣用戶就可以在手機短信上和電子郵箱中同時收到交易完成的提醒了。
3.1生產者
聲明Exchange_fandout_inform交換機,並且聲明兩個隊列並且綁定到此交換機上。
public class Producer_Publish {
//隊列名稱
//專門用於接收郵箱通知的隊列
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
//專門用於接收短信通知的隊列
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
//交換機
private static final String EXCHANGE_FANOUT_INFORM="exchange_fanout_inform";
public static void main(String[] args) {
Connection connection = null;
Channel channel = null;
try {
//通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
//建立新連接
connection = connectionFactory.newConnection();
//創建與交換機的通道,每個通道代表一個會話
channel = connection.createChannel();
//聲明交換機
/**
* 參數明細:String exchange, BuiltinExchangeType type
* exchange:交換機名稱,這個我們在上面已經定義好了
* type:有四種類型fanout、topic、direct、headers,發佈訂閱工作模式下選擇fanout
*/
channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM,BuiltinExchangeType.FANOUT);
//聲明隊列
/**
* 參數明細:(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String,Object> arguments)
* String queue:隊列名稱
* boolean durable:是否持久化
* boolean exclusive:是否獨佔此隊列
* boolean autoDelete:隊列不用是否自動刪除
* Map<String,Object> arguments:參數,可以設置一個隊列的擴展參數,比如:可設置存活時間等
*/
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
//將交換機和隊列綁定
/**
* 參數明細:String queue, String exchange, String routingKey
* String queue:隊列名稱
* String exchange:交換機名稱
* String routingKey:路由key,這個我們暫時用不到爲空即可,在路由模式中會講解
*/
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
//發送消息
for (int i = 0;i < 10;i++){
String message = "inform to user"+i;
//向交換機發送消息
/**
* 參數明細:String exchange, String routingKey, BasicProperties props,byte[] body
* String exchange:交換機名稱,不指定的話就會使用默認交換機Default Exchange
* tring routingKey:routingKey(路由key),根據key名稱將消息轉發到具體的隊列,暫時用不到。
* 如果這個字段填寫隊列名,則稱表示消息將發到此隊列
* BasicProperties props:消息屬性
* byte[] body:消息內容
*/
channel.basicPublish(EXCHANGE_FANOUT_INFORM,"",null,message.getBytes());
System.out.println("Send Message is:'"+message+"'");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//關閉連接
//先關閉通道
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.2消費者
1)郵件發送消費者
public class Consumer_Subscribe_Email {
//隊列名稱
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String EXCHANGE_FANOUT_INFORM="inform_exchange_fanout";
public static void main(String[] args) throws IOException, TimeoutException {
//創建一個與MQ的連接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
//創建一個連接
Connection connection = factory.newConnection();
//創建與交換機的通道,每個通道代表一個會話
Channel channel = connection.createChannel();
//聲明交換機
channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM,BuiltinExchangeType.FANOUT);
//聲明隊列,只監聽郵件的對列即可
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
//交換機和隊列綁定
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
//定義消費方法,這裏就是接收到MQ中對列發來的消息後需要做的事情,即把收到的信息發給用戶的郵箱
//我們這裏就簡單的把收到的消息打印出來即可
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
String exchange = envelope.getExchange();
//消息內容
String message = new String(body, "utf‐8");
System.out.println(message);
}
};
//監聽隊列
/**
* 參數明細:String queue, boolean autoAck,Consumer callback
* String queue:隊列名稱
* boolean autoAck:是否自動回覆,設置爲true爲表示消息接收到自動向mq回覆接收到了,
* mq接收到回覆會刪除消息,設置爲false則需要手動回覆
* Consumer callback:消費消息的方法,消費者接收到消息後調用此方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
2)短信發送消費者
短信發送消費者的代碼幾乎同上邊郵件發送消費者的代碼一模一樣,我們只需把需要綁定和監聽的隊列換爲QUEUE_INFORM_EMAIL即可
public class Consumer_Subscribe_SMS {
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_FANOUT_INFORM="inform_exchange_fanout";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM,BuiltinExchangeType.FANOUT);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
long deliveryTag = envelope.getDeliveryTag();
String exchange = envelope.getExchange();
String message = new String(body, "utf-8");
System.out.println(message);
}
};
channel.basicConsume(QUEUE_INFORM_SMS,true,defaultConsumer);
}
}
3.3測試
運行Producer_Publish類後,打開RabbitMQ的管理後臺可以發現一共有20條待發送的消息(每個對列中10條),並多了兩個隊列。之後我們分別運行Consumer_Subscribe_Email和Consumer_Subscribe_SMS,MQ就會把消息發給這二者。
四、路由模式
路由模式相較於發佈訂閱模式的不同點在於,路由模式要求隊列在綁定交換機的時候要指定routingkey,這個routingkey的主要作用就是讓交換機根據不同的routingkey把消息發送到不同的隊列中。不像發佈訂閱模式那樣,如果交換機收到了10條消息,它會把這10條消息不加篩選的一股腦全部發給每個隊列。而對於路由模式,生產者將消息發給交換機,由交換機會根據routingkey來轉發消息到指定的隊列,同時每個消費者也會監聽自己的隊列,並且設置相應的routingkey進而只接收指定隊列發來的消息。
下面我們依舊通過代碼來演示一下:
4.1生產者
聲明exchange_routing_inform交換機;聲明兩個隊列並且綁定到此交換機,綁定時需要指定routingkey;發送消息時需要指定routingkey。
public class Producer_Routing {
//隊列名稱
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
//交換機名稱
private static final String EXCHANGE_ROUTING_INFORM="exchange_routing_inform";
//routingKey
private static final String ROUTINGKEY_EMAIL="inform_email";
private static final String ROUTINGKEY_SMS="inform_sms";
public static void main(String[] args) {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);//端口
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.newConnection();
channel = connection.createChannel();
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
//聲明一個交換機
/**
* 參數明細:String exchange, String type
* 1、交換機的名稱
* 2、交換機的類型
* fanout:對應的rabbitmq的工作模式是 publish/subscribe
* direct:對應的Routing 工作模式
* topic:對應的Topics工作模式
* headers: 對應的headers工作模式
*/
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
//進行交換機和隊列綁定
/**
* 參數明細:String queue, String exchange, String routingKey
* 1、queue 隊列名稱
* 2、exchange 交換機名稱
* 3、routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列中,在發佈訂閱模式中調協爲空字符串
*/
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS);
//發送消息
/**
* 參數明細:String exchange, String routingKey, BasicProperties props, byte[] body
* 1、exchange,交換機,如果不指定將使用mq的默認交換機(設置爲"")
* 2、routingKey,路由key,交換機根據路由key來將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
* 3、props,消息的屬性
* 4、body,消息內容
*/
for(int i=0;i<10;i++){
//發送消息的時候指定routingKey
if(i < 5){
String message = "send inform message to email";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL,null,message.getBytes());
System.out.println("send to mq "+message);
}else {
String message = "send inform message to sms";
channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS,null,message.getBytes());
System.out.println("send to mq "+message);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4.2消費者
1)郵件發送消費者
public class Consumer_Routing_Email {
//隊列名稱
private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
//交換機名稱
private static final String EXCHANGE_ROUTING_INFORM="exchange_routing_inform";
//routingkey
private static final String ROUTINGKEY_EMAIL="inform_email";
public static void main(String[] args) throws IOException, TimeoutException {
//通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);//端口
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
//建立新連接
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
//聲明一個交換機
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
//進行交換機和隊列綁定
/**
* 參數明細:String queue, String exchange, String routingKey
* 1、queue 隊列名稱
* 2、exchange 交換機名稱
* 3、routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列中,在發佈訂閱模式中調協爲空字符串
*/
channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
//實現消費方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
/**
* 當接收到消息後此方法將被調用
* @param consumerTag 消費者標籤,用來標識消費者的,在監聽隊列時設置channel.basicConsume
* @param envelope 信封,通過envelope
* @param properties 消息屬性
* @param body 消息內容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交換機
String exchange = envelope.getExchange();
//消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
long deliveryTag = envelope.getDeliveryTag();
//消息內容
String message= new String(body,"utf-8");
System.out.println("receive message:"+message);
}
};
//監聽隊列
/**
* 參數明細:String queue, boolean autoAck, Consumer callback
* 1、queue 隊列名稱
* 2、autoAck 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲tru表示會自動回覆mq,如果設置爲false要通過編程實現回覆
* 3、callback,消費方法,當消費者接收到消息要執行的方法
*/
channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
}
}
2)短信發送消費者
public class Consumer_Routing_SMS {
private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
private static final String EXCHANGE_ROUTING_INFORM="exchange_routing_inform";
private static final String ROUTINGKEY_SMS="inform_sms";
public static void main(String[] args) throws IOException, TimeoutException {
//通過連接工廠創建新的連接和mq建立連接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("127.0.0.1");
connectionFactory.setPort(5672);//端口
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS);
//實現消費方法
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交換機
String exchange = envelope.getExchange();
//消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
long deliveryTag = envelope.getDeliveryTag();
//消息內容
String message= new String(body,"utf-8");
System.out.println("receive message:"+message);
}
};
channel.basicConsume(QUEUE_INFORM_SMS,true,defaultConsumer);
}
}
五、主題模式
主題工作模式其實和路由模式很相似,在交換機和隊列進行綁定的時候也需要指定routingkey,只不過主題模式中的routingkey可以帶有通配符,而路由模式的routingkey必須精確指定。另外一點不同的地方就是交換機的type類型,路由模式的交換機type類型爲direct,而主題模式的交換機type類型爲topic。但Topic模式功能更多加強大,它可以實現Routing、publish/subscirbe模式的功能。
下面我們將上面路由模式中的應用場景稍作需改,根據用戶的通知設置去通知用戶交易信息:設置Email通知的用戶只通過Email接收交易信息,設置SMS通知的用戶只通過SMS接收交易信息,設置兩種通知類型都接收的則兩種通知都有效。
因爲這部分的代碼與上面路由模式的代碼很相識,這裏就不給出完整代碼,只貼出部分代碼:
5.1生產者
/**
* 聲明交換機
* param1:交換機名稱
* param2:交換機類型 四種交換機類型:direct、fanout、topic、headers
*/
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
//Email通知
channel.basicPublish(EXCHANGE_TOPICS_INFORM, "inform.email", null, message.getBytes());
//sms通知
channel.basicPublish(EXCHANGE_TOPICS_INFORM, "inform.sms", null, message.getBytes());
//兩種都通知
channel.basicPublish(EXCHANGE_TOPICS_INFORM, "inform.sms.email", null, message.getBytes());
5.2消費者
隊列綁定交換機指定通配符:
統配符規則:中間以“.”分隔;符號#可以匹配多個詞,符號*可以匹配一個詞語。
//聲明隊列
channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null);
channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null);
//聲明交換機
channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
//綁定email通知隊列
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,"inform.#.email.#");
//綁定sms通知隊列
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,"inform.#.sms.#");
5.3總結
其實本案例的需求使用Routing工作模式也可以實現,但是一共需要設置三個 routingkey,分別是email、sms、all。email隊列綁定email和
all,sms隊列綁定sms和all,這樣就可以實現上邊案例的功能,實現過程比topics複雜一些。
六、Header模式
header模式與routing不同的地方在於,header模式取消了routingkey,使用header中的 key/value(鍵值對)匹配
隊列。
我們還是以上面的案例爲基礎,根據用戶的通知設置去通知用戶,設置接收Email的用戶只接收Email,設置接收sms的用戶只接收sms。
下面給出部分代碼片段:
6.1生產者
隊列與交換機綁定的代碼與之前不同,如下:
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_type", "email");
Map<String, Object> headers_sms = new Hashtable<String, Object>();
headers_sms.put("inform_type", "sms");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_HEADERS_INFORM,"",headers_sms);
通知部分的代碼(只給出郵件部分,短信部分類似):
String message = "email inform to user"+i;
Map<String,Object> headers = new Hashtable<String, Object>();
headers.put("inform_type", "email");//匹配email通知消費者綁定的header
//headers.put("inform_type", "sms");//匹配sms通知消費者綁定的header
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
properties.headers(headers);
//Email通知
channel.basicPublish(EXCHANGE_HEADERS_INFORM, "", properties.build(), message.getBytes());
6.2.消費者(只給出郵件部分,短信部分類似):
channel.exchangeDeclare(EXCHANGE_HEADERS_INFORM, BuiltinExchangeType.HEADERS);
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_email", "email");
//交換機和隊列綁定
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
//指定消費隊列
channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer);
七、RPC模式
這一種模式代碼實現起來比較多,就不給大家展示代碼了
RPC即客戶端遠程調用服務端的方法 ,使用MQ可以實現RPC的異步調用,基於Direct交換機實現,流程如下:
1、客戶端即是生產者也是消費者,向RPC請求隊列發送RPC調用消息,同時監聽RPC響應隊列。
2、服務端監聽RPC請求隊列的消息,收到消息後執行服務端的方法,得到方法返回的結果
3、服務端將RPC方法 的結果發送到RPC響應隊列
4、客戶端(RPC調用方)監聽RPC響應隊列,接收到RPC調用結果。
關於RabbitMQ的工作模式就介紹這麼多吧,謝謝大家的耐心閱讀。