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. 消息生产者代码如下:

[java]  view plain  copy
  1. package com.ljq.durian.test.activemq;  
  2.   
  3. import javax.jms.Connection;  
  4. import javax.jms.ConnectionFactory;  
  5. import javax.jms.DeliveryMode;  
  6. import javax.jms.Destination;  
  7. import javax.jms.MessageProducer;  
  8. import javax.jms.Session;  
  9. import javax.jms.TextMessage;  
  10.   
  11. import org.apache.activemq.ActiveMQConnectionFactory;  
  12.   
  13. /** 
  14.  * 消息的生产者(发送者) 
  15.  *  
  16.  * @author Administrator 
  17.  *  
  18.  */  
  19. public class JMSProducer {  
  20.   
  21.       
  22.     public static void main(String[] args) {  
  23.         try {  
  24.             //第一步:建立ConnectionFactory工厂对象,需要填入用户名、密码、以及要连接的地址,均使用默认即可,默认端口为"tcp://localhost:61616"  
  25.             ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(  
  26.                     ActiveMQConnectionFactory.DEFAULT_USER,   
  27.                     ActiveMQConnectionFactory.DEFAULT_PASSWORD,   
  28.                     "failover:(tcp://localhost:61616)?Randomize=false");  
  29.               
  30.             //第二步:通过ConnectionFactory工厂对象我们创建一个Connection连接,并且调用Connection的start方法开启连接,Connection默认是关闭的。  
  31.             Connection connection = connectionFactory.createConnection();  
  32.             connection.start();  
  33.               
  34.             //第三步:通过Connection对象创建Session会话(上下文环境对象),用于接收消息,参数配置1为是否启用是事务,参数配置2为签收模式,一般我们设置自动签收。  
  35.             Session session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);  
  36.               
  37.             //第四步:通过Session创建Destination对象,指的是一个客户端用来指定生产消息目标和消费消息来源的对象,在PTP模式中,Destination被称作Queue即队列;在Pub/Sub模式,Destination被称作Topic即主题。在程序中可以使用多个Queue和Topic。  
  38.             Destination destination = session.createQueue("HelloWorld");  
  39.               
  40.             //第五步:我们需要通过Session对象创建消息的发送和接收对象(生产者和消费者)MessageProducer/MessageConsumer。  
  41.             MessageProducer producer = session.createProducer(null);  
  42.               
  43.             //第六步:我们可以使用MessageProducer的setDeliveryMode方法为其设置持久化特性和非持久化特性(DeliveryMode),我们稍后详细介绍。  
  44.             //producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  
  45.               
  46.             //第七步:最后我们使用JMS规范的TextMessage形式创建数据(通过Session对象),并用MessageProducer的send方法发送数据。同理客户端使用receive方法进行接收数据。最后不要忘记关闭Connection连接。  
  47.               
  48.             for(int i = 0 ; i < 10 ; i ++){  
  49.                 TextMessage msg = session.createTextMessage("我是消息内容" + i);  
  50.                 // 第一个参数目标地址  
  51.                 // 第二个参数 具体的数据信息  
  52.                 // 第三个参数 传送数据的模式  
  53.                 // 第四个参数 优先级  
  54.                 // 第五个参数 消息的过期时间  
  55.                 producer.send(destination, msg, DeliveryMode.NON_PERSISTENT, 0 , 1000L);  
  56.                 System.out.println("发送消息:" + msg.getText());  
  57.                 session.commit(); //启用事务时记得提交事务,不然消费端接收不到消息  
  58.                 Thread.sleep(1000);  
  59.             }  
  60.   
  61.             if(connection != null){  
  62.                 connection.close();  
  63.             }              
  64.         } catch (Exception e) {  
  65.             e.printStackTrace();  
  66.         }  
  67.           
  68.     }  
  69.   
  70. }  

3. 消息消费者代码如下:

[java]  view plain  copy
  1. package com.ljq.durian.test.activemq;  
  2.   
  3. import javax.jms.Connection;  
  4. import javax.jms.ConnectionFactory;  
  5. import javax.jms.Destination;  
  6. import javax.jms.MessageConsumer;  
  7. import javax.jms.Session;  
  8. import javax.jms.TextMessage;  
  9.   
  10. import org.apache.activemq.ActiveMQConnectionFactory;  
  11.   
  12. /** 
  13.  * 消息的消费者(接受者) 
  14.  *  
  15.  * @author Administrator 
  16.  *  
  17.  */  
  18. public class JMSConsumer {  
  19.   
  20.   
  21.     public static void main(String[] args)  {  
  22.         try {  
  23.             //第一步:建立ConnectionFactory工厂对象,需要填入用户名、密码、以及要连接的地址,均使用默认即可,默认端口为"tcp://localhost:61616"  
  24.             ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(  
  25.                     ActiveMQConnectionFactory.DEFAULT_USER,   
  26.                     ActiveMQConnectionFactory.DEFAULT_PASSWORD,   
  27.                     "failover:(tcp://localhost:61616)?Randomize=false");  
  28.               
  29.             //第二步:通过ConnectionFactory工厂对象我们创建一个Connection连接,并且调用Connection的start方法开启连接,Connection默认是关闭的。  
  30.             Connection connection = connectionFactory.createConnection();  
  31.             connection.start();  
  32.               
  33.             //第三步:通过Connection对象创建Session会话(上下文环境对象),用于接收消息,参数配置1为是否启用是事务,参数配置2为签收模式,一般我们设置自动签收。  
  34.             Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);  
  35.               
  36.             //第四步:通过Session创建Destination对象,指的是一个客户端用来指定生产消息目标和消费消息来源的对象,在PTP模式中,Destination被称作Queue即队列;在Pub/Sub模式,Destination被称作Topic即主题。在程序中可以使用多个Queue和Topic。  
  37.             Destination destination = session.createQueue("HelloWorld");  
  38.             //第五步:通过Session创建MessageConsumer  
  39.             MessageConsumer consumer = session.createConsumer(destination);  
  40.               
  41.             while(true){  
  42.                 TextMessage msg = (TextMessage)consumer.receive();  
  43.                 if(msg == null) {  
  44.                     break;  
  45.                 }  
  46.                 System.out.println("收到的内容:" + msg.getText());  
  47.             }              
  48.         } catch (Exception e) {  
  49.             e.printStackTrace();  
  50.         }  
  51.           
  52.     }  
  53.   
  54. }  


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没有消息消费者,消息将进行存储。


点对点方式使用生产者-消费者模式,生产者代码如下:

[java]  view plain  copy
  1. public Producer() throws JMSException {    
  2.     factory = new ActiveMQConnectionFactory(brokerURL);    
  3.     connection = factory.createConnection();    
  4.     connection.start();    
  5.     session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);    
  6.     producer = session.createProducer(null);    
  7. }  
  8.   
  9. public void sendMessage() throws JMSException {    
  10.     for(int i = 0; i < jobs.length; i++)    
  11.     {    
  12.         String job = jobs[i];    
  13.         Destination destination = session.createQueue("JOBS." + job);    
  14.         Message message = session.createObjectMessage(i);    
  15.         System.out.println("Sending: id: " + ((ObjectMessage)message).getObject() + " on queue: " + destination);    
  16.         producer.send(destination, message);    
  17.     }    
  18. }    
  19.   
  20. public static void main(String[] args) throws JMSException {    
  21.     Producer producer = new Producer();    
  22.     for(int i = 0; i < 10; i++) {    
  23.         producer.sendMessage();    
  24.         System.out.println("Produced " + i + " job messages");    
  25.     try {    
  26.             Thread.sleep(1000);    
  27.         } catch (InterruptedException x) {    
  28.         e.printStackTrace();    
  29.         }    
  30.     }    
  31.     producer.close();    
  32. }    

生产者将消息放入队列中,由消费者使用,消费者代码如下:

[java]  view plain  copy
  1. public Consumer() throws JMSException {    
  2.     factory = new ActiveMQConnectionFactory(brokerURL);    
  3.     connection = factory.createConnection();    
  4.     connection.start();    
  5.     session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);    
  6. }    
  7.   
  8. public static void main(String[] args) throws JMSException {    
  9.     Consumer consumer = new Consumer();    
  10.     for (String job : consumer.jobs) {    
  11.         Destination destination = consumer.getSession().createQueue("JOBS." + job);    
  12.         MessageConsumer messageConsumer = consumer.getSession().createConsumer(destination);    
  13.         messageConsumer.setMessageListener(new Listener(job));    
  14.     }    
  15. }    
  16.         
  17. public Session getSession() {    
  18.     return session;    
  19. }    
具体注册的对象需要实现MessageListener接口:

[java]  view plain  copy
  1. import javax.jms.Message;    
  2. import javax.jms.MessageListener;    
  3. import javax.jms.ObjectMessage;    
  4.     
  5. public class Listener implements MessageListener {    
  6.     
  7.     private String job;    
  8.         
  9.     public Listener(String job) {    
  10.         this.job = job;    
  11.     }    
  12.     
  13.     public void onMessage(Message message) {    
  14.         try {    
  15.             //do something here    
  16.             System.out.println(job + " id:" + ((ObjectMessage)message).getObject());    
  17.         } catch (Exception e) {    
  18.             e.printStackTrace();    
  19.         }    
  20.     }    
  21.     
  22. }    



2. 发布-订阅(publish-subscribe)

“发布-订阅”模式下,消息会被复制多份,分别发送给所有“订阅”者。


Publisher
publisher是属于发布信息的一方,它通过定义一个或者多个topic,然后给这些topic发送消息。

[java]  view plain  copy
  1. public Publisher() throws JMSException {    
  2.         factory = new ActiveMQConnectionFactory(brokerURL);    
  3.         connection = factory.createConnection();    
  4.         try {    
  5.         connection.start();    
  6.         } catch (JMSException jmse) {    
  7.             connection.close();    
  8.             throw jmse;    
  9.         }    
  10.         session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);    
  11.         producer = session.createProducer(null);   
  12. }    
我们按照前面说的流程定义了基本的connectionFactory, connection, session, producer。这里代码就是主要实现初始化的效果。接着,我们需要定义一系列的topic让所有的consumer来订阅,设置topic的代码如下:

[java]  view plain  copy
  1. protected void setTopics(String[] stocks) throws JMSException {    
  2.     destinations = new Destination[stocks.length];    
  3.     for(int i = 0; i < stocks.length; i++) {    
  4.         destinations[i] = session.createTopic("STOCKS." + stocks[i]);    
  5.     }    
  6. }    
这里destinations是一个内部定义的成员变量Destination[]。这里我们总共定义了的topic数取决于给定的参数stocks。在定义好topic之后我们要给这些指定的topic发消息,具体实现的代码如下:
[java]  view plain  copy
  1. protected void sendMessage(String[] stocks) throws JMSException {    
  2.     for(int i = 0; i < stocks.length; i++) {    
  3.         Message message = createStockMessage(stocks[i], session);    
  4.         System.out.println("Sending: " + ((ActiveMQMapMessage)message).getContentMap() + " on destination: " + destinations[i]);    
  5.         producer.send(destinations[i], message);    
  6.     }    
  7. }    
  8.     
  9. protected Message createStockMessage(String stock, Session session) throws JMSException {    
  10.     MapMessage message = session.createMapMessage();    
  11.     message.setString("stock", stock);    
  12.     message.setDouble("price"1.00);    
  13.     message.setDouble("offer"0.01);    
  14.     message.setBoolean("up"true);  
  15.     return message;    
  16. }    
在sendMessage方法里我们遍历每个topic,然后给每个topic发送定义的Message消息。在定义好前面发送消息的基础之后,我们调用他们的代码就很简单了:

[java]  view plain  copy
  1. public static void main(String[] args) throws JMSException {    
  2.     if(args.length < 1)    
  3.         throw new IllegalArgumentException();    
  4.         
  5.         // Create publisher         
  6.         Publisher publisher = new Publisher();    
  7.             
  8.         // Set topics    
  9.     publisher.setTopics(args);    
  10.             
  11.     for(int i = 0; i < 10; i++) {    
  12.         publisher.sendMessage(args);    
  13.         System.out.println("Publisher '" + i + " price messages");    
  14.         try {    
  15.             Thread.sleep(1000);    
  16.         } catch(InterruptedException e) {    
  17.             e.printStackTrace();    
  18.         }    
  19.     }    
  20.     // Close all resources    
  21.     publisher.close();    
  22. }    
调用他们的代码就是我们遍历所有topic,然后通过sendMessage发送消息。在发送一个消息之后先sleep1秒钟。要注意的一个地方就是我们使用完资源之后必须要使用close方法将这些资源关闭释放。close方法关闭资源的具体实现如下:

[java]  view plain  copy
  1. public void close() throws JMSException {    
  2.     if (connection != null) {    
  3.         connection.close();    
  4.      }    
  5. }   
Consumer
Consumer的代码也很类似,具体的步骤无非就是1.初始化资源。 2. 接收消息。 3. 必要的时候关闭资源。初始化资源可以放到构造函数里面:

[java]  view plain  copy
  1. public Consumer() throws JMSException {    
  2.         factory = new ActiveMQConnectionFactory(brokerURL);    
  3.         connection = factory.createConnection();    
  4.         connection.start();    
  5.         session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);    
  6. }    
接收和处理消息的方法有两种,分为同步和异步的,一般同步的方式我们是通过MessageConsumer. receive ()方法来处理接收到的消息。而异步的方法则是通过注册一个 MessageListener 的方法,使用MessageConsumer.setMessageListener()。这里我们采用异步的方式实现:
[java]  view plain  copy
  1. public static void main(String[] args) throws JMSException {    
  2.     Consumer consumer = new Consumer();    
  3.     for (String stock : args) {    
  4.     Destination destination = consumer.getSession().createTopic("STOCKS." + stock);    
  5.     MessageConsumer messageConsumer = consumer.getSession().createConsumer(destination);    
  6.     messageConsumer.setMessageListener(new Listener());    
  7.     }    
  8. }    
  9.         
  10. public Session getSession() {    
  11.     return session;    
  12. }    
在前面的代码里我们先找到同样的topic,然后遍历所有的topic去获得消息。对于消息的处理我们专门通过Listener对象来负责。Listener对象的职责很简单,主要就是处理接收到的消息:

[java]  view plain  copy
  1. public class Listener implements MessageListener {    
  2.     
  3.     public void onMessage(Message message) {    
  4.         try {    
  5.             MapMessage map = (MapMessage)message;    
  6.             String stock = map.getString("stock");    
  7.             double price = map.getDouble("price");    
  8.             double offer = map.getDouble("offer");    
  9.             boolean up = map.getBoolean("up");    
  10.             DecimalFormat df = new DecimalFormat( "#,###,###,##0.00" );    
  11.             System.out.println(stock + "\t" + df.format(price) + "\t" + df.format(offer) + "\t" + (up?"up":"down"));    
  12.         } catch (Exception e) {    
  13.             e.printStackTrace();    
  14.         }    
  15.     }    
  16.     
  17. }    
它实现了MessageListener接口,里面的onMessage方法就是在接收到消息之后会被调用的方法。

3. 请求-响应(request-response)

和前面两种方式比较起来,request-response的通信方式很常见,但是不是默认提供的一种模式。在前面的两种模式中都是一方负责发送消息而另外一方负责处理。而我们实际中的很多应用相当于一种一应一答的过程,需要双方都能给对方发送消息。于是请求-应答的这种通信方式也很重要。它也应用的很普遍。 
请求-应答方式并不是JMS规范系统默认提供的一种通信方式,而是通过在现有通信方式的基础上稍微运用一点技巧实现的。下图是典型的请求-应答方式的交互过程:


在JMS里面,如果要实现请求/应答的方式,可以利用JMSReplyTo和JMSCorrelationID消息头来将通信的双方关联起来。另外,QueueRequestor和TopicRequestor能够支持简单的请求/应答过程。现在,如果我们要实现这么一个过程,在发送请求消息并且等待返回结果的client端的流程如下:

[java]  view plain  copy
  1. // client side    
  2. Destination tempDest = session.createTemporaryQueue();    
  3. MessageConsumer responseConsumer = session.createConsumer(tempDest);    
  4. ...    
  5.     
  6. // send a request..    
  7. message.setJMSReplyTo(tempDest)    
  8. message.setJMSCorrelationID(myCorrelationID);    
  9.     
  10. producer.send(message);    
client端创建一个 临时队列 并在发送的消息里 指定了发送返回消息的destination以及correlationID 。那么在处理消息的server端得到这个消息后就知道该发送给谁了。Server端的大致流程如下:

[java]  view plain  copy
  1. public void onMessage(Message request) {    
  2.     
  3.   Message response = session.createMessage();    
  4.   response.setJMSCorrelationID(request.getJMSCorrelationID())    
  5.     
  6.   producer.send(request.getJMSReplyTo(), response)    
  7. }    
这里我们是用server端注册 MessageListener ,通过设置返回信息的 CorrelationID JMSReplyTo 将信息返回。以上就是发送和接收消息的双方的大致程序结构。具体的实现代码如下:

Client侧实现

[java]  view plain  copy
  1. public Client() {    
  2.         ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");    
  3.         Connection connection;    
  4.         try {    
  5.             connection = connectionFactory.createConnection();    
  6.             connection.start();    
  7.             Session session = connection.createSession(transacted, ackMode);    
  8.             Destination adminQueue = session.createQueue(clientQueueName);    
  9.     
  10.             //Setup a message producer to send message to the queue the server is consuming from    
  11.             this.producer = session.createProducer(adminQueue);    
  12.             this.producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);    
  13.     
  14.             //Create a temporary queue that this client will listen for responses on then create a consumer    
  15.             //that consumes message from this temporary queue...for a real application a client should reuse    
  16.             //the same temp queue for each message to the server...one temp queue per client    
  17.             Destination tempDest = session.createTemporaryQueue();    
  18.             MessageConsumer responseConsumer = session.createConsumer(tempDest);    
  19.     
  20.             //This class will handle the messages to the temp queue as well    
  21.             responseConsumer.setMessageListener(this);    
  22.     
  23.             //Now create the actual message you want to send    
  24.             TextMessage txtMessage = session.createTextMessage();    
  25.             txtMessage.setText("MyProtocolMessage");    
  26.     
  27.             //Set the reply to field to the temp queue you created above, this is the queue the server    
  28.             //will respond to    
  29.             txtMessage.setJMSReplyTo(tempDest);    
  30.     
  31.             //Set a correlation ID so when you get a response you know which sent message the response is for    
  32.             //If there is never more than one outstanding message to the server then the    
  33.             //same correlation ID can be used for all the messages...if there is more than one outstanding    
  34.             //message to the server you would presumably want to associate the correlation ID with this    
  35.             //message somehow...a Map works good    
  36.             String correlationId = this.createRandomString();    
  37.             txtMessage.setJMSCorrelationID(correlationId);    
  38.             this.producer.send(txtMessage);    
  39.         } catch (JMSException e) {    
  40.             //Handle the exception appropriately    
  41.         }    
  42.     }    
这里的代码除了初始化构造函数里的参数还同时设置了 两个destination 一个是自己要发送消息出去的destination,在这一句设置:

[java]  view plain  copy
  1. session.createProducer(adminQueue);  
另外一个是自己要接收的消息destination, 通过这两句指定了要接收消息的目的地:
[java]  view plain  copy
  1. Destination tempDest = session.createTemporaryQueue();   
  2. responseConsumer = session.createConsumer(tempDest);   

这里是用的一个临时队列。在前面指定了返回消息的通信队列之后,我们需要通知server端知道发送返回消息给哪个队列。于是

[java]  view plain  copy
  1. txtMessage.setJMSReplyTo(tempDest);  

指定了这一部分,同时:

[java]  view plain  copy
  1. txtMessage.setJMSCorrelationID(correlationId);  

方法主要是为了保证每次发送回来请求的server端能够知道对应的是哪个请求。这里一个请求和一个应答是相当于对应一个相同的序列号一样。

因为client端在发送消息之后还要接收server端返回的消息,所以它也要实现一个消息receiver的功能。这里采用实现MessageListener接口的方式:

[java]  view plain  copy
  1. public void onMessage(Message message) {    
  2.         String messageText = null;    
  3.         try {    
  4.             if (message instanceof TextMessage) {    
  5.                 TextMessage textMessage = (TextMessage) message;    
  6.                 messageText = textMessage.getText();    
  7.                 System.out.println("messageText = " + messageText);    
  8.             }    
  9.         } catch (JMSException e) {    
  10.             //Handle the exception appropriately    
  11.         }    
  12.     }    
Server侧实现
server端要执行的过程和client端相反,它是先接收消息,在接收到消息后根据提供的JMSCorelationID来发送返回的消息:

[java]  view plain  copy
  1. public void onMessage(Message message) {    
  2.     try {    
  3.         TextMessage response = this.session.createTextMessage();    
  4.         if (message instanceof TextMessage) {    
  5.             TextMessage txtMsg = (TextMessage) message;    
  6.             String messageText = txtMsg.getText();    
  7.             response.setText(this.messageProtocol.handleProtocolMessage(messageText));    
  8.         }    
  9.   
  10.         //Set the correlation ID from the received message to be the correlation id of the response message    
  11.         //this lets the client identify which message this is a response to if it has more than    
  12.         //one outstanding message to the server    
  13.         response.setJMSCorrelationID(message.getJMSCorrelationID());    
  14.   
  15.         //Send the response to the Destination specified by the JMSReplyTo field of the received message,    
  16.         //this is presumably a temporary queue created by the client    
  17.         this.replyProducer.send(message.getJMSReplyTo(), response);    
  18.     } catch (JMSException e) {    
  19.         //Handle the exception appropriately    
  20.     }    
  21. }   
在replyProducer.send()方法里,message.getJMSReplyTo()就得到了要发送消息回去的destination。另外,设置这些发送返回信息的replyProducer的信息主要在构造函数相关的方法里实现了:

[java]  view plain  copy
  1. public Server() {    
  2.     try {    
  3.         //This message broker is embedded    
  4.         BrokerService broker = new BrokerService();    
  5.         broker.setPersistent(false);    
  6.         broker.setUseJmx(false);    
  7.         broker.addConnector(messageBrokerUrl);    
  8.         broker.start();    
  9.     } catch (Exception e) {    
  10.         //Handle the exception appropriately    
  11.     }    
  12.   
  13.     //Delegating the handling of messages to another class, instantiate it before setting up JMS so it    
  14.     //is ready to handle messages    
  15.     this.messageProtocol = new MessageProtocol();    
  16.     this.setupMessageQueueConsumer();    
  17. }    
  18.   
  19. private void setupMessageQueueConsumer() {    
  20.     ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(messageBrokerUrl);    
  21.     Connection connection;    
  22.     try {    
  23.         connection = connectionFactory.createConnection();    
  24.         connection.start();    
  25.         this.session = connection.createSession(this.transacted, ackMode);    
  26.         Destination adminQueue = this.session.createQueue(messageQueueName);    
  27.   
  28.         //Setup a message producer to respond to messages from clients, we will get the destination    
  29.         //to send to from the JMSReplyTo header field from a Message    
  30.         this.replyProducer = this.session.createProducer(null);    
  31.         this.replyProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);    
  32.   
  33.         //Set up a consumer to consume messages off of the admin queue    
  34.         MessageConsumer consumer = this.session.createConsumer(adminQueue);    
  35.         consumer.setMessageListener(this);    
  36.     } catch (JMSException e) {    
  37.         //Handle the exception appropriately    
  38.     }    
  39. }   
总体来说,整个的交互过程并不复杂,只是比较繁琐。

对于请求/应答的方式来说,这种典型交互的过程就是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。示例如下:

[java]  view plain  copy
  1. ......  
  2. for(int index = 0 ; index < 10 ; index++) {  
  3.     TextMessage outMessage = session.createTextMessage();  
  4.     outMessage.setText("这是发送的消息内容:" + index);  
  5.     if(index % 2 == 0) {  
  6.         sender.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  
  7.     } else {  
  8.         sender.setDeliveryMode(DeliveryMode.PERSISTENT);  
  9.     }  
  10.     sender.send(outMessage);  
  11. }  
  12. ......  
那么当 JMS服务节点 重启后(注意不是 producer 重启),以上代码中发送的10条消息只有其中5条消息能够保存下来。

发送NON_PERSISTENT Message时,消息发送方默认使用异步方式:即是说消息发送后发送方不会等待NON_PERSISTENT Message在服务端的任何回执。那么问题来了:如果这时服务端已经出现了消息堆积,并且堆积程度已经达到“无法再接收新消息”的极限情况了,那么消息发送方如果知晓并采取相应的策略呢?

实际上所谓的异步发送也并非绝对的异步,消息发送者会在发送一定大小的消息后等待服务端进行回执(这个配置只是针对使用异步方式进行发送消息的情况)

[java]  view plain  copy
  1. ......  
  2. // 以下语句设置消息发送者在累计发送102400byte大小的消息后(可能是一条消息也可能是多条消息)  
  3. // 等待服务端进行回执,以便确定之前发送的消息是否被正确处理  
  4. // 确定服务器端是否产生了过量的消息堆积,需要减慢消息生产端的生产速度  
  5. connectionFactory.setProducerWindowSize(102400);  
  6. ......  


如果不特意指定消息的发送类型,那么消息生产者默认发送 PERSISTENT Meaage 。这样的消息发送到ActiveMQ服务端后将被进行 持久化存储 ,并且消息发送者 默认等待 ActiveMQ服务端对这条消息处理情况的回执。
以上这个过程非常耗时,ActiveMQ服务端不但要接受消息,在内存中完成存储,并且按照ActiveMQ服务端设置的 持久化存储方案 对消息进行存储(主要的处理时间耗费在这里)。为了提高ActiveMQ在接受PERSISTENT Meaage时的性能,ActiveMQ允许开发人员遵从JMS API中的设置方式,为消息发送端在发送PERSISTENT Meaage时提供 异步方式

[java]  view plain  copy
  1. ......  
  2. // 使用异步传输  
  3. // 上文已经说过,如果发送的是NON_PERSISTENT Message  
  4. // 那么默认就是异步方式  
  5. connectionFactory.setUseAsyncSend(true);  
  6. ......  
一旦您进行了这样的设置,就需要设置回执窗口:

[java]  view plain  copy
  1. ......  
  2. // 同样设置消息发送者在累计发送102400byte大小的消息后  
  3. // 等待服务端进行回执,以便确定之前发送的消息是否被正确处理  
  4. // 确定服务器端是否产生了过量的消息堆积,需要减慢消息生产端的生产速度  
  5. connectionFactory.setProducerWindowSize(102400);  
  6. ......  

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重启时,消息是否保留
QueueN-YN
QueueY-YY
TopicNNNN
TopicNYYN
TopicYNNN
TopicYYYY

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,可谓高度优化;支持内存缓存也是必然,但在缓存回复策略上不如内存存储器。

[java]  view plain  copy
  1. <broker brokerName="broker" persistent="true" useShutdownHook="false">  
  2.         <persistenceAdapter>  
  3.                 <kahaDB directory="${activemq.data}/kahadb" journalMaxFileLength="16mb"/>  
  4.         </persistenceAdapter>  
  5. </broker>  

2、 AMQ message store

在基于文件、支持事务方面和KahaDB类似。不同之处如下:
优点:索引用的是hashbin(哈希桶,没有查到权威定义,可理解为哈希表),自然比KahaDB的Btree索引要快,并且磁盘读写用的是nio,速度也快,所以用于消息吞吐量要求比较大的时候是最佳选择。(有的人把吞吐量理解成消息总数量其实不正确,应该是消息出入队的速率。)
缺点:对于每个destination都要建一个索引,所以不适于很多destination并发的场合,而这恰恰是KahaDB的优势,它可以支持最大10000个queue的同时等待。(AMQ为每个索引使用两个分开的文件,并且每个 Destination 都有一个索引,所以当你打算在代理中使用数千个队列的时候,不应该使用它。)

[plain]  view plain  copy
  1. <persistenceAdapter>  
  2.         <amqPersistenceAdapter  
  3.                 directory="${activemq.data}/kahadb"  
  4.                 syncOnWrite="true"  
  5.                 indexPageSize="16kb"  
  6.                 indexMaxBinSize="100"  
  7.                 maxFileLength="10mb" />  
  8. </persistenceAdapter>  

3、 JDBC message store

默认的JDBC驱动是ApacheDerby,同时支持MySQL、PostgreSQL、Oracle、SQLServer、Sybase、Informix、MaxDB等主流的关系数据库。用三张表结构来存储消息,分别是ACTIVEMQ_MSGSACTIVEMQ_ACKSACTIVEMQ_LOCK。第二张表外键关联到第一张表,共同存储消息,第三张表用于锁定保证只有一个broker实例可以访问数据库。选择关系型数据库,通常的原因是企业已经具备了管理关系型数据的专长,但是它在性能上绝对不优于上述消息存储实现

[java]  view plain  copy
  1. <beans>  
  2.         <broker brokerName="test-broker" persistent="true" xmlns="http://activemq.apache.org/schema/core">  
  3.                 <persistenceAdapter>  
  4.                         <jdbcPersistenceAdapter dataSource="#mysql-ds"/>  
  5.                 </persistenceAdapter>  
  6.         </broker>  
  7.         <bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">  
  8.                 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
  9.                 <property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/>  
  10.                 <property name="username" value="activemq"/>  
  11.                 <property name="password" value="activemq"/>  
  12.                 <property name="maxActive" value="200"/>  
  13.                 <property name="poolPreparedStatements" value="true"/>  
  14.         </bean>  
  15. </beans>  

4、 Memory message store

用于实时消息的缓存,只针对非持久订阅的消费者提供了5种订阅恢复策略,可以极大程度增强非持久订阅的可用性。也就是说对于持久订阅的消费者是用不到内存存储的。

[plain]  view plain  copy
  1. <broker brokerName="test-broker" persistent="false" xmlns="http://activemq.apache.org/schema/core">  
  2.         <transportConnectors>  
  3.                 <transportConnector uri="tcp://localhost:61635"/>  
  4.         </transportConnectors>  
  5. </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中增加如下查询字符串: 

[java]  view plain  copy
  1. String brokerUrl = "tcp://localhost:61616?" +     
  2.                    "jms.optimizeAcknowledge=true" +     
  3.                    "&jms.optimizeAcknowledgeTimeOut=30000" +     
  4.                    "&jms.redeliveryPolicy.maximumRedeliveries=6";    
  5. ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerUrl);   
 2) 在 destinationUri 中,增加如下查询字符串:

[java]  view plain  copy
  1. String queueName = "test-queue?customer.prefetchSize=100";    
  2. Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);    
  3. 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都需要单独设定:

[plain]  view plain  copy
  1. "&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端仍然不会“延迟确认”,即:
[java]  view plain  copy
  1. 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模式有关)。

[java]  view plain  copy
  1. Message message = sessionMessageQueue.dequeue();    
  2. if(message != null){    
  3.     ack(message);    
  4. }    
  5. return message    

2. 异步消费机制

基于异步调用时,消息的确认是在onMessage方法返回之后,如果onMessage方法异常,会导致消息不能被ACK,会触发重发。

[java]  view plain  copy
  1. //基于listener    
  2. Session session = connection.getSession(consumerId);    
  3. sessionQueueBuffer.enqueue(message);    
  4. Runnable runnable = new Ruannale(){    
  5.     run(){    
  6.         Consumer consumer = session.getConsumer(consumerId);    
  7.         Message md = sessionQueueBuffer.dequeue();    
  8.         try{    
  9.             consumer.messageListener.onMessage(md);    
  10.             ack(md);//    
  11.         }catch(Exception e){    
  12.             redelivery();//sometime,not all the time;    
  13.     }    
  14. }    
  15. //session中将采取线程池的方式,分发异步消息    
  16. //因此同一个session中多个consumer可以并行消费    
  17. 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)。以下代码示例了如何在生产者端使用事务发送消息:

[java]  view plain  copy
  1. ......  
  2. //进行连接  
  3. connection = connectionFactory.createQueueConnection();  
  4. connection.start();  
  5.   
  6. //建立会话(设置一个带有事务特性的会话)  
  7. session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);  
  8. //建立queue(当然如果有了就不会重复建立)  
  9. Queue sendQueue = session.createQueue("/test");  
  10. //建立消息发送者对象  
  11. MessageProducer sender = session.createProducer(sendQueue);  
  12.   
  13. //发送(JMS是支持事务的)  
  14. for(int index = 0 ; index < 10 ; index++) {  
  15.     TextMessage outMessage = session.createTextMessage();  
  16.     outMessage.setText("这是发送的消息内容-------------------" + index);  
  17.     // 无论是NON_PERSISTENT message还是PERSISTENT message  
  18.     // 都要在commit后才能真正的入队  
  19.     if(index % 2 == 0) {  
  20.         sender.setDeliveryMode(DeliveryMode.NON_PERSISTENT);  
  21.     } else {  
  22.         sender.setDeliveryMode(DeliveryMode.PERSISTENT);  
  23.     }  
  24.   
  25.     // 没有commit的消息,也是要先发送给服务端的  
  26.     sender.send(outMessage);  
  27. }  
  28.   
  29. session.commit();  
  30. ......  
在“connection.createSession”这个方法中一共有两个参数(这句代码在上文中已经出现过多次)。第一个布尔型参数很好理解,就是标示这个连接会话是否启动事务;第二个整型参数标示了消息消费者的“应答模型”。

2. 消息消费者事务

JMS规范除了为消息生产者端提供事务支持以外,还为消费服务端准备了事务的支持。您可以通过在消费者端操作事务的commit和rollback方法,向服务器告知一组消息是否处理完成。采用事务的意义在于,一组消息要么被全部处理并确认成功,要么全部被回滚并重新处理。

[java]  view plain  copy
  1. ......  
  2. //建立会话(采用commit方式确认一批消息处理完毕)  
  3. session = connection.createSession(true, Session.SESSION_TRANSACTED);  
  4. //建立Queue(当然如果有了就不会重复建立)  
  5. sendQueue = session.createQueue("/test");  
  6. //建立消息发送者对象  
  7. MessageConsumer consumer = session.createConsumer(sendQueue);  
  8. consumer.setMessageListener(new MyMessageListener(session));  
  9.   
  10. ......  
  11.   
  12. class MyMessageListener implements MessageListener {  
  13.     private int number = 0;  
  14.   
  15.     /** 
  16.      * 会话 
  17.      */  
  18.     private Session session;  
  19.   
  20.     public MyMessageListener(Session session) {  
  21.         this.session = session;  
  22.     }  
  23.   
  24.     @Override  
  25.     public void onMessage(Message message) {  
  26.         // 打印这条消息  
  27.         System.out.println("Message = " + message);  
  28.         // 如果条件成立,就向服务器确认这批消息处理成功  
  29.         // 服务器将从队列中删除这些消息  
  30.         if(number++ % 3 == 0) {  
  31.             try {  
  32.                 this.session.commit();  
  33.             } catch (JMSException e) {  
  34.                 e.printStackTrace(System.out);  
  35.             }  
  36.         }  
  37.     }  
  38. }  
以上代码演示的是 消费者通过事务commit的方式,向服务器确认一批消息正常处理完成的方式 。请注意代码示例中的“session = connection.createSession(true, Session.SESSION_TRANSACTED);”语句。第一个参数表示 连接会话启用事务支持 ;第二个参数表示 使用commit或者rollback的方式进行向服务器应答
这是调用commit的情况,那么如果调用rollback方法又会发生什么情况呢?调用rollback方法时,在rollback之前已处理过的消息(注意,并不是所有预取的消息)将 重新发送一次到消费者端 (发送给同一个连接会话)。并且消息中 redeliveryCounter (重发计数器)属性将会加1。请看如下所示的代码片段和运行结果:

[java]  view plain  copy
  1. @Override  
  2. public void onMessage(Message message) {  
  3.     // 打印这条消息  
  4.     System.out.println("Message = " + message);  
  5.     // rollback这条消息  
  6.     this.session.rollback();  
  7. }  
以上代码片段中,我们不停的回滚正在处理的这条消息,通过打印出来的信息可以看到,这条消息被不停的重发:

[plain]  view plain  copy
  1. Message = ActiveMQTextMessage {...... redeliveryCounter = 0, text = 这是发送的消息内容-------------------20}  
  2. Message = ActiveMQTextMessage {...... redeliveryCounter = 1, text = 这是发送的消息内容-------------------20}  
  3. Message = ActiveMQTextMessage {...... redeliveryCounter = 2, text = 这是发送的消息内容-------------------20}  
  4. Message = ActiveMQTextMessage {...... redeliveryCounter = 3, text = 这是发送的消息内容-------------------20}  
  5. 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在超过重发上限后,也移入“死信队列”:

[java]  view plain  copy
  1. <policyEntry queue=">">    
  2.     <deadLetterStrategy>    
  3.         <sharedDeadLetterStrategy processNonPersistent="true" />    
  4.     </deadLetterStrategy>    
  5. </policyEntry>  

上文提到的默认重发次数redeliveryCounter的上限也是可以进行设置的,为了保证消息异常情况下尽可能小的影响消费者端的处理效率,实际工作中建议将这个上限值设置为3。原因上文已经说过,如果消息本身的业务内容就存在问题,那么重发多少次也没有用。

[java]  view plain  copy
  1. RedeliveryPolicy redeliveryPolicy = connectionFactory.getRedeliveryPolicy();  
  2. // 设置最大重发次数  
  3. redeliveryPolicy.setMaximumRedeliveries(3);  

实际上ActiveMQ的重发机制还有包括以上提到的rollback方式在内的多种方式:
1. 在支持事务的消费者连接会话中调用 rollback方法

2. 在支持事务的消费者连接会话中,使用commit方法明确告知服务器端消息已处理成功前,会话连接就终止了(最可能是异常终止)

3. 在需要使用ACK模式的会话中,使用消息的acknowledge方式明确告知服务器端消息已处理成功前,会话连接就终止了(最可能是异常终止)
但是以上几种重发机制有一些小小的差异,主要体现在redeliveryCounter属性的作用区域。简而言之,第一种方法redeliveryCounter属性的作用区域是本次连接会话,而后两种redeliveryCounter属性的作用区域是在整个ActiveMQ系统范围。

以上是这篇博文的主要内容,参考了很多文章,想要给出ActiveMQ的一个大概,在其消息协议上还不大清楚,对于消息机制略有涉及,下一篇准备总结下其部署和集群。

  • 9
    点赞
  • 13
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
评论

打赏作者

echojson

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值