RabbitMQ 簡介
RabbitMQ是一個在AMQP(Advanced Message Queuing Protocol )基礎上實現的,可復用的企業消息系統。它可以用于大型軟件系統各個模塊之間的高效通信,支持高并發,支持可擴展。
AMQP
AMQP,即Advanced Message Queuing Protocol,一個提供統一消息服務的應用層標準高級消息隊列協議,是應用層協議的一個開放標準,為面向消息的中間件設計。基于此協議的客戶端與消息中間件可傳遞消息,并不受客戶端/中間件不同產品,不同的開發語言等條件的限制。
消息隊列
MQ 全稱為Message Queue, 消息隊列。是一種應用程序對應用程序的通信方法。應用程序通過讀寫出入隊列的消息(針對應用程序的數據)來通信,而無需專用連接來鏈接它們。
消息傳遞指的是程序之間通過在消息中發送數據進行通信,而不是通過直接調用彼此來通信。隊列的使用除去了接收和發送應用程序同時執行的要求。
在項目中,將一些無需即時返回且耗時的操作提取出來,進行了異步處理,而這種異步處理的方式大大的節省了服務器的請求響應時間,從而提高了系統的吞吐量。
消息隊列的使用場景是怎樣的?小紅和小明讀書的例子
RabbitMQ 應用場景
對于一個大型的軟件系統來說,它會有很多的組件或者說模塊或者說子系統或者(subsystem or Component or submodule)。那么這些模塊的如何通信?這和傳統的IPC有很大的區別。傳統的IPC很多都是在單一系統上的,模塊耦合性很大,不適合擴展(Scalability);如果使用socket那么不同的模塊的確可以部署到不同的機器上,但是還是有很多問題需要解決。比如:
1)信息的發送者和接收者如何維持這個連接,如果一方的連接中斷,這期間的數據如何方式丟失?
2)如何降低發送者和接收者的耦合度?
3)如何讓Priority高的接收者先接到數據?
4)如何做到load balance?有效均衡接收者的負載?
5)如何有效的將數據發送到相關的接收者?也就是說將接收者subscribe 不同的數據,如何做有效的filter。
6)如何做到可擴展,甚至將這個通信模塊發到cluster上?
7)如何保證接收者接收到了完整,正確的數據?
AMDQ協議解決了以上的問題,而RabbitMQ實現了AMQP。
概念介紹
- Broker:簡單來說就是消息隊列服務器實體。
- Exchange:消息交換機,它指定消息按什么規則,路由到哪個隊列。
- Queue:消息隊列載體,每個消息都會被投入到一個或多個隊列。
- Binding:綁定,它的作用就是把exchange和queue按照路由規則綁定起來。
- Routing Key:路由關鍵字,exchange根據這個關鍵字進行消息投遞。
- vhost:虛擬主機,一個broker里可以開設多個vhost,用作不同用戶的權限分離。
- producer:消息生產者,就是投遞消息的程序。
- consumer:消息消費者,就是接受消息的程序。
- channel:消息通道,在客戶端的每個連接里,可建立多個channel,每個channel代表一個會話任務。
RabbitMQ使用流程
AMQP模型中,消息在producer中產生,發送到MQ的exchange上,exchange根據配置的路由方式發到相應的Queue上,Queue又將消息發送給consumer,消息從queue到consumer有push和pull兩種方式。 消息隊列的使用過程大概如下:
- 客戶端連接到消息隊列服務器,打開一個channel。
- 客戶端聲明一個exchange,并設置相關屬性。
- 客戶端聲明一個queue,并設置相關屬性。
- 客戶端使用routing key,在exchange和queue之間建立好綁定關系。
- 客戶端投遞消息到exchange。
exchange接收到消息后,就根據消息的key和已經設置的binding,進行消息路由,將消息投遞到一個或多個隊列里。 exchange也有幾個類型,完全根據key進行投遞的叫做Direct交換機,例如,綁定時設置了routing key為”abc”,那么客戶端提交的消息,只有設置了key為”abc”的才會投遞到隊列。
RabbitMQ安裝教程
- Windows Linux安裝教程
-
Mac 安裝教程
安裝成功后打開瀏覽器,訪問 http://localhost:15672
rabbitMQ常用的命令
啟動監控管理器:rabbitmq-plugins enable rabbitmq_management
關閉監控管理器:rabbitmq-plugins disable rabbitmq_management
啟動rabbitmq:rabbitmq-service start
關閉rabbitmq:rabbitmq-service stop
查看所有的隊列:rabbitmqctl list_queues
清除所有的隊列:rabbitmqctl reset
關閉應用:rabbitmqctl stop_app
啟動應用:rabbitmqctl start_app
用戶和權限設置
添加用戶:rabbitmqctl add_user username password
分配角色:rabbitmqctl set_user_tags username administrator
新增虛擬主機:rabbitmqctl add_vhost vhost_name
將新虛擬主機授權給新用戶:rabbitmqctl set_permissions -p vhost_name username “.*” “.*” “.*”
(后面三個”*”代表用戶擁有配置、寫、讀全部權限)
角色說明
- 超級管理員(administrator)
可登陸管理控制臺,可查看所有的信息,并且可以對用戶,策略(policy)進行操作。 - 監控者(monitoring)
可登陸管理控制臺,同時可以查看rabbitmq節點的相關信息(進程數,內存使用情況,磁盤使用情況等) - 策略制定者(policymaker)
可登陸管理控制臺, 同時可以對policy進行管理。但無法查看節點的相關信息(上圖紅框標識的部分)。 - 普通管理者(management)
僅可登陸管理控制臺,無法看到節點信息,也無法對策略進行管理。 - 其他
無法登陸管理控制臺,通常就是普通的生產者和消費者。
Java入門實例(Helloworld)
一個producer發送消息,一個接收者接收消息,并在控制臺打印出來。如下圖:
Java客戶端配置
下面是Java客戶端的maven依賴的配置。
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.0.0</version>
</dependency>
發送端:Send.java 連接到RabbitMQ(此時服務需要啟動),發送一條數據,然后退出。
package cn.buyforyou;
import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Send
{
//隊列名稱
private final static String QUEUE_NAME = "helloMQ";
public static void main(String[] argv) throws java.io.IOException, TimeoutException
{
/**
* 創建連接連接到MabbitMQ
*/
ConnectionFactory factory = new ConnectionFactory();
//設置MabbitMQ所在主機ip或者主機名
factory.setHost("localhost");
//創建一個連接
Connection connection = factory.newConnection();
//創建一個頻道
Channel channel = connection.createChannel();
//指定一個隊列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//發送的消息
String message = "hello world!";
//往隊列中發出一條消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//關閉頻道和連接
channel.close();
connection.close();
}
}
值得注意的是隊列只會在它不存在的時候創建,多次聲明并不會重復創建。信息的內容是字節數組,也就意味著你可以傳遞任何數據。
接收端:Recv.java 不斷等待服務器推送消息,然后在控制臺輸出。
package cn.buyforyou;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Recv {
// 隊列名稱
private final static String QUEUE_NAME = "helloMQ";
public static void main(String[] argv) throws Exception {
// 打開連接和創建頻道,與發送端一樣
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//聲明隊列,主要為了防止消息接收者先運行此程序,隊列還不存在時創建隊列。
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//創建消費者
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, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
RabbitMQ工作隊列-Work Queues(Java實例)
創建一個工作隊列用來在工作者(consumer)間分發耗時任務。
工作隊列的主要任務是:避免立刻執行資源密集型任務,然后必須等待其完成。相反地,我們進行任務調度:我們把任務封裝為消息發送給隊列。工作進行在后臺運行并不斷的從隊列中取出任務然后執行。當你運行了多個工作進程時,任務隊列中的任務將會被工作進程共享執行。
這樣的概念在web應用中極其有用,當在很短的HTTP請求間需要執行復雜的任務。
準備
我們使用Thread.sleep來模擬耗時的任務。我們在發送到隊列的消息的末尾添加一定數量的點,每個點代表在工作線程中需要耗時1秒,例如hello…將會需要等待3秒。
發送端:
NewTask.java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
public class NewTask {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
String message = getMessage(argv);
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
private static String getMessage(String[] strings) {
if (strings.length < 1)
return "Hello World!";
return joinStrings(strings, " ");
}
private static String joinStrings(String[] strings, String delimiter) {
int length = strings.length;
if (length == 0) return "";
StringBuilder words = new StringBuilder(strings[0]);
for (int i = 1; i < length; i++) {
words.append(delimiter).append(strings[i]);
}
return words.toString();
}
}
接收端:
Work.java
import com.rabbitmq.client.*;
import java.io.IOException;
public class Worker {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
final 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, "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] Done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
}
private static void doWork(String task) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
循環調度
使用任務隊列的好處是能夠很容易的并行工作。如果我們積壓了很多工作,我們僅僅通過增加更多的工作者就可以解決問題,使系統的伸縮性更加容易。
消息確認
執行一個任務需要花費幾秒鐘。你可能會擔心當一個工作者在執行任務時發生中斷。我們上面的代碼,一旦RabbItMQ交付了一個信息給消費者,會馬上從內存中移除這個信息。在這種情況下,如果殺死正在執行任務的某個工作者,我們會丟失它正在處理的信息。我們也會丟失已經轉發給這個工作者且它還未執行的消息。
boolean ack = false ; //打開應答機制
channel.basicConsume(QUEUE_NAME, ack, consumer);
//另外需要在每次處理完成一個消息后,手動發送一次應答。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
消息的持久性
我們已經學習了即使消費者被殺死,消息也不會被丟失。但是如果此時RabbitMQ服務被停止,我們的消息仍然會丟失。
當RabbitMQ退出或者異常退出,將會丟失所有的隊列和信息,除非你告訴它不要丟失。我們需要做兩件事來確保信息不會被丟失:我們需要給所有的隊列和消息設置持久化的標志。
第一, 我們需要確認RabbitMQ永遠不會丟失我們的隊列。為了這樣,我們需要聲明它為持久化的。
boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);
注:RabbitMQ不允許使用不同的參數重新定義一個隊列,所以已經存在的隊列,我們無法修改其屬性。
第二, 我們需要標識我們的信息為持久化的。通過設置MessageProperties(implements BasicProperties)值為PERSISTENT_TEXT_PLAIN。
channel.basicPublish("", "task_queue",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
現在你可以執行一個發送消息的程序,然后關閉服務,再重新啟動服務,運行消費者程序做下實驗。
公平的分配
或許會發現,目前的消息轉發機制(Round-robin)并非是我們想要的。例如,這樣一種情況,對于兩個消費者,有一系列的任務,奇數任務特別耗時,而偶數任務卻很輕松,這樣造成一個消費者一直繁忙,另一個消費者卻很快執行完任務后等待。
造成這樣的原因是因為RabbitMQ僅僅是當消息到達隊列進行轉發消息。并不在乎有多少任務消費者并未傳遞一個應答給RabbitMQ。僅僅盲目轉發所有的奇數給一個消費者,偶數給另一個消費者。
為了解決這樣的問題,我們可以使用basicQos方法,傳遞參數為prefetchCount = 1。這樣告訴RabbitMQ不要在同一時間給一個消費者超過一條消息。換句話說,只有在消費者空閑的時候會發送下一條信息。
int prefetchCount = 1;
channel.basicQos(prefetchCount);