本文章翻譯自http://www.rabbitmq.com/api-guide.html,并沒有及時更新。
術語對照
指南:Guide
教程:Tutorial
代理(即RabbitMQ服務端):Broker、RabbitMQ server
客戶端:Client
發布者:Publisher
消費者:Consumer
連接:Connection
連接工廠:Connection Factory
通道:Channel
交換機:Exchange
隊列:Queue
聲明:Declare
路由:Route、routable、unroutable
持久化:durable、non-durable
自動刪除:autodelete、non-autodelete
排斥:exclusive
路由關鍵字:routing key
推:Push
拉:Pull
(消息)投遞:Delivery
關閉:Shutdown
度量:Metrics
本指南涵蓋RabbitMQ Java客戶端API,但不是教程,教程在另外的文檔。
Java客戶端API使用三個License:
●?Apache Public License 2.0
●?Mozilla Public License
●?GPL 2.0
如果需要更詳細的信息,請參考相關的Javadoc文檔。
與Java客戶端一起的還有命令行工具。
客戶端API嚴密地按照AMQP 0-9-1協議規范來進行建模,為簡化使用進行了額外的抽象。
1 概述
RabbitMQ Java客戶端使用“com.rabbitmq.client”作為頂級包。主要的類和接口:
●?Channel
●?Connection
●?ConnectionFactory
●?Consumer
通過“Channel”接口可以進行協議操作。
“Connection”用來打開通道,注冊連接生命周期的事件處理器,和關閉不需要的連接。
“Connection”通過“ConnectionFactory”來實例化。你可以配置“ConnectionFactory”使用不同的連接參數,比如vhost或username。
2 連接和通道
核心的API類是“Connection”和“Channel”,“Connection”代表AMQP 0-9-1連接,“Channel”代表通道。使用之前需要導入:
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
2.1 連接到代理
下面的代碼使用給定參數(主機名、端口號、等等)連接到一個AMQP代理:
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(userName);
factory.setPassword(password);
factory.setVirtualHost(virtualHost);
factory.setHost(hostName);
factory.setPort(portNumber);
Connection conn = factory.newConnection();
如果RabbitMQ服務端運行在本地,這些參數都不需要配置,它們有合適的默認值。
還可以使用URIs 來進行配置:
ConnectionFactory factory = new ConnectionFactory();
factory.setUri("amqp://userName:password@hostName:portNumber/virtualHost");
Connection conn = factory.newConnection();
然后,“Connection”接口可用用來打開一個通道:
Channel channel = conn.createChannel();
現在,這個通道就可以用來發送和接收消息了,下面章節將進行闡述。
要想與代理斷連,簡單的關閉通道和連接即可:
channel.close();
conn.close();
注意,關閉通道是一個很好的習慣,但并不是嚴格必須的。當底層的連接關閉的時候,通道都將會自動被關閉。
3 使用交換機和隊列
客戶端應用與交換機和隊列一起工作,交換機和隊列是AMQP的高級模塊。在使用它們之前必須先“聲明”。聲明它們就能確保以該名字命名的交換機或隊列是存在的,如果不存在,就創建。
繼續上面的例子,下面的代碼聲明了一個交換機和一個隊列,然后將它們綁定在一起。
channel.exchangeDeclare(exchangeName, "direct", true);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, routingKey);
這樣就聲明了交換機和隊列,兩者都可以用額外的參數來定制。下面是它們的參數:
●?交換機:持久化的(durable)、非自動刪除的(non-autodelete)、類型是“direct”
●?隊列:非持久化的(non-durable)、排斥的(exclusive)、自動刪除的(autodelete)、名字是隨機生成的(generated?name)
然后,通過給定的路由關鍵字(routing key)將隊列綁定到交換機。
注意:當只有一個客戶端需要與該隊列一起工作時,這是比較常見的聲明方式。該隊列不需要一個指定的名字,也不能被其他客戶端所使用(即它是排斥的),而且將在該客戶端消失時自動被清理(即它是自動刪除的)。如果多個客戶端要共用一個命名的隊列,下面的代碼會比較合適:
channel.exchangeDeclare(exchangeName, "direct", true);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
這樣就聲明了:
●?交換機:持久化的(durable)、非自動刪除的(non-autodelete)、類型是“direct”
●?隊列:持久化的(durable)、非排斥的(non-exclusive)、非自動刪除的(non-autodelete)、名字是指定的(well-known?name)
注意,“Channel”的所有這些API都是重載的。參數較少的API比較方便,使用一些合適的默認值;參數較多的API在必要時讓你能夠覆蓋那些默認值,以便有完全的控制。
這種“short form, long form”模式(即方法重載模式)在客戶端API中經常使用。
4 發布消息
要發布一個消息到交換機,則使用“Channel.basicPublish”:
byte[] messageBodyBytes = "Hello, world!".getBytes();
channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
為了更好的控制,你可以使用重載版本,指定“mandatory”標識符,或者使用預置的消息屬性來發送消息:
channel.basicPublish(exchangeName, routingKey, mandatory,
????MessageProperties.PERSISTENT_TEXT_PLAIN,
????messageBodyBytes);
上面是以投遞模式2(delivery mode 2)、優先級1、內容類型為“text/plain”來發送消息。你可以使用“Builder”類來構建你自己的消息屬性對象,引用你喜歡的屬性,比如:
channel.basicPublish(exchangeName, routingKey,
????new AMQP.BasicProperties.Builder()
????????.contentType("text/plain")
????????.deliveryMode(2)
????????.priority(1)
????????.userId("bob")
????????.build()),
????messageBodyBytes);
下面的例子使用定制的headers來發布消息:
Map headers = new HashMap();
headers.put("latitude",51.5252949);
headers.put("longitude", -0.0905493);
channel.basicPublish(exchangeName, routingKey,
????new AMQP.BasicProperties.Builder()
????????.headers(headers)
????????.build()),
????messageBodyBytes);
下面的例子使用過期時間來發布消息:
channel.basicPublish(exchangeName, routingKey,
????new AMQP.BasicProperties.Builder()
????????.expiration("60000")
????????.build()),
????messageBodyBytes);
我們不在這里闡述所有的發布消息的屬性配置。
注意,”BasicProperties”類是”AMQP”類的一個內部類,“AMQP”類是一個自動生成的持有者類(autogenerated holder class即專門用來定義靜態內部類和靜態屬性)
如果資源不足告警 有效的話,那么調用”Channel#basicPublish”時可能最終會導致阻塞。
5 通道和并發考慮(線程安全)
一般來說,應該避免在線程間共享使用“Channel”實例。應用應該選擇每個線程使用一個“Channel”,而不是在多個線程之間共享同一個“Channel”。
在通道上的一些操作是并發調用安全的,一些不是安全的,會導致線上的幀錯誤的交織在一起(即數據幀錯亂)、重復的消息確認等等。
在一個共享通道上并發的發布消息可能會導致線上的幀錯誤的交織在一起(即數據幀錯亂),同時會觸發連接級別的協議異常和連接關閉。因此,在應用代碼中必須顯式的進行同步(”Channel#basicPublish”必須放在一個臨界區進行調用)。線程間共享通道還會對發布者確認(PublisherConfirms) 造成干擾。我們強烈推薦應該避免在一個共享通道上并發的發布消息。
一個線程使用共享通道消費消息,另一個線程使用該通道發布消息,這樣是安全的。
服務端推送(Server-pushed)的消息投遞(見下節)在并發的執行時,會保證每個通道內的消息順序是一致的。這種分發機制采用了每個連接分配一個“java.util.concurrent.ExecutorService”。通過使用“ConnectionFactory#setSharedExecutor”方法提供一個定制的在同一個“ConnectionFactory”建立的所有連接之間共享的executor是可能的。
當使用手動確認時,考慮由哪個線程來發送ack很重要。如果發送ack的線程不同于收到該次消息投遞的的線程(比如,“Consumer#handleDelivery”方法把對消息投遞的處理委托給一個不同的線程),那么,將“multiple”參數設置為true來發送ack是不安全的,這將會導致重復確認和通道級別的協議異常,該異常將關閉通道。每次對一個消息進行發送ack是安全的。
6 通過訂閱接收消息(Push API)
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
接收消息的最有效的方法是使用“Consumer”接口發起訂閱。當消息到達隊列時,將自動被投遞,而無需顯式的請求消息。
當調用“Consumer”相關的API方法時,各自的訂閱總是由它們的消費者標簽(consumer tags)來引用。消費者tag就是消費者id,它由客戶端或服務端生成。為了讓RabbitMQ生成節點唯一的tag,使用不帶消費者tag參數或者傳空字符串給該參數的“Channel#basicConsume”方法,并使用該方法的返回值即可。消費者標簽用于取消消費者的訂閱。
不同的“Consumer”實例必須有不同的消費者標簽。在一個連接上使用重復的消費者標簽是絕對禁止的,它會引發連接自動恢復的問題,而且會混淆對消費者的監控。
實現一個“Consumer”的最簡單的方法是繼承“DefaultConsumer”。然后,將該子類的一個對象傳入“basicConsume”方法即可發起訂閱:
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "myConsumerTag",
????new DefaultConsumer(channel) {
????????@Override
????????public voidhandleDelivery(String consumerTag,
????????????????????????????????????????????????????Envelope envelope,
????????????????????????????????????????????????????AMQP.BasicProperties properties,
????????????????????????????????????????????????????byte[] body)
????????????throws IOException
????????{
????????????String routingKey =envelope.getRoutingKey();
????????????String contentType = properties.getContentType();
????????????long deliveryTag =envelope.getDeliveryTag();
? ??? ??? ??// (process the message?components here ...)
????????????channel.basicAck(deliveryTag, false);
????????}
????});
這里,我們設置了”autoAck = false”,所以必須對投遞到該“Consumer”的消息進行確認,這個很方便就在“handleDelivery”方法(收到消息會被調用)內就完成了,如代碼所示。
更加復雜的“Consumer”需要覆蓋更多方法。特別是,“handleShutdownSignal”方法將在通道和連接關閉時被調用,”handleConsumeOk”方法在調用其他任何到該“Consumer”的回調之前進行調用,傳入的參數是消費者標簽。
消費者還可以實現“handleCancelOk”和“handleCancel”方法,這樣就能夠在顯式和隱式取消消費者(訂閱)時得到通知。
你可以顯式的取消一個特定的“Consumer”,使用”Channel.basicCancel”方法,傳入消費者標簽:
channel.basicCancel(consumerTag);
跟發布者類似,考慮消費者的并發安全也很重要。
“Consumer”的回調是在一個線程池中進行分發的,該線程池獨立于實例化該通道的線程。這意味著”ConsumerS”可以在該連接和通道上安全的調用阻塞方法,比如“Channel#queueDeclare”或者“Channel#basicCancel”。
每個通道都有自己的分發線程。最常見的使用場景是每個通道只有一個消費者,意味著該通道沒有其他消費者。如果一個通道上有多個消費者,一個運行時間長的消費者可能會占用該通道其他消費者的回調的分發。
請參考“并發考慮(線程安全)”一節。
7 主動獲取消息(Pull API)
要想顯式的獲取消息,使用“Channel.basicGet”方法。返回值是一個“GetResponse”的實例,可以從該實例中提取出header信息(屬性)和消息體:
boolean autoAck = false;
GetResponse response = channel.basicGet(queueName, autoAck);
if (response == null) {
? ??// No message retrieved.
} else {
????AMQP.BasicProperties props =response.getProps();
????byte[] body =response.getBody();
????long deliveryTag =response.getEnvelope().getDeliveryTag();
????...
由于上面設置了“autoAck = false”,你必須調用“Channel.basicAck”來卻你已經成功收到消息:
...
????channel.basicAck(method.deliveryTag, false);// acknowledge receipt of the message
}
8 處理不可路由的消息
如果一個消息發布時設置了“mandatory”標識符,但又不能路由,代理將會把它返回給發送客戶端(通過AMQP.Basic.Return命令)。
為了得到這種消息被返回的通知,客戶端可以實現“ReturnListener”接口,然后調用“Channel.setReturnListener”方法。如果客戶端沒有為通道配置一個返回監聽器,那么相關的返回消息將被安靜的丟棄。
channel.setReturnListener(new ReturnListener() {
????public voidhandleBasicReturn(int replyCode,
????????????????????????????????????????????????????String replyText,
????????????????????????????????????????????????????String exchange,
????????????????????????????????????????????????????String routingKey,
????????????????????????????????????????????????????AMQP.BasicProperties properties,
????????????????????????????????????????????????????byte[] body)
????throws IOException {
????????...
????}
});
舉個例子,如果客戶端將設置“mandatory”標識符的消息發布到一個沒有綁定任何隊列的“direct”類型的交換機,就會調用返回監聽器。
9 關閉協議
9.1 AMQP客戶端關閉的概述
AMQP 0-9-1的連接和通道都使用相同的方法來管理網絡故障、內部故障、和顯式的本地關閉。
AMQP 0-9-1的連接和通道對象在其生命周期中有如下狀態:
●?打開(open):該對象可以使用。
●?正在關閉(closing):該對象得到本地顯式的通知要關閉,已經向底層的對象發送了一個關閉請求,正在等待它們完成關閉過程。
●?關閉(closed):該對象從底層對象得到關閉完成的通知,并已經將自己關閉。
AMQP連接和通道對象總是以關閉狀態終止,不管關閉的原因是什么,比如應用的請求、內部客戶端庫的故障、遠程網絡請求、或網絡故障。
AMQP連接和通道對象擁有下列關閉相關的方法:
●?“addShutdownListener(ShutdownListener listener)”
和“removeShutdownListener(ShutdownListener?listener)”:用于管理關閉監聽器。這些關閉監聽器將在對象狀態轉換為關閉狀態時觸發。注意,添加一個關閉監聽器到已經關閉的對象,將立即觸發該監聽器。
●?“getCloseReason()”:用以允許檢查導致該對象關閉的原因。
●?“isOpen()”:用以檢測該對象是否處于打開狀態。
●?“close(int closeCode, String closeMessage)”:顯式通知該對象關閉。
監聽器簡單的用法如下:
import com.rabbitmq.client.ShutdownSignalException;
import com.rabbitmq.client.ShutdownListener;
connection.addShutdownListener(new ShutdownListener() {
????public voidshutdownCompleted(ShutdownSignalException cause)
????{
????????...
????}
});
9.2 關閉的環境
我們可以捕獲“ShutdownSignalException”,該異常包含了關于關閉原因的所有可獲取的信息,或是顯式調用“getCloseReason()”方法能獲取的,或是使用“ShutdownListener”類的“service(ShutdownSignalException cause)”方法的“cause”參數獲取的。
“ShutdownSignalException”類提供了用于分析關閉原因的方法。調用“isHardError()”方法可以判別是連接出錯還是通道出錯。“getReason()”方法以一個AMQP method的形式返回該原因的相關信息-或是”AMQP.Channel.Close”或是“AMQP.Connection.Close”(或是為null,如果該原因是庫內某個異常,比如網絡通信失敗,這種情況下可以用“getCause()”方法捕獲異常)。
public void shutdownCompleted(ShutdownSignalException cause)
{
????if (cause.isHardError())
????{
????????Connection conn =(Connection)cause.getReference();
????????if(!cause.isInitiatedByApplication())
????????{
????????????Method reason =cause.getReason();
????????????...
? ? ? ? ?}
????????...
????} else {
????????Channel ch =(Channel)cause.getReference();
????????...
????}
}
9.3 isOpen()方法的原子性和使用
不推薦在生產代碼中使用通道和連接對象的“isOpen()”方法。因為該方法的返回值取決于關閉原因是否存在。下面的代碼闡述了競爭條件的可能性:
public void brokenMethod(Channel channel)
{
????if (channel.isOpen())
????{
????????// The following codedepends on the channel being in open state.
????????// However there is apossibility of the change in the channel state
????????// between isOpen() andbasicQos(1) call
????????...
????????channel.basicQos(1);
????}
}
相反,我們通常應該忽略這種檢查,簡單的嘗試所需的操作。如果執行操作期間該連接的該通道被關閉,將會拋出“ShutdownSignalException”異常表示該對象處于無效的狀態。當代理意外關閉連接時,我們還應該捕獲”SocketException”引發的“IOException”;當代理發起正常關閉時,我們應該捕獲“ShutdownSignalException”。
public void validMethod(Channel channel)
{
????try {
????????...
????????channel.basicQos(1);
????} catch (ShutdownSignalExceptionsse) {
????????// possibly check if channelwas closed
????????// by the time we startedaction and reasons for
????????// closing it
????????...
????} catch (IOException ioe) {
????????// check why connection wasclosed
????????...
????}
}
10 高級連接選項
10.1 消費者線程池
消費者線程(見“Push API”一節)默認是從一個新的“ExecutorService”線程池中自動分配的。如果需要更多控制權,可以在調用“newConnection()”方法時傳入一個“ExecutorService”,從而使用你傳入的線程池。下面的例子傳入一個比默認情況更大的線程池:
ExecutorService es = Executors.newFixedThreadPool(20);
Connection conn = factory.newConnection(es);
“Executors”和“ExecutorService”類都在“java.util.concurrent”包中。
當該連接關閉時,默認的“ExecutorService”將被“shutdown()”,但是用戶提供的“ExecutorService”(像上面的es)將不會自動“shutdown()”。提供定制“ExecutorService”的客戶端必須確保它最終是關閉的(通過調用它的“shutdown()方法”),否則線程池中的線程會阻止JVM的終止。
同一個executor service可能被多個連接所共享,或者當重新連接時連續復用,但當它“shutdown()”后就不能被使用了。
如果有證據表明在處理“Consumer”的回調時出現嚴重的瓶頸,才應該考慮使用此功能。如果沒有“Consumer”回調可執行,或很少回調,默認的線程池分配就已經足夠了。開銷最初是最小的,而分配的總線程資源是有限的,即使消費者活動激增會偶爾發生。
10.2使用多個主機
我們也可以傳入“Address”數組到“newConnection()”方法。一個“Address”就是“com.rabbitmq.client”包中的帶有“host”和“port”的很方便的類。比如:
Address[] addrArr = new Address[]{ new Address(hostname1, portnumber1)
,new Address(hostname2, portnumber2)};
Connection conn = factory.newConnection(addrArr);
上面的代碼嘗試連接到“hostname1:portnumber1”,如果失敗則嘗試連接“hostname2:portnumber2”。返回的連接是數組中第一個成功連接上的(沒有拋出“IOException”)。這跟每次重復設置連接工廠的主機名和端口并調用“factory.newConnection()”直到有一次連接成功,是完全等效的。
如果同時提供“ExecutoryService”(即調用“factory.newConnection(es, addrArr)”),線程池將和第一個成功的連接進行關聯。
如果你想要對所連接的主機進行更多的控制,參考下一節關于服務發現的內容。
10.3 使用AddressResolver接口的服務發現
從版本3.6.6開始,就可以實現“AddressResolver”接口在創建連接時選擇連接到哪個地址:
Connection conn = factory.newConnection(addressResolver);
“AddressResolver”接口如下:
public interface AddressResolver {
????List getAddresses()throws IOException;
}
就想上節使用多個主機一樣,第一個返回的“Address”將首先嘗試,如果失敗,則嘗試第二個,以此往下。
如果同時提供“ExecutoryService”(即調用“factory.newConnection(es, addressResolver)”),線程池將和第一個成功的連接進行關聯。
“AddressResolver”接口可以用來很好的實現定制的服務發現的邏輯,這在一個動態的基礎設施環境中會很有用。加上自動恢復功能(見11章),客戶端可以自動連接上那些在客戶端第一次啟動時還未啟動的節點。定制“AddressResolver”在親和性和負載均衡場景下也很有用。
Java客戶端API自帶了如下實現(見Javadoc):
●?DnsRecordIpAddressResolver:給定主機名,返回其IP地址(針對DNS服務器平臺的方案)。這對于簡單的基于DNS的負載均衡和故障轉移很有用。
●?DnsSrvRecordAddressResolver:給定服務的名稱,返回主機名和端口。這個搜索通過DNS SRV請求來實現。當使用諸如“HashiCorp Consul”的服務注冊表時,這就很有用。
10.4 心跳超時
參見“心跳指南”來獲取更多關于心跳和如何在Java客戶端配置心跳的信息。
10.5 定制線程工廠
諸如Google App Engine(GAE)的環境可能會限制直接實例化線程。在這樣的環境中使用RabbitMQ的Java客戶端,必須配置一個定制的“ThreadFactory”,它使用合適的方法來實例化線程,比如GAE的“ThreadManager”。下面是一個針對GAE的例子:
import com.google.appengine.api.ThreadManager;
ConnectionFactory cf = new ConnectionFactory();
cf.setThreadFactory(ThreadManager.backgroundThreadFactory());
10.6 支持Java非阻塞IO
版本4.0的Java客戶端API提供了對Java非阻塞IO(即Java NIO)的試驗性支持。NIO并不一定比阻塞IO快,它只是允許你更容易的控制資源(本例中就是線程)。
在默認的阻塞IO模式下,每一個連接都使用一個線程來從網絡套接口上讀取數據。在NIO模式下,你可以控制讀寫網絡套接口的線程數量。
如果你的Java進程使用很多連接(幾十或上百個),就要使用NIO模式。這樣應該會比默認的阻塞模式使用更少的線程。如果設置了適當的線程數,就不應該嘗試降低性能,尤其是在連接不那么繁忙的情況下。
NIO必須顯式開啟:
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.useNio();
NIO模式可以通過“NioParams”類進行配置:
connectionFactory.setNioParams(new NioParams().setNbIoThreads(4));
NIO模式使用了合理的參數默認值,但你也許需要根據你的工作負載來修改。其中一些設置是:使用的IO線程總數、緩沖區的大小、用以IO循環的service executor、內存中寫隊列的參數(寫請求在發送到網絡之前要先入隊)。請閱讀Javadoc獲取更多細節和默認值。
11 從網絡故障中自動恢復
11.1 連接恢復
客戶端和RabbitMQ節點之間的網絡連接可能會發生故障。RabbitMQ的Java客戶端API支持連接和topology(隊列、交換機、綁定和消費者)的自動恢復。很多應用的自動恢復過程都是如下步驟:
1.重連
2.恢復連接監聽器
3.重新打開通道
4.恢復通道監聽器
5.恢復通道的”basic.qos”設置,發布者確認和事務設置
topology恢復包含如下動作,針對每個通道而執行:
1.重新聲明交換機(除預定義交換機以外)
2.重新聲明隊列
3.恢復所有綁定
4.恢復所有消費者
4.0.0版本的Java客戶端API,自動恢復是默認開啟的(topology恢復也是)。
要關閉或開啟自動連接恢復,要使用“factory.setAutomaticRecoveryEnabled(boolean)”。下面的代碼段展示了如何顯示的開啟啟動恢復(版本4.0.0之后):
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername(userName);
factory.setPassword(password);
factory.setVirtualHost(virtualHost);
factory.setHost(hostName);
factory.setPort(portNumber);
factory.setAutomaticRecoveryEnabled(true);
// connection that will recover automatically
Connection conn = factory.newConnection();
如果因為異常(比如RabbitMQ節點不可到達)導致恢復失敗,將會以一個固定的時間間隔(默認5秒)繼續嘗試。該時間間隔可配置:
ConnectionFactory factory = new ConnectionFactory();
// attempt recovery every 10 seconds
factory.setNetworkRecoveryInterval(10000);
當使用多個地址時,將會隨機排列地址列表并一個一個嘗試所有的地址:
ConnectionFactory factory = new ConnectionFactory();
Address[] addresses = {new Address("192.168.1.4"), newAddress("192.168.1.5")};
factory.newConnection(addresses);
11.2 恢復監聽器
可以注冊一個或多個恢復監聽器,來監聽可恢復的連接和通道。當開啟了連接恢復,由“ConnectionFactory#newConnection”和“Connection#createChannel”返回的連接實現了“com.rabbitmq.client.Recoverable”接口,提供了相當明顯的兩個方法:
●?addRecoveryListener
●?removeRecoveryListener
注意,目前需要將連接和通道強制轉換成“Recoverable”類型,才能使用這兩個方法。
11.3 對發布消息的影響
當連接斷開時,使用“Channel.basicPublish”發布的消息將會丟失。客戶端不會將它們入隊緩存直到連接恢復后重新發布。要確保發布的消息到達了RabbitMQ,應用需要使用“發布者確認”,并對連接失效負責。
11.4 Topology恢復
topology恢復包括交換機、隊列、綁定和消費者的恢復。當自動恢復開啟時,默認也就開啟了topology恢復。因此,Java客戶端API的4.0.0版本默認是開啟topology恢復的。
如有需要,可以顯示關閉topology恢復:
ConnectionFactory factory = new ConnectionFactory();
Connection conn = factory.newConnection();
// enable automatic recovery (e.g. Java client prior 4.0.0)
factory.setAutomaticRecoveryEnabled(true);
// disable topology recovery
factory.setTopologyRecoveryEnabled(false);
11.5 手動確認和自動恢復
當使用手動確認時,到RabbitMQ節點的網絡連接可能在消息投遞和確認中間發生故障。在連接恢復之后,RabbitMQ將會重置所有通道的投遞標簽。這意味著帶有舊投遞標簽的“basic.ack”、“basic.nack”和“basic.reject”將會引發通道異常。為了避免這種情況,RabbitMQ的Java客戶端API跟蹤并更新投遞標簽,并使投遞標簽在每次連接恢復時單調增長。然后,“Channel.basicAck”、“Channel.basicNack”和“Channel.basicReject”將調整過后的投遞標簽轉換成RabbitMQ使用的標簽。這樣,帶有舊投遞標簽的確認將不會被發送出去。使用手動確認和自動恢復的應用必須能夠處理重新投遞的情況。
12 未處理的異常
有關連接、通道、恢復、和消費者生命周期的未處理異常,都被委托給異常處理器。異常處理器可以是實現了“ExceptionHandler”接口的任何對象。默認情況下,使用的是“DefaultExceptionHandler”的一個實例。它將異常的詳細信息打印到標準輸出。
使用“ConnectionFactory#setExceptionHandler”可以覆蓋默認的處理器,它將用于由該工廠創建的所有連接:
ConnectionFactory factory = new ConnectionFactory();
cf.setExceptionHandler(customHandler);
異常處理器應該用于異常的記錄。
13 度量和監控
從版本4.0.0開始,客戶端收集運行時的度量(比如已經發布的消息數量)。度量收集是可選的并在“ConnectionFactory”級別使用“setMetricsCollector”方法進行設置。該方法需要一個“MetricsCollector”實例,該實例將在客戶端代碼的多個地方被調用。
客戶端自帶有一個使用“Dropwizard Metrics”庫的“MetricsCollector”實現。可以按如下方式來開啟度量收集:
ConnectionFactory connectionFactory = new ConnectionFactory();
StandardMetricsCollector metrics = new StandardMetricsCollector();
connectionFactory.setMetricsCollector(metrics);
...
metrics.getPublishedMessages(); // get Metrics' Meter object
下面是收集的各種度量:
●?打開的連接數量(默認實現的一個“Counter”)
●?打開的通道數量(默認實現的一個“Counter”)
●?已經發布的消息數量(默認實現的一個“Meter”)
●?已經消費的消息數量(默認實現的一個“Meter”)
●?已經確認的消息數量(默認實現的一個“Meter”)
●?已經拒絕的消息數量(默認實現的一個“Meter”)
通過使用Dropwizard Metrics,不僅可以獲得計數,還可以獲得過去五分鐘的速率等,還有各種開箱即用的報告工具(JMX、Graphite、Ganglia、HTTP)。
請注意下面關于度量收集的信息:
●?如果你使用基于Dropwizard?Metrics的默認實現,不要忘記在你的classpath中添加合適的JAR文件。(Java客戶端API沒有自帶Dropwizard Metrics,這是一個可選依賴)。
●?度量收集是可擴展的,你可以實現自己的”MetricsCollector”滿足特殊需求。
●?“MetricsCollector”是在“ConnectionFactory”級別設置的,但可在多個實例間共享。
●?度量收集不支持事務。比如,如果在一個事務中發送了一個ack,而該事務后來又回滾了,該ack在客戶端度量中被計數了(但顯然在代理上沒有計數)。注意,該ack實際上被發送到代理,而然后又被事務回滾所取消,所以客戶端的發送ack數量這個度量是正確的。總之,不要為敏感業務監控使用客戶度量,因為它們不保證完全正確。
13.1 度量報告
如果你使用基于Dropwizard Metrics的“StandardMetricsCollector”,你可以發送這些度量值到多個報告后端:控制臺、JMX、HTTP、Graphite、Ganglia等等。
你通常要傳入一個“MetricsRegistry”的實例到“StandardMetricsCollector”。下面是JMX的一個例子:
MetricRegistry registry = new MetricRegistry();
StandardMetricsCollector metrics = new StandardMetricsCollector(registry);
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setMetricsCollector(metrics);
JmxReporter reporter = JmxReporter
????.forRegistry(registry)
????.inDomain("com.rabbitmq.client.jmx")
????.build();
reporter.start();
14 Google App Engine上的RabbitMQJava Client
在Google App Engine(GAE)上使用RabbitMQ Java客戶端需要使用一個定制的線程工廠,該工廠使用GAE的“ThreadManager”(見之前所述)來實例化線程。此外,必須設置比較短的心跳間隔(4-5秒)來避免遇到GAE上的“InputStream”讀超時。
ConnectionFactory factory = new ConnectionFactory();
cf.setRequestedHeartbeat(5);
15 警告和限制
為了topology恢復成為可能,RabbitMQ Java客戶端維護了一個已聲明的隊列、交換機、和綁定的緩存。每個連接都有自己的緩存。一些RabbitMQ功能讓客戶端有可能觀察topology的變化,比如TTL導致隊列被刪。RabbitMQ Java客戶端在下列情況下嘗試讓緩存項無效:
●?隊列被刪
●?交換機被刪
●?綁定被刪
●?在自動刪除的隊列上消費者被取消
●?當隊列或交換機從一個自動刪除的交換機上解除綁定。
但是,客戶端不能再單個連接之外跟蹤這些topology的變化。那些依賴于自動刪除的隊列或交換機,還有隊列TTL(注意:不是消息TTL),和使用自動連接恢復的應用,應該顯式的刪除那些未使用或已被刪除的實體(即隊列、交換機、綁定等),來清理客戶端的topology緩存。這可以使用“Channel#queueDelete”、“Channel#exchangeDelete”、“Channel#queueUnbind”、和“Channel#exchangeUnbind”來實現,它們在RabbitMQ
3.3.x中是冪等的操作(即多次操作的結果是一樣的,推導出:刪除不存在的不會導致異常)。
16 RPC(請求/響應)模式
為方便編程,Java客戶端API提供了一個“RpcClient”類,它使用一個臨時的應答隊列來提供簡單的符合AMQP 0-9-1的RPC風格的通信設施。
這個類沒有對RPC參數和返回值強制為特殊格式。它簡單的提供了一種機制:發送帶有特定路由關鍵字的消息到一個給定交換機,然后在一個應答隊列上等待響應。
import com.rabbitmq.client.RpcClient;
RpcClient rpc = new RpcClient(channel, exchangeName, routingKey);
(關于這個類如何使用AMQP 0-9-1的實現細節如下:請求消息將“basic.correlation_id”字段設置為在該“RpcClient”實例中是唯一的值,將“basic.reply_to”字段設置為應答隊列的名稱。)
一旦你創建了該類的實例,你可以以如下任意一種方法來發送RPC請求:
●?byte[] primitiveCall(byte[] message);
●?String stringCall(String message)
●?Map mapCall(Map message)
●?Map mapCall(Object[] keyValuePairs)
“primitiveCall”方法傳輸原始的字節數組來作為請求體和響應體。“stringCall”是對“primitiveCall”的方便的包裹(wrapper),把消息體作為默認字符編碼的“String”的實例。
“mapCall”有一點復雜:它把包含普通Java值的“java.util.Map”編碼成AMQP 0-9-1的二進制表格的形式,對響應也是以同樣的方式解碼。(注意,這里所使用的值的類型存在一些限制,參見javadoc)
所有編碼/解碼的比較方便的方式都是使用“primitiveCall”作為傳輸機制的,僅僅是在此之上提供了一個封裝層而已。
17 TLS支持
在客戶端和代理之間的通信可以使用TLS來進行加密。也支持客戶端和服務器之間的身份驗證(即節點認證,peer verification)。下面是Java客戶端使用加密的最簡單的方式:
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5671);
factory.useSslProtocol();
注意,在上面的例子中,客戶端默認是不強制服務端認證的(同行證書鏈驗證:peer certificate chain verification),例子中使用的是“信任所有證書”的“TrustManager”。這在本地開發的時候很方便,但很容易受到中間人攻擊,因此不推薦在生產中使用。要學習更多關于RabbitMQ的TLS支持的信息,參見“TLS指南”。如果你僅僅是想配置Java客戶端(尤其是節點認證和trust?manager部分),請閱讀TLS指南的相關章節。