轉載:架構設計:系統間通信(21)——ActiveMQ的安裝與使用
轉載:ActiveMQ學習心得之ActiveMQ四種存儲器分析
轉載:架構設計:系統間通信(22)——提高ActiveMQ工作性能(上)
轉載:架構設計:系統間通信(23)——提高ActiveMQ工作性能(中)
轉載:架構設計:系統間通信(24)——提高ActiveMQ工作性能(下)
一、ActiveMQ
ActiveMQ是Apache軟件基金會的開源產品,支持AMQP協議、MQTT協議(和XMPP協議作用類似)、Openwire協議和Stomp協議等多種消息協議。並且ActiveMQ完整支持JMS API接口規範,Apache也提供多種其他語言的客戶端,例如:C、C++、C#、Ruby、Perl。
二、ActiveMQ的簡單使用
1. 安裝和啓動ActiveMQ
2. 消息生產者代碼如下:
package com.ljq.durian.test.activemq;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* 消息的生產者(發送者)
*
* @author Administrator
*
*/
public class JMSProducer {
public static void main(String[] args) {
try {
//第一步:建立ConnectionFactory工廠對象,需要填入用戶名、密碼、以及要連接的地址,均使用默認即可,默認端口爲"tcp://localhost:61616"
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD,
"failover:(tcp://localhost:61616)?Randomize=false");
//第二步:通過ConnectionFactory工廠對象我們創建一個Connection連接,並且調用Connection的start方法開啓連接,Connection默認是關閉的。
Connection connection = connectionFactory.createConnection();
connection.start();
//第三步:通過Connection對象創建Session會話(上下文環境對象),用於接收消息,參數配置1爲是否啓用是事務,參數配置2爲簽收模式,一般我們設置自動簽收。
Session session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
//第四步:通過Session創建Destination對象,指的是一個客戶端用來指定生產消息目標和消費消息來源的對象,在PTP模式中,Destination被稱作Queue即隊列;在Pub/Sub模式,Destination被稱作Topic即主題。在程序中可以使用多個Queue和Topic。
Destination destination = session.createQueue("HelloWorld");
//第五步:我們需要通過Session對象創建消息的發送和接收對象(生產者和消費者)MessageProducer/MessageConsumer。
MessageProducer producer = session.createProducer(null);
//第六步:我們可以使用MessageProducer的setDeliveryMode方法爲其設置持久化特性和非持久化特性(DeliveryMode),我們稍後詳細介紹。
//producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//第七步:最後我們使用JMS規範的TextMessage形式創建數據(通過Session對象),並用MessageProducer的send方法發送數據。同理客戶端使用receive方法進行接收數據。最後不要忘記關閉Connection連接。
for(int i = 0 ; i < 10 ; i ++){
TextMessage msg = session.createTextMessage("我是消息內容" + i);
// 第一個參數目標地址
// 第二個參數 具體的數據信息
// 第三個參數 傳送數據的模式
// 第四個參數 優先級
// 第五個參數 消息的過期時間
producer.send(destination, msg, DeliveryMode.NON_PERSISTENT, 0 , 1000L);
System.out.println("發送消息:" + msg.getText());
session.commit(); //啓用事務時記得提交事務,不然消費端接收不到消息
Thread.sleep(1000);
}
if(connection != null){
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. 消息消費者代碼如下:
package com.ljq.durian.test.activemq;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* 消息的消費者(接受者)
*
* @author Administrator
*
*/
public class JMSConsumer {
public static void main(String[] args) {
try {
//第一步:建立ConnectionFactory工廠對象,需要填入用戶名、密碼、以及要連接的地址,均使用默認即可,默認端口爲"tcp://localhost:61616"
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD,
"failover:(tcp://localhost:61616)?Randomize=false");
//第二步:通過ConnectionFactory工廠對象我們創建一個Connection連接,並且調用Connection的start方法開啓連接,Connection默認是關閉的。
Connection connection = connectionFactory.createConnection();
connection.start();
//第三步:通過Connection對象創建Session會話(上下文環境對象),用於接收消息,參數配置1爲是否啓用是事務,參數配置2爲簽收模式,一般我們設置自動簽收。
Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
//第四步:通過Session創建Destination對象,指的是一個客戶端用來指定生產消息目標和消費消息來源的對象,在PTP模式中,Destination被稱作Queue即隊列;在Pub/Sub模式,Destination被稱作Topic即主題。在程序中可以使用多個Queue和Topic。
Destination destination = session.createQueue("HelloWorld");
//第五步:通過Session創建MessageConsumer
MessageConsumer consumer = session.createConsumer(destination);
while(true){
TextMessage msg = (TextMessage)consumer.receive();
if(msg == null) {
break;
}
System.out.println("收到的內容:" + msg.getText());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4. 啓動消息生產者產生消息,可在ActiveMQ的網頁管理中看到消息的狀態。
5. 啓動消息消費者消費消息,可在ActiveMQ的網頁管理中看到消息的狀態。
網上例子較多,公司不能傳圖,留待後補。
三、ActiveMQ的架構
ActiveMQ主要涉及到5個方面:
1. 傳輸協議:消息之間的傳遞,無疑需要協議進行溝通,啓動一個ActiveMQ打開了一個監聽端口, ActiveMQ提供了廣泛的連接模式,其中主要包括SSL、STOMP、XMPP;ActiveMQ默認的使用的協議是openWire,端口號:61616;
2. 消息域:ActiveMQ主要包含Point-to-Point (點對點),Publish/Subscribe Model (發佈/訂閱者),其中在Publich/Subscribe 模式下又有Nondurable subscription和durable subscription (持久化訂閱)2種消息處理方式
3. 消息存儲:在消息傳遞過程中,部分重要的消息可能需要存儲到數據庫或文件系統中,當中介崩潰時,信息不回丟失
4. Cluster (集羣): 最常見到 集羣方式包括network of brokers和Master Slave;
5. Monitor (監控) :ActiveMQ一般由jmx來進行監控
默認配置下的ActiveMQ只適合學習代碼而不適用於實際生產環境,ActiveMQ的性能需要通過配置挖掘,其性能提高包括代碼級性能、規則性能、存儲性能、網絡性能以及多節點協同方法(集羣方案),所以我們優化ActiveMQ的中心思路也是這樣的:
1. 優化ActiveMQ單個節點的性能,包括NIO模型選擇和存儲選擇。
2. 配置ActiveMQ的集羣(ActiveMQ的高性能和高可用需要通過集羣表現出來)。
四、ActiveMQ的通信方式
1. 點對點(p2p)
點對點模式下一條消息將會發送給一個消息消費者,如果當前Queue沒有消息消費者,消息將進行存儲。
點對點方式使用生產者-消費者模式,生產者代碼如下:
public Producer() throws JMSException {
factory = new ActiveMQConnectionFactory(brokerURL);
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
producer = session.createProducer(null);
}
public void sendMessage() throws JMSException {
for(int i = 0; i < jobs.length; i++)
{
String job = jobs[i];
Destination destination = session.createQueue("JOBS." + job);
Message message = session.createObjectMessage(i);
System.out.println("Sending: id: " + ((ObjectMessage)message).getObject() + " on queue: " + destination);
producer.send(destination, message);
}
}
public static void main(String[] args) throws JMSException {
Producer producer = new Producer();
for(int i = 0; i < 10; i++) {
producer.sendMessage();
System.out.println("Produced " + i + " job messages");
try {
Thread.sleep(1000);
} catch (InterruptedException x) {
e.printStackTrace();
}
}
producer.close();
}
生產者將消息放入隊列中,由消費者使用,消費者代碼如下:
public Consumer() throws JMSException {
factory = new ActiveMQConnectionFactory(brokerURL);
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
}
public static void main(String[] args) throws JMSException {
Consumer consumer = new Consumer();
for (String job : consumer.jobs) {
Destination destination = consumer.getSession().createQueue("JOBS." + job);
MessageConsumer messageConsumer = consumer.getSession().createConsumer(destination);
messageConsumer.setMessageListener(new Listener(job));
}
}
public Session getSession() {
return session;
}
具體註冊的對象需要實現MessageListener接口:
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ObjectMessage;
public class Listener implements MessageListener {
private String job;
public Listener(String job) {
this.job = job;
}
public void onMessage(Message message) {
try {
//do something here
System.out.println(job + " id:" + ((ObjectMessage)message).getObject());
} catch (Exception e) {
e.printStackTrace();
}
}
}
2. 發佈-訂閱(publish-subscribe)
“發佈-訂閱”模式下,消息會被複制多份,分別發送給所有“訂閱”者。
Publisher
publisher是屬於發佈信息的一方,它通過定義一個或者多個topic,然後給這些topic發送消息。
public Publisher() throws JMSException {
factory = new ActiveMQConnectionFactory(brokerURL);
connection = factory.createConnection();
try {
connection.start();
} catch (JMSException jmse) {
connection.close();
throw jmse;
}
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
producer = session.createProducer(null);
}
我們按照前面說的流程定義了基本的connectionFactory, connection, session, producer。這裏代碼就是主要實現初始化的效果。接着,我們需要定義一系列的topic讓所有的consumer來訂閱,設置topic的代碼如下:
protected void setTopics(String[] stocks) throws JMSException {
destinations = new Destination[stocks.length];
for(int i = 0; i < stocks.length; i++) {
destinations[i] = session.createTopic("STOCKS." + stocks[i]);
}
}
這裏destinations是一個內部定義的成員變量Destination[]。這裏我們總共定義了的topic數取決於給定的參數stocks。在定義好topic之後我們要給這些指定的topic發消息,具體實現的代碼如下:protected void sendMessage(String[] stocks) throws JMSException {
for(int i = 0; i < stocks.length; i++) {
Message message = createStockMessage(stocks[i], session);
System.out.println("Sending: " + ((ActiveMQMapMessage)message).getContentMap() + " on destination: " + destinations[i]);
producer.send(destinations[i], message);
}
}
protected Message createStockMessage(String stock, Session session) throws JMSException {
MapMessage message = session.createMapMessage();
message.setString("stock", stock);
message.setDouble("price", 1.00);
message.setDouble("offer", 0.01);
message.setBoolean("up", true);
return message;
}
在sendMessage方法裏我們遍歷每個topic,然後給每個topic發送定義的Message消息。在定義好前面發送消息的基礎之後,我們調用他們的代碼就很簡單了:
public static void main(String[] args) throws JMSException {
if(args.length < 1)
throw new IllegalArgumentException();
// Create publisher
Publisher publisher = new Publisher();
// Set topics
publisher.setTopics(args);
for(int i = 0; i < 10; i++) {
publisher.sendMessage(args);
System.out.println("Publisher '" + i + " price messages");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// Close all resources
publisher.close();
}
調用他們的代碼就是我們遍歷所有topic,然後通過sendMessage發送消息。在發送一個消息之後先sleep1秒鐘。要注意的一個地方就是我們使用完資源之後必須要使用close方法將這些資源關閉釋放。close方法關閉資源的具體實現如下:
public void close() throws JMSException {
if (connection != null) {
connection.close();
}
}
ConsumerConsumer的代碼也很類似,具體的步驟無非就是1.初始化資源。 2. 接收消息。 3. 必要的時候關閉資源。初始化資源可以放到構造函數裏面:
public Consumer() throws JMSException {
factory = new ActiveMQConnectionFactory(brokerURL);
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
}
接收和處理消息的方法有兩種,分爲同步和異步的,一般同步的方式我們是通過MessageConsumer.receive()方法來處理接收到的消息。而異步的方法則是通過註冊一個MessageListener的方法,使用MessageConsumer.setMessageListener()。這裏我們採用異步的方式實現:public static void main(String[] args) throws JMSException {
Consumer consumer = new Consumer();
for (String stock : args) {
Destination destination = consumer.getSession().createTopic("STOCKS." + stock);
MessageConsumer messageConsumer = consumer.getSession().createConsumer(destination);
messageConsumer.setMessageListener(new Listener());
}
}
public Session getSession() {
return session;
}
在前面的代碼裏我們先找到同樣的topic,然後遍歷所有的topic去獲得消息。對於消息的處理我們專門通過Listener對象來負責。Listener對象的職責很簡單,主要就是處理接收到的消息:
public class Listener implements MessageListener {
public void onMessage(Message message) {
try {
MapMessage map = (MapMessage)message;
String stock = map.getString("stock");
double price = map.getDouble("price");
double offer = map.getDouble("offer");
boolean up = map.getBoolean("up");
DecimalFormat df = new DecimalFormat( "#,###,###,##0.00" );
System.out.println(stock + "\t" + df.format(price) + "\t" + df.format(offer) + "\t" + (up?"up":"down"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
它實現了MessageListener接口,裏面的onMessage方法就是在接收到消息之後會被調用的方法。3. 請求-響應(request-response)
和前面兩種方式比較起來,request-response的通信方式很常見,但是不是默認提供的一種模式。在前面的兩種模式中都是一方負責發送消息而另外一方負責處理。而我們實際中的很多應用相當於一種一應一答的過程,需要雙方都能給對方發送消息。於是請求-應答的這種通信方式也很重要。它也應用的很普遍。
請求-應答方式並不是JMS規範系統默認提供的一種通信方式,而是通過在現有通信方式的基礎上稍微運用一點技巧實現的。下圖是典型的請求-應答方式的交互過程:
在JMS裏面,如果要實現請求/應答的方式,可以利用JMSReplyTo和JMSCorrelationID消息頭來將通信的雙方關聯起來。另外,QueueRequestor和TopicRequestor能夠支持簡單的請求/應答過程。現在,如果我們要實現這麼一個過程,在發送請求消息並且等待返回結果的client端的流程如下:
// client side
Destination tempDest = session.createTemporaryQueue();
MessageConsumer responseConsumer = session.createConsumer(tempDest);
...
// send a request..
message.setJMSReplyTo(tempDest)
message.setJMSCorrelationID(myCorrelationID);
producer.send(message);
client端創建一個臨時隊列並在發送的消息裏指定了發送返回消息的destination以及correlationID。那麼在處理消息的server端得到這個消息後就知道該發送給誰了。Server端的大致流程如下:
public void onMessage(Message request) {
Message response = session.createMessage();
response.setJMSCorrelationID(request.getJMSCorrelationID())
producer.send(request.getJMSReplyTo(), response)
}
這裏我們是用server端註冊MessageListener,通過設置返回信息的CorrelationID和JMSReplyTo將信息返回。以上就是發送和接收消息的雙方的大致程序結構。具體的實現代碼如下:
Client側實現
public Client() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection;
try {
connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(transacted, ackMode);
Destination adminQueue = session.createQueue(clientQueueName);
//Setup a message producer to send message to the queue the server is consuming from
this.producer = session.createProducer(adminQueue);
this.producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//Create a temporary queue that this client will listen for responses on then create a consumer
//that consumes message from this temporary queue...for a real application a client should reuse
//the same temp queue for each message to the server...one temp queue per client
Destination tempDest = session.createTemporaryQueue();
MessageConsumer responseConsumer = session.createConsumer(tempDest);
//This class will handle the messages to the temp queue as well
responseConsumer.setMessageListener(this);
//Now create the actual message you want to send
TextMessage txtMessage = session.createTextMessage();
txtMessage.setText("MyProtocolMessage");
//Set the reply to field to the temp queue you created above, this is the queue the server
//will respond to
txtMessage.setJMSReplyTo(tempDest);
//Set a correlation ID so when you get a response you know which sent message the response is for
//If there is never more than one outstanding message to the server then the
//same correlation ID can be used for all the messages...if there is more than one outstanding
//message to the server you would presumably want to associate the correlation ID with this
//message somehow...a Map works good
String correlationId = this.createRandomString();
txtMessage.setJMSCorrelationID(correlationId);
this.producer.send(txtMessage);
} catch (JMSException e) {
//Handle the exception appropriately
}
}
這裏的代碼除了初始化構造函數裏的參數還同時設置了兩個destination,一個是自己要發送消息出去的destination,在這一句設置:
session.createProducer(adminQueue);
另外一個是自己要接收的消息destination, 通過這兩句指定了要接收消息的目的地:Destination tempDest = session.createTemporaryQueue();
responseConsumer = session.createConsumer(tempDest);
這裏是用的一個臨時隊列。在前面指定了返回消息的通信隊列之後,我們需要通知server端知道發送返回消息給哪個隊列。於是
txtMessage.setJMSReplyTo(tempDest);
指定了這一部分,同時:
txtMessage.setJMSCorrelationID(correlationId);
方法主要是爲了保證每次發送回來請求的server端能夠知道對應的是哪個請求。這裏一個請求和一個應答是相當於對應一個相同的序列號一樣。
因爲client端在發送消息之後還要接收server端返回的消息,所以它也要實現一個消息receiver的功能。這裏採用實現MessageListener接口的方式:
public void onMessage(Message message) {
String messageText = null;
try {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
messageText = textMessage.getText();
System.out.println("messageText = " + messageText);
}
} catch (JMSException e) {
//Handle the exception appropriately
}
}
Server側實現server端要執行的過程和client端相反,它是先接收消息,在接收到消息後根據提供的JMSCorelationID來發送返回的消息:
public void onMessage(Message message) {
try {
TextMessage response = this.session.createTextMessage();
if (message instanceof TextMessage) {
TextMessage txtMsg = (TextMessage) message;
String messageText = txtMsg.getText();
response.setText(this.messageProtocol.handleProtocolMessage(messageText));
}
//Set the correlation ID from the received message to be the correlation id of the response message
//this lets the client identify which message this is a response to if it has more than
//one outstanding message to the server
response.setJMSCorrelationID(message.getJMSCorrelationID());
//Send the response to the Destination specified by the JMSReplyTo field of the received message,
//this is presumably a temporary queue created by the client
this.replyProducer.send(message.getJMSReplyTo(), response);
} catch (JMSException e) {
//Handle the exception appropriately
}
}
在replyProducer.send()方法裏,message.getJMSReplyTo()就得到了要發送消息回去的destination。另外,設置這些發送返回信息的replyProducer的信息主要在構造函數相關的方法裏實現了:
public Server() {
try {
//This message broker is embedded
BrokerService broker = new BrokerService();
broker.setPersistent(false);
broker.setUseJmx(false);
broker.addConnector(messageBrokerUrl);
broker.start();
} catch (Exception e) {
//Handle the exception appropriately
}
//Delegating the handling of messages to another class, instantiate it before setting up JMS so it
//is ready to handle messages
this.messageProtocol = new MessageProtocol();
this.setupMessageQueueConsumer();
}
private void setupMessageQueueConsumer() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(messageBrokerUrl);
Connection connection;
try {
connection = connectionFactory.createConnection();
connection.start();
this.session = connection.createSession(this.transacted, ackMode);
Destination adminQueue = this.session.createQueue(messageQueueName);
//Setup a message producer to respond to messages from clients, we will get the destination
//to send to from the JMSReplyTo header field from a Message
this.replyProducer = this.session.createProducer(null);
this.replyProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//Set up a consumer to consume messages off of the admin queue
MessageConsumer consumer = this.session.createConsumer(adminQueue);
consumer.setMessageListener(this);
} catch (JMSException e) {
//Handle the exception appropriately
}
}
總體來說,整個的交互過程並不複雜,只是比較繁瑣。
對於請求/應答的方式來說,這種典型交互的過程就是Client端在設定正常發送請求的Queue同時也設定一個臨時的Queue。同時在要發送的message裏頭指定要返回消息的destination以及CorelationID,這些就好比是一封信裏面所帶的回執。根據這個信息服務器才知道怎麼給客戶端回信。
對於Server端來說則要額外創建一個producer,在處理接收到消息的方法裏再利用producer將消息發回去。這一系列的過程看起來很像http協議裏面請求-應答的方式,都是一問一答。
五、ActiveMQ的存儲
1. 持久化消息和非持久化消息
JMS中對非持久化消息和非持久化消息的稱呼分別是:NON_PERSISTENTMessage和PERSISTENT
Meaage。它們指的是消息在任何一種“發送-接受”模式下(“訂閱-發佈”模式和“負載均衡模式”),是否進行持久化存儲。
NON_PERSISTENT Message只存儲在JMS服務節點的內存區域,不會存儲在某種持久化介質上(AcitveMQ可支持的持久化介質有:KahaBD、AMQ和關係型數據)。在極限情況下,JMS服務節點的內存區域不夠使用了,也只會採用某種輔助方案進行轉存(例如ActiveMQ會使用磁盤上的一個“臨時存儲區域”進行暫存)。一旦JMS服務節點宕機了,這些NON_PERSISTENT
Message就會丟失。
JMS中對PERSISTENT Meaage的定義是:這些消息不受JMS服務端異常狀態的影響,JMS服務端會使用某種持久化存儲方案保存這些消息,直到JMS服務端認爲這些PERSISTENTMeaage被消費端成功處理。例如ActiveMQ中可以選擇的持久化存儲方案就包括:KahaDB、AMQ和關係型數據庫。
在JMS標準API中,使用setDeliveryMode標記消息發送者是發送的PERSISTENT Meaage還是NON_PERSISTENT Message。示例如下:
......
for(int index = 0 ; index < 10 ; index++) {
TextMessage outMessage = session.createTextMessage();
outMessage.setText("這是發送的消息內容:" + index);
if(index % 2 == 0) {
sender.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
} else {
sender.setDeliveryMode(DeliveryMode.PERSISTENT);
}
sender.send(outMessage);
}
......
那麼當JMS服務節點重啓後(注意不是producer重啓),以上代碼中發送的10條消息只有其中5條消息能夠保存下來。
發送NON_PERSISTENT Message時,消息發送方默認使用異步方式:即是說消息發送後發送方不會等待NON_PERSISTENT Message在服務端的任何回執。那麼問題來了:如果這時服務端已經出現了消息堆積,並且堆積程度已經達到“無法再接收新消息”的極限情況了,那麼消息發送方如果知曉並採取相應的策略呢?
實際上所謂的異步發送也並非絕對的異步,消息發送者會在發送一定大小的消息後等待服務端進行回執(這個配置只是針對使用異步方式進行發送消息的情況)
......
// 以下語句設置消息發送者在累計發送102400byte大小的消息後(可能是一條消息也可能是多條消息)
// 等待服務端進行回執,以便確定之前發送的消息是否被正確處理
// 確定服務器端是否產生了過量的消息堆積,需要減慢消息生產端的生產速度
connectionFactory.setProducerWindowSize(102400);
......
以上這個過程非常耗時,ActiveMQ服務端不但要接受消息,在內存中完成存儲,並且按照ActiveMQ服務端設置的持久化存儲方案對消息進行存儲(主要的處理時間耗費在這裏)。爲了提高ActiveMQ在接受PERSISTENT Meaage時的性能,ActiveMQ允許開發人員遵從JMS API中的設置方式,爲消息發送端在發送PERSISTENT Meaage時提供異步方式:
......
// 使用異步傳輸
// 上文已經說過,如果發送的是NON_PERSISTENT Message
// 那麼默認就是異步方式
connectionFactory.setUseAsyncSend(true);
......
一旦您進行了這樣的設置,就需要設置回執窗口:
......
// 同樣設置消息發送者在累計發送102400byte大小的消息後
// 等待服務端進行回執,以便確定之前發送的消息是否被正確處理
// 確定服務器端是否產生了過量的消息堆積,需要減慢消息生產端的生產速度
connectionFactory.setProducerWindowSize(102400);
......
2. 持久化訂閱和非持久化訂閱
持續訂閱和非持續訂閱,是針對“訂閱-發佈”模式的細分處理策略,在JMS規範中的標準稱呼是:Durable-Subscribers和Non-Durable Subscribers。
Durable-Subscribers是指在“訂閱-發佈”模式下,即使標記爲Durable-Subscribers的訂閱者下線了(可能是因爲訂閱者宕機,也可能是因爲這個訂閱者故意下線),“訂閱-發佈”模式的Topic隊列也要保存這些消息(視消息不同的持久化策略影響,保存機制不一樣),直到下次這個被標記爲Durable-Subscribers的訂閱者重新上線,並正確處理這條消息爲止。換句話說,標記爲Durable-Subscribers的訂閱者是否能獲得某條消息,和它是否曾經下線沒有任何關係。
Non-Durable Subscribers是指在“訂閱-發佈”模式下,“訂閱-發佈”模式的Topic隊列不用爲這些已經下線的訂閱者保留消息。當後者將消息按照既定的廣播規則發送給當前在線的訂閱者後,消息就可以被標記爲“處理完成”。
3. ActiveMQ的存儲機制
ActiveMQ 在 隊列中存儲 Message 時,採用先進先出順序(FIFO)存儲。同一時間一個消息被分派給單個消費者,且只有當 Message 被消費並確認時,它才能從存儲中刪除。
對於持久化訂閱者來說,每個消費者獲得 Message 的副本。爲了節省存儲空間,Provider 僅存儲消息的一個副本。持久化訂閱者維護了指向下一個 Message 的指針,並將其副本分派給消費者。以這種方式實現消息存儲,因爲每個持久化訂閱者可能以不同的速率消費
Message,或者它們可能不是全部同時運行。此外,因每個 Message 可能存在多個消費者,所以在它被成功地傳遞給所有持久化訂閱者之前,不能從存儲中刪除。
關於持久化和消息的保留見下表:
消息類型 | 是否持久化 | 是否有Durable訂閱者 | 消費者延遲啓動時,消息是否保留 | Broker重啓時,消息是否保留 |
Queue | N | - | Y | N |
Queue | Y | - | Y | Y |
Topic | N | N | N | N |
Topic | N | Y | Y | N |
Topic | Y | N | N | N |
Topic | Y | Y | Y | Y |
ActiveMQ有四種存儲器,下面分別介紹和分析各自的特點和優缺點。
1、KahaDB message store:
是ActiveMQ的默認以及推薦的存儲器,特點是基於文件、支持事務日誌、可靠、可擴展、速度快等。重點討論一下後兩點。
KahaDB主要元素包括:一個內存Metadata Cache用來在內存中檢索消息的存儲位置、若干用於記錄消息內容的Data log文件、一個在磁盤上檢索消息存儲位置的Metadata Store、還有一個用於在系統異常關閉後恢復Btree結構的redo文件。
a. 可擴展體現在KahaDB支持其他三種存儲器的外接擴展,也就是說可以同時用不止一種,這樣可以取長補短,適合更廣的應用場景,達到性能最佳。
b. 速度快:(1)快速的事務日誌;(2)高度優化的消息ID索引;(3)在內存中的消息緩存。具體分析,消息直接添加在當前日誌文件的尾部,所以存的快(類似Redis的Aof);用一個索引文件存儲所有的destination,可謂高度優化;支持內存緩存也是必然,但在緩存回覆策略上不如內存存儲器。
<broker brokerName="broker" persistent="true" useShutdownHook="false">
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb" journalMaxFileLength="16mb"/>
</persistenceAdapter>
</broker>
2、AMQ message store:
在基於文件、支持事務方面和KahaDB類似。不同之處如下:
優點:索引用的是hashbin(哈希桶,沒有查到權威定義,可理解爲哈希表),自然比KahaDB的Btree索引要快,並且磁盤讀寫用的是nio,速度也快,所以用於消息吞吐量要求比較大的時候是最佳選擇。(有的人把吞吐量理解成消息總數量其實不正確,應該是消息出入隊的速率。)
缺點:對於每個destination都要建一個索引,所以不適於很多destination併發的場合,而這恰恰是KahaDB的優勢,它可以支持最大10000個queue的同時等待。(AMQ爲每個索引使用兩個分開的文件,並且每個 Destination 都有一個索引,所以當你打算在代理中使用數千個隊列的時候,不應該使用它。)
<persistenceAdapter>
<amqPersistenceAdapter
directory="${activemq.data}/kahadb"
syncOnWrite="true"
indexPageSize="16kb"
indexMaxBinSize="100"
maxFileLength="10mb" />
</persistenceAdapter>
3、JDBC message store:
默認的JDBC驅動是ApacheDerby,同時支持MySQL、PostgreSQL、Oracle、SQLServer、Sybase、Informix、MaxDB等主流的關係數據庫。用三張表結構來存儲消息,分別是ACTIVEMQ_MSGS、ACTIVEMQ_ACKS、ACTIVEMQ_LOCK。第二張表外鍵關聯到第一張表,共同存儲消息,第三張表用於鎖定保證只有一個broker實例可以訪問數據庫。選擇關係型數據庫,通常的原因是企業已經具備了管理關係型數據的專長,但是它在性能上絕對不優於上述消息存儲實現。
<beans>
<broker brokerName="test-broker" persistent="true" xmlns="http://activemq.apache.org/schema/core">
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds"/>
</persistenceAdapter>
</broker>
<bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/>
<property name="username" value="activemq"/>
<property name="password" value="activemq"/>
<property name="maxActive" value="200"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
</beans>
4、Memory message store:
用於實時消息的緩存,只針對非持久訂閱的消費者提供了5種訂閱恢復策略,可以極大程度增強非持久訂閱的可用性。也就是說對於持久訂閱的消費者是用不到內存存儲的。
<broker brokerName="test-broker" persistent="false" xmlns="http://activemq.apache.org/schema/core">
<transportConnectors>
<transportConnector uri="tcp://localhost:61635"/>
</transportConnectors>
</broker>
5. LevelDB方式
從ActiveMQ 5.6版本之後,又推出了LevelDB的持久化引擎。目前默認的持久化方式仍然是KahaDB,不過LevelDB持久化性能高於KahaDB,可能是以後的趨勢。
在ActiveMQ 5.9版本提供了基於LevelDB和Zookeeper的數據複製方式,用於Master-slave方式的首選數據複製方案。
五、ActiveMQ的消息傳輸機制
1. 整體架構
Producer客戶端使用來發送消息的, Consumer客戶端用來消費消息;它們的協同中心就是ActiveMQ broker,broker也是讓producer和consumer調用過程解耦的工具,最終實現了異步RPC/數據交換的功能。隨着ActiveMQ的不斷髮展,支持了越來越多的特性,也解決開發者在各種場景下使用ActiveMQ的需求。比如producer支持異步調用;使用flow control機制讓broker協同consumer的消費速率;consumer端可以使用prefetchACK來最大化消息消費的速率;提供"重發策略"等來提高消息的安全性等。一條消息的生命週期如下:
圖片中簡單的描述了一條消息的生命週期,不過在不同的架構環境中,message的流動行可能更加複雜.將在稍後有關broker的架構中詳解..一條消息從producer端發出之後,一旦被broker正確保存,那麼它將會被consumer消費,然後ACK,broker端纔會刪除;不過當消息過期或者存儲設備溢出時,也會終結它。
這是一張很複雜,而且有些凌亂的圖片;這張圖片中簡單的描述了:1)producer端如何發送消息 2) consumer端如何消費消息 3) broker端如何調度。
2. optimizeACK
"可優化的ACK",這是ActiveMQ對於consumer在消息消費時,對消息ACK的優化選項,也是consumer端最重要的優化參數之一,你可以通過如下方式開啓:
1) 在brokerUrl中增加如下查詢字符串:
String brokerUrl = "tcp://localhost:61616?" +
"jms.optimizeAcknowledge=true" +
"&jms.optimizeAcknowledgeTimeOut=30000" +
"&jms.redeliveryPolicy.maximumRedeliveries=6";
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerUrl);
2) 在destinationUri中,增加如下查詢字符串:
String queueName = "test-queue?customer.prefetchSize=100";
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination queue = session.createQueue(queueName);
我們需要在brokerUrl指定optimizeACK選項,在destinationUri中指定prefetchSize(預獲取)選項,其中brokerUrl參數選項是全局的,即當前factory下所有的connection/session/consumer都會默認使用這些值;而destinationUri中的選項,只會在使用此destination的consumer實例中有效;如果同時指定,brokerUrl中的參數選項值將會被覆蓋。
optimizeAck表示是否開啓“優化ACK”,只有在爲true的情況下,prefetchSize(下文中將會簡寫成prefetch)以及optimizeAcknowledgeTimeout參數纔會有意義。此處需要注意"optimizeAcknowledgeTimeout"選項只能在brokerUrl中配置。
prefetch值建議在destinationUri中指定,因爲在brokerUrl中指定比較繁瑣;
在brokerUrl中,queuePrefetchSize和topicPrefetchSize都需要單獨設定:
"&jms.prefetchPolicy.queuePrefetch=12&jms.prefetchPolicy.topicPrefetch=12"
等來逐個指定。
2.1 prefetchACK和prefetch
如果prefetchACK爲true,那麼prefetch必須大於0;當prefetchACK爲false時,你可以指定prefetch爲0以及任意大小的正數。
- 1. 當prefetch=0是,表示consumer將使用PULL(拉取)的方式從broker端獲取消息,broker端將不會主動push消息給client端,直到client端發送PullCommand時;
- 2. 當prefetch>0時,就開啓了broker push模式,此後只要當client端消費且ACK了一定的消息之後,會立即push給client端多條消息。
當consumer端使用receive()方法同步獲取消息時,prefetch可以爲0和任意正值:
- 1. 當prefetch=0時,那麼receive()方法將會首先發送一個PULL指令並阻塞,直到broker端返回消息爲止,這也意味着消息只能逐個獲取(類似於Request<->Response),這也是Activemq中PULL消息模式;
- 2. 當prefetch > 0時,broker端將會批量push給client 一定數量的消息(<= prefetch),client端會把這些消息(unconsumedMessage)放入到本地的隊列中,只要此隊列有消息,那麼receive方法將會立即返回,當一定量的消息ACK之後,broker端會繼續批量push消息給client端。
當consumer端使用MessageListener異步獲取消息時,這就需要開發設定的prefetch值必須 >=1,即至少爲1;在異步消費消息模式中,設定prefetch=0,是相悖的,也將獲得一個Exception。
2.2 redelivery
此外,我們還可以brokerUrl中配置“redelivery”策略,比如當一條消息處理異常時,broker端可以重發的最大次數;和下文中提到REDELIVERED_ACK_TYPE互相協同。當消息需要broker端重發時,consumer會首先在本地的“deliveredMessage隊列”(Consumer已經接收但還未確認的消息隊列)刪除它,然後向broker發送“REDELIVERED_ACK_TYPE”類型的確認指令,broker將會把指令中指定的消息重新添加到pendingQueue(亟待發送給consumer的消息隊列)中,直到合適的時機,再次push給client。
2.3 optimizeACK和prefetch模型
到目前爲止,或許你知道了optimizeACK和prefeth的大概意義,不過我們可能還會有些疑惑!!optimizeACK和prefetch配合,將會達成一個高效的消息消費模型:批量獲取消息,並“延遲”確認(ACK)。prefetch表達了“批量獲取”消息的語義,broker端主動的批量push多條消息給client端,總比client多次發送PULL指令然後broker返回一條消息的方式要優秀很多,它不僅減少了client端在獲取消息時阻塞的次數和阻塞的時間,還能夠大大的減少網絡開支。optimizeACK表達了“延遲確認”的語義(ACK時機),client端在消費消息後暫且不發送ACK,而是把它緩存下來(pendingACK),等到這些消息的條數達到一定閥值時,只需要通過一個ACK指令把它們全部確認;這比對每條消息都逐個確認,在性能上要提高很多。由此可見,prefetch優化了消息傳送的性能,optimizeACK優化了消息確認的性能。
2.4 optimizeACK和prefetch模型的例外情況
當consumer端消息消費的速率很高(相對於producer生產消息),而且消息的數量也很大時(比如消息源源不斷的生產),我們使用optimizeACK + prefetch將會極大的提升consumer的性能。不過反過來:
1) 如果consumer端消費速度很慢(對消息的處理是耗時的),過大的prefetchSize,並不能有效的提升性能,反而不利於consumer端的負載均衡(只針對queue);按照良好的設計準則,當consumer消費速度很慢時,我們通常會部署多個consumer客戶端,並使用較小的prefetch,同時關閉optimizeACK,可以讓消息在多個consumer間“負載均衡”(即均勻的發送給每個consumer);如果較大的prefetchSize,將會導致broker一次性push給client大量的消息,但是這些消息需要很久才能ACK(消息積壓),而且在client故障時,還會導致這些消息的重發。
2) 如果consumer端消費速度很快,但是producer端生成消息的速率較慢,比如生產者10秒鐘生成10條消息,但是consumer一秒就能消費完畢,而且我們還部署了多個consumer!!這種場景下,建議開啓optimizeACK,但是需要設置的prefetchSize不能過大;這樣可以保證每個consumer都能有"活幹",否則將會出現一個consumer非常忙碌,但是其他consumer幾乎收不到消息。
3) 如果消息很重要,特別是不願意接收到”redelivery“的消息,那麼我們需要將optimizeACK=false,prefetchSize=1
既然optimizeACK是”延遲“確認,那麼就引入一種潛在的風險:在消息被消費之後還沒有來得及確認時,client端發生故障,那麼這些消息就有可能會被重新發送給其他consumer,那麼這種風險就需要client端能夠容忍“重複”消息。
2.5 定製prefetchSize
prefetch值默認爲1000,當然這個值可能在很多場景下是偏大的;我們暫且不考慮ACK模式,通常情況下,我們只需要簡單的統計出單個consumer每秒的最大消費消息數即可,比如一個consumer每秒可以處理100個消息,我們期望consumer端每2秒確認一次,那麼我們的prefetchSize可以設置爲100 * 2 /0.65大概爲300。無論如何設定此值,client持有的消息條數最大爲:prefetch + “DELIVERED_ACK_TYPE消息條數”(DELIVERED_ACK_TYPE參見下文)即使當optimizeACK爲true,也只會當session的ACK模式爲AUTO_ACKNOWLEDGE時纔會生效,即在其他類型的ACK模式時consumer端仍然不會“延遲確認”,即:
consumer.optimizeAck = connection.optimizeACK && session.isAutoAcknowledge()
當consumer.optimizeACK有效時,如果客戶端已經消費但尚未確認的消息(deliveredMessage)達到prefetch * 0.65,consumer端將會自動進行ACK;同時如果離上一次ACK的時間間隔,已經超過"optimizeAcknowledgeTimout"毫秒,也會導致自動進行ACK。此外簡單的補充一下,批量確認消息時,只需要在ACK指令中指明“firstMessageId”和“lastMessageId”即可,即消息區間,那麼broker端就知道此consumer(根據consumerId識別)需要確認哪些消息。
3. ACK模式與類型介紹
3.1 ACK類型
JMS API中約定了Client端可以使用四種ACK模式,在javax.jms.Session接口中:
- AUTO_ACKNOWLEDGE = 1 自動確認
- CLIENT_ACKNOWLEDGE = 2 客戶端手動確認
- DUPS_OK_ACKNOWLEDGE = 3 自動批量確認
- SESSION_TRANSACTED = 0 事務提交併確認
- INDIVIDUAL_ACKNOWLEDGE = 4 單條消息確認
ACK模式描述了Consumer與broker確認消息的方式(時機),比如當消息被Consumer接收之後,Consumer將在何時確認消息。對於broker而言,只有接收到ACK指令,纔會認爲消息被正確的接收或者處理成功了,通過ACK,可以在consumer(/producer)與Broker之間建立一種簡單的“擔保”機制.
AUTO_ACKNOWLEDGE:
自動確認,這就意味着消息的確認時機將有consumer擇機確認."擇機確認"似乎充滿了不確定性,這也意味着,開發者必須明確知道"擇機確認"的具體時機,否則將有可能導致消息的丟失,或者消息的重複接收.那麼在ActiveMQ中,AUTO_ACKNOWLEDGE是如何運作的呢?
1) 對於consumer而言,optimizeAcknowledge屬性只會在AUTO_ACK模式下有效。
2) 其中DUPS_ACKNOWLEGE也是一種潛在的AUTO_ACK,只是確認消息的條數和時間上有所不同。
3) 在“同步”(receive)方法返回message之前,會檢測optimizeACK選項是否開啓,如果沒有開啓,此單條消息將立即確認,所以在這種情況下,message返回之後,如果開發者在處理message過程中出現異常,會導致此消息也不會redelivery,即"潛在的消息丟失";如果開啓了optimizeACK,則會在unAck數量達到prefetch * 0.65時確認,當然我們可以指定prefetchSize
= 1來實現逐條消息確認。
4) 在"異步"(messageListener)方式中,將會首先調用listener.onMessage(message),此後再ACK,
如果onMessage方法異常,將導致client端補充發送一個ACK_TYPE爲REDELIVERED_ACK_TYPE確認指令;
如果onMessage方法正常,消息將會正常確認(STANDARD_ACK_TYPE)。此外需要注意,消息的重發次數是有限制的,每條消息中都會包含“redeliveryCounter”計數器,用來表示此消息已經被重發的次數,如果重發次數達到閥值,將會導致發送一個ACK_TYPE爲POSION_ACK_TYPE確認指令,這就導致broker端認爲此消息無法消費,此消息將會被刪除或者遷移到"dead
letter"通道中。
因此當我們使用messageListener方式消費消息時,通常建議在onMessage方法中使用try-catch,這樣可以在處理消息出錯時記錄一些信息,而不是讓consumer不斷去重發消息;如果你沒有使用try-catch,就有可能會因爲異常而導致消息重複接收的問題,需要注意你的onMessage方法中邏輯是否能夠兼容對重複消息的判斷。
CLIENT_ACKNOWLEDGE :
客戶端手動確認,這就意味着AcitveMQ將不會“自作主張”的爲你ACK任何消息,開發者需要自己擇機確認。在此模式下,開發者需要需要關注幾個方法:
1) message.acknowledge(),
2) ActiveMQMessageConsumer.acknowledege(),
3) ActiveMQSession.acknowledge();
其1)和3)是等效的,將當前session中所有consumer中尚未ACK的消息都一起確認,2)只會對當前consumer中那些尚未確認的消息進行確認。開發者可以在合適的時機必須調用一次上述方法。爲了避免混亂,對於這種ACK模式下,建議一個session下只有一個consumer。
我們通常會在基於Group(消息分組)情況下會使用CLIENT_ACKNOWLEDGE,我們將在一個group的消息序列接受完畢之後確認消息(組);不過當你認爲消息很重要,只有當消息被正確處理之後才能確認時,也可以使用此模式 。
如果開發者忘記調用acknowledge方法,將會導致當consumer重啓後,會接受到重複消息,因爲對於broker而言,那些尚未真正ACK的消息被視爲“未消費”。
開發者可以在當前消息處理成功之後,立即調用message.acknowledge()方法來"逐個"確認消息,這樣可以儘可能的減少因網絡故障而導致消息重發的個數;當然也可以處理多條消息之後,間歇性的調用acknowledge方法來一次確認多條消息,減少ack的次數來提升consumer的效率,不過這仍然是一個利弊權衡的問題。
除了message.acknowledge()方法之外,ActiveMQMessageConumser.acknowledge()和ActiveMQSession.acknowledge()也可以確認消息,只不過前者只會確認當前consumer中的消息。其中sesson.acknowledge()和message.acknowledge()是等效的。
無論是“同步”/“異步”,ActiveMQ都不會發送STANDARD_ACK_TYPE,直到message.acknowledge()調用。如果在client端未確認的消息個數達到prefetchSize * 0.5時,會補充發送一個ACK_TYPE爲DELIVERED_ACK_TYPE的確認指令,這會觸發broker端可以繼續push消息到client端。(參看PrefetchSubscription.acknwoledge方法)
在broker端,針對每個Consumer,都會保存一個因爲"DELIVERED_ACK_TYPE"而“拖延”的消息個數,這個參數爲prefetchExtension,事實上這個值不會大於prefetchSize * 0.5,因爲Consumer端會嚴格控制DELIVERED_ACK_TYPE指令發送的時機(參見ActiveMQMessageConsumer.ackLater方法),broker端通過“prefetchExtension”與prefetchSize互相配合,來決定即將push給client端的消息個數,count
= prefetchExtension + prefetchSize - dispatched.size(),其中dispatched表示已經發送給client端但是還沒有“STANDARD_ACK_TYPE”的消息總量;由此可見,在CLIENT_ACK模式下,足夠快速的調用acknowledge()方法是決定consumer端消費消息的速率;如果client端因爲某種原因導致acknowledge方法未被執行,將導致大量消息不能被確認,broker端將不會push消息,事實上client端將處於“假死”狀態,而無法繼續消費消息。我們要求client端在消費1.5*prefetchSize個消息之前,必須acknowledge()一次;通常我們總是每消費一個消息調用一次,這是一種良好的設計。
此外需要額外的補充一下:所有ACK指令都是依次發送給broker端,在CLIET_ACK模式下,消息在交付給listener之前,都會首先創建一個DELIVERED_ACK_TYPE的ACK指令,直到client端未確認的消息達到"prefetchSize * 0.5"時纔會發送此ACK指令,如果在此之前,開發者調用了acknowledge()方法,會導致消息直接被確認(STANDARD_ACK_TYPE)。broker端通常會認爲“DELIVERED_ACK_TYPE”確認指令是一種“slow
consumer”信號,如果consumer不能及時的對消息進行acknowledge而導致broker端阻塞,那麼此consumer將會被標記爲“slow”,此後queue中的消息將會轉發給其他Consumer。
DUPS_OK_ACKNOWLEDGE :
"消息可重複"確認,意思是此模式下,可能會出現重複消息,並不是一條消息需要發送多次ACK纔行。它是一種潛在的"AUTO_ACK"確認機制,爲批量確認而生,而且具有“延遲”確認的特點。
對於開發者而言,這種模式下的代碼結構和AUTO_ACKNOWLEDGE一樣,不需要像CLIENT_ACKNOWLEDGE那樣調用acknowledge()方法來確認消息。
1) 在ActiveMQ中,如果在Destination是Queue通道,我們真的可以認爲DUPS_OK_ACK就是“AUTO_ACK+
optimizeACK + (prefetch > 0)”這種情況,在確認時機上幾乎完全一致;此外在此模式下,如果prefetchSize =1 或者沒有開啓optimizeACK,也會導致消息逐條確認,從而失去批量確認的特性。
2) 如果Destination爲Topic,DUPS_OK_ACKNOWLEDGE纔會產生JMS規範中詮釋的意義,即無論optimizeACK是否開啓,都會在消費的消息個數>=prefetch * 0.5時,批量確認(STANDARD_ACK_TYPE),在此過程中,不會發送DELIVERED_ACK_TYPE的確認指令,這是1)和AUTO_ACK的最大的區別。
這也意味着,當consumer故障重啓後,那些尚未ACK的消息會重新發送過來。
SESSION_TRANSACTED :
當session使用事務時,就是使用此模式。在事務開啓之後,和session.commit()之前,所有消費的消息,要麼全部正常確認,要麼全部redelivery。這種嚴謹性,通常在基於GROUP(消息分組)或者其他場景下特別適合。
在SESSION_TRANSACTED模式下,optimizeACK並不能發揮任何效果,因爲在此模式下,optimizeACK會被強制設定爲false,不過prefetch仍然可以決定DELIVERED_ACK_TYPE的發送時機。
因爲Session非線程安全,那麼當前session下所有的consumer都會共享同一個transactionContext;同時建議,一個事務類型的Session中只有一個Consumer,以避免rollback()或者commit()方法被多個consumer調用而造成的消息混亂。
當consumer接受到消息之後,首先檢測TransactionContext是否已經開啓,如果沒有,就會開啓並生成新的transactionId,並把信息發送給broker;此後將檢測事務中已經消費的消息個數是否 >= prefetch * 0.5,如果大於則補充發送一“DELIVERED_ACK_TYPE”的確認指令;這時就開始調用onMessage()方法,如果是同步(receive),那麼即返回message。上述過程,和其他確認模式沒有任何特殊的地方。
當開發者決定事務可以提交時,必須調用session.commit()方法,commit方法將會導致當前session的事務中所有消息立即被確認;事務的確認過程中,首先把本地的deliveredMessage隊列中尚未確認的消息全部確認(STANDARD_ACK_TYPE);此後向broker發送transaction提交指令並等待broker反饋,如果broker端事務操作成功,那麼將會把本地deliveredMessage隊列清空,新的事務開始;如果broker端事務操作失敗(此時broker已經rollback),那麼對於session而言,將執行inner-rollback,這個rollback所做的事情,就是將當前事務中的消息清空並要求broker重發(REDELIVERED_ACK_TYPE),同時commit方法將拋出異常。
當session.commit方法異常時,對於開發者而言通常是調用session.rollback()回滾事務(事實上開發者不調用也沒有問題),當然你可以在事務開始之後的任何時機調用rollback(),rollback意味着當前事務的結束,事務中所有的消息都將被重發。需要注意,無論是inner-rollback還是調用session.rollback()而導致消息重發,都會導致message.redeliveryCounter計數器增加,最終都會受限於brokerUrl中配置的"jms.redeliveryPolicy.maximumRedeliveries",如果rollback的次數過多,而達到重發次數的上限時,消息將會被DLQ(dead
letter)。
INDIVIDUAL_ACKNOWLEDGE :
單條消息確認,這種確認模式,我們很少使用,它的確認時機和CLIENT_ACKNOWLEDGE幾乎一樣,當消息消費成功之後,需要調用message.acknowledege來確認此消息(單條),而CLIENT_ACKNOWLEDGE模式先message.acknowledge()方法將導致整個session中所有消息被確認(批量確認)。
3.2 ACK類型
Client端指定了ACK模式,但是在Client與broker在交換ACK指令的時候,還需要告知ACK_TYPE,ACK_TYPE表示此確認指令的類型,不同的ACK_TYPE將傳遞着消息的狀態,broker可以根據不同的ACK_TYPE對消息進行不同的操作。比如Consumer消費消息時出現異常,就需要向broker發送ACK指令,ACK_TYPE爲"REDELIVERED_ACK_TYPE",那麼broker就會重新發送此消息。在JMS API中並沒有定義ACT_TYPE,因爲它通常是一種內部機制,並不會面向開發者。ActiveMQ中定義瞭如下幾種ACK_TYPE(參看MessageAck類):
- DELIVERED_ACK_TYPE = 0 消息"已接收",但尚未處理結束
- STANDARD_ACK_TYPE = 2 "標準"類型,通常表示爲消息"處理成功",broker端可以刪除消息了
- POSION_ACK_TYPE = 1 消息"錯誤",通常表示"拋棄"此消息,比如消息重發多次後,都無法正確處理時,消息將會被刪除或者DLQ(死信隊列)
- REDELIVERED_ACK_TYPE = 3 消息需"重發",比如consumer處理消息時拋出了異常,broker稍後會重新發送此消息
- INDIVIDUAL_ACK_TYPE = 4 表示只確認"單條消息",無論在任何ACK_MODE下
- UNMATCHED_ACK_TYPE = 5 在Topic中,如果一條消息在轉發給“訂閱者”時,發現此消息不符合Selector過濾條件,那麼此消息將 不會轉發給訂閱者,消息將會被存儲引擎刪除(相當於在Broker上確認了消息)。
3.3 ACK
我們需要在創建Session時指定ACK模式,由此可見,ACK模式將是session共享的,意味着一個session下所有的 consumer都使用同一種ACK模式。在創建Session時,開發者不能指定除ACK模式列表之外的其他值。
如果此session爲事務類型,用戶指定的ACK模式將被忽略,而強制使用"SESSION_TRANSACTED"類型;
如果此session爲非事務類型時,也將不能將 ACK模式設定爲"SESSION_TRANSACTED",畢竟這是相悖的。
Consumer消費消息的風格有2種: 同步/異步。使用consumer.receive()就是同步,使用messageListener就是異步;在同一個consumer中,我們不能同時使用這2種風格,比如在使用listener的情況下,當調用receive()方法將會獲得一個Exception。兩種風格下,消息確認時機有所不同。
1. 同步消費機制
同步調用時,在消息從receive方法返回之前,就已經調用了ACK;因此如果Client端沒有處理成功,此消息將丟失(可能重發,與ACK模式有關)。
Message message = sessionMessageQueue.dequeue();
if(message != null){
ack(message);
}
return message
2. 異步消費機制
基於異步調用時,消息的確認是在onMessage方法返回之後,如果onMessage方法異常,會導致消息不能被ACK,會觸發重發。
//基於listener
Session session = connection.getSession(consumerId);
sessionQueueBuffer.enqueue(message);
Runnable runnable = new Ruannale(){
run(){
Consumer consumer = session.getConsumer(consumerId);
Message md = sessionQueueBuffer.dequeue();
try{
consumer.messageListener.onMessage(md);
ack(md);//
}catch(Exception e){
redelivery();//sometime,not all the time;
}
}
//session中將採取線程池的方式,分發異步消息
//因此同一個session中多個consumer可以並行消費
threadPool.execute(runnable);
六、ActiveMQ的事務機制
1. 消息生產者事務
JMS規範中支持帶事務的消息,也就是說您可以啓動一個事務(並由消息發送者的連接會話設置一個事務號Transaction ID),然後在事務中發送多條消息。這個事務提交前這些消息都不會進入隊列(無論是Queue還是Topic)。
不進入隊列,並不代表JMS不會在事務提交前將消息發送給ActiveMQ服務端。 實際上這些消息都會發送給服務端,服務端發現這是一條帶有Transaction ID的消息,就會將先把這條消息放置在“transaction store”區域中(並且帶有redo日誌,這樣保證在收到rollback指令後能進行取消操作),等待這個Transaction ID被rollback或者commit。
一旦這個Transaction ID被commit,ActiveMQ纔會依據自身設置的PERSISTENT Message處理規則或者NON_PERSISTENT Meaage處理規則,將Transaction ID對應的message進行入隊操作(無論是Queue還是Topic)。以下代碼示例瞭如何在生產者端使用事務發送消息:
......
//進行連接
connection = connectionFactory.createQueueConnection();
connection.start();
//建立會話(設置一個帶有事務特性的會話)
session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
//建立queue(當然如果有了就不會重複建立)
Queue sendQueue = session.createQueue("/test");
//建立消息發送者對象
MessageProducer sender = session.createProducer(sendQueue);
//發送(JMS是支持事務的)
for(int index = 0 ; index < 10 ; index++) {
TextMessage outMessage = session.createTextMessage();
outMessage.setText("這是發送的消息內容-------------------" + index);
// 無論是NON_PERSISTENT message還是PERSISTENT message
// 都要在commit後才能真正的入隊
if(index % 2 == 0) {
sender.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
} else {
sender.setDeliveryMode(DeliveryMode.PERSISTENT);
}
// 沒有commit的消息,也是要先發送給服務端的
sender.send(outMessage);
}
session.commit();
......
在“connection.createSession”這個方法中一共有兩個參數(這句代碼在上文中已經出現過多次)。第一個布爾型參數很好理解,就是標示這個連接會話是否啓動事務;第二個整型參數標示了消息消費者的“應答模型”。2. 消息消費者事務
JMS規範除了爲消息生產者端提供事務支持以外,還爲消費服務端準備了事務的支持。您可以通過在消費者端操作事務的commit和rollback方法,向服務器告知一組消息是否處理完成。採用事務的意義在於,一組消息要麼被全部處理並確認成功,要麼全部被回滾並重新處理。
......
//建立會話(採用commit方式確認一批消息處理完畢)
session = connection.createSession(true, Session.SESSION_TRANSACTED);
//建立Queue(當然如果有了就不會重複建立)
sendQueue = session.createQueue("/test");
//建立消息發送者對象
MessageConsumer consumer = session.createConsumer(sendQueue);
consumer.setMessageListener(new MyMessageListener(session));
......
class MyMessageListener implements MessageListener {
private int number = 0;
/**
* 會話
*/
private Session session;
public MyMessageListener(Session session) {
this.session = session;
}
@Override
public void onMessage(Message message) {
// 打印這條消息
System.out.println("Message = " + message);
// 如果條件成立,就向服務器確認這批消息處理成功
// 服務器將從隊列中刪除這些消息
if(number++ % 3 == 0) {
try {
this.session.commit();
} catch (JMSException e) {
e.printStackTrace(System.out);
}
}
}
}
以上代碼演示的是消費者通過事務commit的方式,向服務器確認一批消息正常處理完成的方式。請注意代碼示例中的“session = connection.createSession(true, Session.SESSION_TRANSACTED);”語句。第一個參數表示連接會話啓用事務支持;第二個參數表示使用commit或者rollback的方式進行向服務器應答。這是調用commit的情況,那麼如果調用rollback方法又會發生什麼情況呢?調用rollback方法時,在rollback之前已處理過的消息(注意,並不是所有預取的消息)將重新發送一次到消費者端(發送給同一個連接會話)。並且消息中redeliveryCounter(重發計數器)屬性將會加1。請看如下所示的代碼片段和運行結果:
@Override
public void onMessage(Message message) {
// 打印這條消息
System.out.println("Message = " + message);
// rollback這條消息
this.session.rollback();
}
以上代碼片段中,我們不停的回滾正在處理的這條消息,通過打印出來的信息可以看到,這條消息被不停的重發:
Message = ActiveMQTextMessage {...... redeliveryCounter = 0, text = 這是發送的消息內容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 1, text = 這是發送的消息內容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 2, text = 這是發送的消息內容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 3, text = 這是發送的消息內容-------------------20}
Message = ActiveMQTextMessage {...... redeliveryCounter = 4, text = 這是發送的消息內容-------------------20}
可以看到同一條記錄被重複的處理,並且其中的redeliveryCounter屬性不斷累加。七、ActiveMQ的重發和死信隊列
消息處理失敗後,不斷的重發消息肯定不是一個最好的處理辦法:如果一條消息被不斷的處理失敗,那麼最可能的情況就是這條消息承載的業務內容本身就有問題。那麼無論重發多少次,這條消息還是會處理失敗。
爲了解決這個問題,ActiveMQ中引入了“死信隊列”(Dead Letter Queue)的概念。即一條消息再被重發了多次後(默認爲重發6次redeliveryCounter==6),將會被ActiveMQ移入“死信隊列”。開發人員可以在這個Queue中查看處理出錯的消息,進行人工干預。
默認情況下“死信隊列”只接受PERSISTENT Message,如果NON_PERSISTENT Message超過了重發上限,將直接被刪除。以下配置信息可以讓NON_PERSISTENT Message在超過重發上限後,也移入“死信隊列”:
<policyEntry queue=">">
<deadLetterStrategy>
<sharedDeadLetterStrategy processNonPersistent="true" />
</deadLetterStrategy>
</policyEntry>
上文提到的默認重發次數redeliveryCounter的上限也是可以進行設置的,爲了保證消息異常情況下儘可能小的影響消費者端的處理效率,實際工作中建議將這個上限值設置爲3。原因上文已經說過,如果消息本身的業務內容就存在問題,那麼重發多少次也沒有用。
RedeliveryPolicy redeliveryPolicy = connectionFactory.getRedeliveryPolicy();
// 設置最大重發次數
redeliveryPolicy.setMaximumRedeliveries(3);
實際上ActiveMQ的重發機制還有包括以上提到的rollback方式在內的多種方式:
1. 在支持事務的消費者連接會話中調用rollback方法。
2. 在支持事務的消費者連接會話中,使用commit方法明確告知服務器端消息已處理成功前,會話連接就終止了(最可能是異常終止)
3. 在需要使用ACK模式的會話中,使用消息的acknowledge方式明確告知服務器端消息已處理成功前,會話連接就終止了(最可能是異常終止)
但是以上幾種重發機制有一些小小的差異,主要體現在redeliveryCounter屬性的作用區域。簡而言之,第一種方法redeliveryCounter屬性的作用區域是本次連接會話,而後兩種redeliveryCounter屬性的作用區域是在整個ActiveMQ系統範圍。
以上是這篇博文的主要內容,參考了很多文章,想要給出ActiveMQ的一個大概,在其消息協議上還不大清楚,對於消息機制略有涉及,下一篇準備總結下其部署和集羣。