MQ入門總結(三)ActiveMQ的用法和實現

轉載:架構設計:系統間通信(21)——ActiveMQ的安裝與使用

轉載:成小胖學習ActiveMQ·基礎篇

轉載:ActiveMQ學習心得之ActiveMQ四種存儲器分析

轉載:ActiveMQ(一)簡介與架構

轉載:ActiveMQ消息傳送機制以及ACK機制詳解

轉載:架構設計:系統間通信(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的架構

120043_IEeh_1767531.png
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();  
     }  
} 
Consumer
Consumer的代碼也很類似,具體的步驟無非就是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,通過設置返回信息的CorrelationIDJMSReplyTo將信息返回。以上就是發送和接收消息的雙方的大致程序結構。具體的實現代碼如下:

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_PERSISTENTMessagePERSISTENT 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);
......


如果不特意指定消息的發送類型,那麼消息生產者默認發送PERSISTENT Meaage。這樣的消息發送到ActiveMQ服務端後將被進行持久化存儲,並且消息發送者默認等待ActiveMQ服務端對這條消息處理情況的回執。
以上這個過程非常耗時,ActiveMQ服務端不但要接受消息,在內存中完成存儲,並且按照ActiveMQ服務端設置的持久化存儲方案對消息進行存儲(主要的處理時間耗費在這裏)。爲了提高ActiveMQ在接受PERSISTENT Meaage時的性能,ActiveMQ允許開發人員遵從JMS API中的設置方式,爲消息發送端在發送PERSISTENT Meaage時提供異步方式

......
// 使用異步傳輸
// 上文已經說過,如果發送的是NON_PERSISTENT Message
// 那麼默認就是異步方式
connectionFactory.setUseAsyncSend(true);
......
一旦您進行了這樣的設置,就需要設置回執窗口:

......
// 同樣設置消息發送者在累計發送102400byte大小的消息後
// 等待服務端進行回執,以便確定之前發送的消息是否被正確處理
// 確定服務器端是否產生了過量的消息堆積,需要減慢消息生產端的生產速度
connectionFactory.setProducerWindowSize(102400);
......

2. 持久化訂閱和非持久化訂閱

持續訂閱和非持續訂閱,是針對“訂閱-發佈”模式的細分處理策略,在JMS規範中的標準稱呼是:Durable-SubscribersNon-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_MSGSACTIVEMQ_ACKSACTIVEMQ_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

如果prefetchACKtrue,那麼prefetch必須大於0;當prefetchACKfalse時,你可以指定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         事務提交併確認
此外AcitveMQ補充了一個自定義的ACK模式:
  • 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_TYPEREDELIVERED_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上確認了消息)。
    到目前爲止,我們已經清楚了大概的原理: Client端在不同的ACK模式時,將意味着在不同的時機發送ACK指令,每個ACK Command中會包含ACK_TYPE,那麼broker端就可以根據ACK_TYPE來決定此消息的後續操作. 接下來,我們詳細的分析ACK模式與ACK_TYPE.

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的一個大概,在其消息協議上還不大清楚,對於消息機制略有涉及,下一篇準備總結下其部署和集羣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章