RabbitMQ
1.RabbitMQ是什麼?
RabbitMQ是一個基於AMQP協議的高級消息中間件,它主要的技術特點是可用性,安全性,集羣,多協議支持,可視化的客戶端,活躍的社區。
2.爲什麼選擇RabbitMQ
- 功能強大,支持死信隊列,優先級隊列,延遲隊列,重試隊列等多種功能無需二次開發。
- 性能相對還算可以,一般單機的QPS在萬級左右,可以滿足一般的應用場景。
- 文檔說明非常豐富,社區活躍,上手容易。
- 強大的可視化管理工具。
3.RabbitMQ模型
Broker(消息代理) : 實際上就是消息服務器實體。
Exchange(交換機) : 用來發送消息的AMQP實體,它指定消息按什麼路由規則,路由到哪個隊列。
Queue(消息隊列) :每個消息都會被投入到一個或多個隊列。
Binding(綁定) : 它的作用就是把交換機(Exchange)和隊列(Queue)按照路由規則綁定起來。
Routing Key(路由關鍵字) :路交換機(Exchange)根據這個關鍵字進行消息投遞。
vhost(虛擬主機) : 虛擬主機,一個消息代理(Broker)裏可以開設多個虛擬主機(vhost),用作不同用戶的權限分離。
Connection(連接) :AMQP連接通常是長連接,Producer和Consumer都是通過TCP連接到RabbitMQ Server的。
Channel(通道) : AMQP通過通道(channels)來處理多連接,可以把通道理解成共享一個TCP連接的多個輕量化連接。
4.交換機(Exchange)和交換機類型
在RabbitMQ中消息並不會被直接投遞到隊列中去,而是有生產者將消息發佈到交換機中,交換機和一個或多個隊列綁定,通過不同的路由規則將消息路由到隊列中去,供消費者消費,RabbitMQ中共提供了四種類型交換機,交換機可以有兩個狀態:持久(durable)、暫存(transient)。持久化的交換機會在消息代理(broker)重啓後依舊存在,而暫存的交換機則不會(它們需要在代理再次上線後重新被聲明)。
- 直連交換機(Direct)
直連型交換機(direct exchange)是根據消息攜帶的路由鍵(routing key)將消息投遞給對應隊列的。直連交換機要求Publisher和Consumer的路由關鍵字(routingKey)完全相同纔會將消息路由到綁定的隊列。直連交換機用來處理消息的單播路由(unicast routing),雖然它可以進行多播。我們用幾行僞代碼來說明它是如何工作的可能更加直觀:
Publisher:
// 路由關鍵字
private static final String[] routingKeys = new String[] {"info", "warn", "error",""debug};
// 聲明一個交換機並指定它的類型爲direct
channel.exchangeDeclare("exchange_direct", "direct")
//發佈消息
for (String routingKey : routingKeys) {
String message = "Send the message : " + severity;
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());
}
Consumer
// 路由關鍵字
private static final String[] routingKeys = new String[] {"info", "warn"};
// 聲明一個交換機並指定它的類型爲direct
channel.exchangeDeclare("exchange_direct", "direct")
// 聲明一個臨時隊列
String queueName = channel.queueDeclare().getQueue();
// 根據路由關鍵字綁定隊列
for (String routingKey : routingKeys) {
channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
}
- 扇形交換機(Faount)
扇型交換機(funout exchange)會將消息路由給綁定到它身上的所有隊列,而不理會綁定的路由鍵,扇型用來交換機處理消息的廣播路由(broadcast routing)。
Publisher
private static final String EXCHANGE_NAME = "exchange.fanout";
// 在發佈端可以不聲明隊列
//channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
for(int i=0;i<5;i++){
String message="this is number"+i;
// 路由關鍵字不能爲null,填寫""
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
System.out.println(" [x] sent ' " + message + " '");
}
Consumer
private static final String EXCHANGE_NAME = "exchange.fanout";
private final static String QUEUE_NAME = "queue.fanout";
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 綁定隊列
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
和扇形交換機綁定的隊列將全部收到收到發佈端的消息。
- 主題交換機(Topic)
主題交換機(topic exchanges)通過對消息的路由鍵和隊列到交換機的綁定模式之間的匹配,將消息路由給一個或多個隊列,屬於多播路。,topic可以進行模糊匹配,可以使用星號和井號#這兩個通配符來進行模糊匹配,其中 號可以代替一個單詞 # 號可以代替任意個單詞,但是需要注意的是topic交換機的路由鍵也不是可以隨意設置的,必須是由點隔開的一系列的標識符組成。標識符一般和消息的某些特性相關,可以定義任意數量的標識符,上限爲255個字節,當路由鍵可以模糊匹配上的時候就能將消息映射到綁定的隊列中去。
- 首部交換機(Header)
首部交換機和扇形交換機一樣不需要路由關鍵字,交換機時通過headers來將消息映射到隊列的,heders是一個hash結構求攜帶一個鍵“x-match”,這個鍵的value可以是any或者all,all代表消息攜帶的Hash是需要全部匹配,any代表僅匹配一個鍵就可以了。首部交換機的最大特點就是匹配規則不被限制爲string,而是object。
Publish
private static String EXCHANGE_NAME = "exchange.hearders";
Map<String, Object> hearders = new HashMap<String, Object>();
hearders.put("api", "login");
hearders.put("version", 1.0);
hearders.put("radom", UUID.randomUUID().toString());
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder()
.headers(hearders)
.build();
String message = "Hello RabbitMQ!";
channel.basicPublish(EXCHANGE_NAME, "", properties, message.getBytes("UTF-8"));
Consumer
private static String EXCHANGE_NAME = "exchange.hearders";
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.HEADERS);
String queueName = channel.queueDeclare().getQueue();
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-match", "any");
// arguments.put("x-match", "all"); 此時將不能匹配,因爲Publisher的頭部中並沒有dataType屬性
arguments.put("api", "login");
arguments.put("version", 1.0);
arguments.put("dataType", "json");
// 隊列綁定時需要指定參數,注意雖然不需要路由鍵但仍舊不能寫成null,需要寫成空字符串""
channel.queueBind(queueName, EXCHANGE_NAME, "", arguments);
- 默認交換機(Default)
默認交換機(default exchange)不是一個真正的交換機類型,實際上是一個由消息代理(Broker)預先聲明好的沒有名字(名字爲空字符串)的直連交換機。它有一個特殊的屬性:那就是每個新建隊列(queue)都會自動綁定到默認交換機上,綁定的路由鍵(routing key)名稱與隊列名稱相同。
很多時候我們對於一些不復雜的場景都會使用這一特殊屬性。
Publisher
private static final String QUEUE_NAME="task_queue";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
Consumer
private static final String QUEUE_NAME="task_queue";
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
上面的僞代碼看起來我們並沒有使用交換機,而是直接將消息投遞到了隊列中去,但實際上這個隊列被綁定到了默認交換機上,而路由鍵就是隊列名稱。
隊列及隊列屬性
channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments);
- queue : 隊列名稱,隊列在聲明(declare)後才能被使用。如果一個隊列尚不存在,聲明一個隊列會創建它。如果聲明的隊列已經存在,並且屬性完全相同,那麼此次聲明不會對原有隊列產生任何影響。如果聲明中的屬性與已存在隊列的屬性有差異,那麼將會拋出一個406通道級異常。
- durable:隊列的聲明默認是存放到內存中的,稱爲暫存隊列,消息代理重啓會丟失。如果想重啓之後還存在就要使隊列持久化,保存到Erlang自帶的Mnesia數據庫中,當rabbitmq重啓之後會讀取該數據庫。但是隊列持久化並不意味着消息持久化當消息代理重啓後消息依舊會丟失。
- exclusive :是否排外的,有兩個作用,一:當連接關閉時connection.close()該隊列是否會自動刪除;二:該隊列是否是私有的private,如果不是排外的,可以使用兩個消費者都訪問同一個隊列,沒有任何問題,如果是排外的,會對當前隊列加鎖,其他通道channel是不能訪問的。
- autoDelete :當最後一個消費者斷開連接之後隊列是否自動被刪除。
arguments :
1. x-message-ttl(Time-To-Live):
設置隊列中的所有消息的生存週期(統一爲整個隊列的所有消息設置生命週期), 也可以在發佈消息的時候單獨爲某個消息指定剩餘生存時間,單位毫秒。生存時間到了,消息會被從隊裏中刪除,注意是消息被刪除,而不是隊列被刪除。
// 統一設置隊列消息過期時間爲10s
Map<String, Object> argumentsMap = new HashMap<>();
argumentsMap.put("x-message-ttl", 10000);
channel.queueDeclare(QUEUE_NAME, true, false, false, argumentsMap);
// 單獨設置某條消息過期時間
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder()
.expiration("10000")
.build();
channel.basicPublish(EXCHANGE_NAME, "", properties, message.getBytes(“UTF-8”));
2.x-expires : 當隊列在指定的時間沒有被訪問則被刪除。
Map<String, Object> argumentsMap = new HashMap<>();
// 設置隊列消息過期時間 單位:毫秒
argumentsMap.put("x-expires", 10000);
channel.queueDeclare(QUEUE_NAME, true, false, false, argumentsMap);
3.x-max-length : 限定隊列的消息的最大值長度,超過指定長度將會把最早的幾條刪除掉,遵循先進先出的原則。(要注意的是雖然消息隊列是異步處理消息,但是消息幾乎是被準實時消費的,這裏只能保證消息隊列的堆積消息不超過最大長度,使用時要特別注意)
Map<String, Object> argumentsMap = new HashMap<>();
// 設置隊列消息對列的最大長度爲5
argumentsMap.put("x-max-length", 5);
channel.queueDeclare(QUEUE_NAME, true, false, false, argumentsMap);
4.x-max-length-bytes :限定隊列最大佔用的內存空間大小。
5.x-dead-letter-exchange : 將從隊列中刪除的消息(大於最大長度、或者過期的等)推送到指定的交換機中去而不是丟棄掉。
6.x-dead-letter-routing-key :將刪除的消息推送到指定交換機的指定路由鍵的隊列中去。
Dead Letter Exchange(死亡交換機) 和 Dead Letter Routing Key(死亡路由鍵)用做死信隊列(由於某些原因消息無法被正確的投遞,爲了確保消息不會被無故的丟棄,一般將其置於一個特殊角色的隊列,這個隊列一般稱之爲死信隊列),當隊列中的消息被刪除時(大於最大長度、或者過期的等),可以將這些被刪除的信息推送到其他交換機中,讓其他消費者訂閱這些被刪除的消息,處理這些消息。
public class Publisher {
private static final String DEAD_QUEUE_NAME = "dead_queue";
private static final String DEAD_EXCHANGE_NAME = "exchange.dead";
private static final String DEAD_ROUTING_KEY = "routingkey.dead";
private static final String QUEUE_NAME = "general_queue";
private static final String EXCHANGE_NAME = "exchange.general";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 聲明接收"死亡消息"的隊列和交換機
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare(DEAD_QUEUE_NAME, false, false, false, null);
channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_ROUTING_KEY);
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-message-ttl", 15000);
arguments.put("x-expires", 30000);
arguments.put("x-max-length", 4);
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
arguments.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);
channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
for(int i = 1; i <= 5; i++) {
String message = "Hello RabbitMQ: "+ i;
channel.basicPublish(EXCHANGE_NAME, "", null, message .getBytes("UTF-8"));
System.out.println("sent message : "+ message);
}
channel.close();
connection.close();
}
}
剛開始由於隊列長度是4,總共發送5條消息,所以最早進入隊列的消息1將被刪除掉,被推送到死亡隊列中,所以看到普通隊列的消息爲4條,死亡隊列的消息爲1條。隨着時間的流逝普通隊列的消息全部過期,所有消息都被推送到死亡隊列中,最後普通隊列被刪除。
7.x-max-priority : 優先級隊列,聲明隊列時先定義最大優先級值(定義最大值一般不要太大),在發佈消息的時候指定該消息的優先級, 優先級更高的消息先被消費。
Publisher
private static final String QUEUE_NAME = "priority_queue";
private static final String EXCHANGE_NAME = "exchange.priority";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-max-priority", 5);
channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
for(int i = 1; i <= 5; i++) {
String message = "Hello RabbitMQ: "+ i;
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder() .priority(i) .build();
channel.basicPublish(EXCHANGE_NAME, "", properties, message .getBytes("UTF-8"));
System.out.println("sent message : "+ message);
}
channel.close();
connection.close();
}
Consumer
private static final String QUEUE_NAME = "priority_queue";
private static final String EXCHANGE_NAME = "exchange.priority";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
Map<String, Object> arguments = new HashMap<String, Object>();
arguments.put("x-max-priority", 5);
channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
com.rabbitmq.client.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("Receive message : " + message);
}
};
channel.basicConsume(QUEUE_NAME,true,consumer);
channel.close();
connection.close();
}
8.x-queue-mode=lazy : 先將消息保存到磁盤上,不放在內存中,當消費者開始消費的時候才加載到內存中,默認爲lazy。
9.x-queue-master-locator : 配置鏡像隊列
消息確認
消費者(Consumer) :在處理消息的時候偶爾會失敗或者有時消息代理會直接崩潰掉。而且網絡原因也有可能引起各種問題,那麼爲了保證消息不會丟失保證消息被正確處理,RabbitMQ提供了兩種消息確認機制:
1.自動應答 :
boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
自動應答表示當消費者連接上隊列,因爲沒有指定消費者一次獲取消息的條數,所以會把隊列中的所有消息一下子推送到消費者端,當消息從隊列被推出的時的那一刻就表示已經對消息進行自動確認了,消息就會從隊列中刪除。
channel.basicConsume(QUEUE_NAME, true, consumer);
當隊列被訂閱後,隊列中的消息全部被清空。
2.手動應答 :
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
Consumer:
com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
channel.basicAck(envelope.getDeliveryTag(), false);
System.out.println("C [x] Received '" + message + "'");
}
};
// 訂閱消息
channel.basicConsume(QUEUE_NAME, false, consumer);
每執行一次channel.basicAck(envelope.getDeliveryTag(), false);Unacked和Total就會減去1,直到兩個值都爲0 。
注意:手動確認一定要channel.basicAck(envelope.getDeliveryTag(), false);否則會導致消息不被確認而一直堆積在隊列中而不被刪除。