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


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

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


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


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


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


  
  
  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. public void sendMessage() throws JMSException {
  9. for( int i = 0; i < jobs.length; i++)
  10. {
  11. String job = jobs[i];
  12. Destination destination = session.createQueue( "JOBS." + job);
  13. Message message = session.createObjectMessage(i);
  14. System.out.println( "Sending: id: " + ((ObjectMessage)message).getObject() + " on queue: " + destination);
  15. producer.send(destination, message);
  16. }
  17. }
  18. public static void main(String[] args) throws JMSException {
  19. Producer producer = new Producer();
  20. for( int i = 0; i < 10; i++) {
  21. producer.sendMessage();
  22. System.out.println( "Produced " + i + " job messages");
  23. try {
  24. Thread.sleep( 1000);
  25. } catch (InterruptedException x) {
  26. e.printStackTrace();
  27. }
  28. }
  29. producer.close();
  30. }

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


  
  
  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. public static void main(String[] args) throws JMSException {
  8. Consumer consumer = new Consumer();
  9. for (String job : consumer.jobs) {
  10. Destination destination = consumer.getSession().createQueue( "JOBS." + job);
  11. MessageConsumer messageConsumer = consumer.getSession().createConsumer(destination);
  12. messageConsumer.setMessageListener( new Listener(job));
  13. }
  14. }
  15. public Session getSession() {
  16. return session;
  17. }
具体注册的对象需要实现MessageListener接口:


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



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

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


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


  
  
  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的代码如下:


  
  
  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发消息,具体实现的代码如下:

  
  
  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. protected Message createStockMessage(String stock, Session session) throws JMSException {
  9. MapMessage message = session.createMapMessage();
  10. message.setString( "stock", stock);
  11. message.setDouble( "price", 1.00);
  12. message.setDouble( "offer", 0.01);
  13. message.setBoolean( "up", true);
  14. return message;
  15. }
在sendMessage方法里我们遍历每个topic,然后给每个topic发送定义的Message消息。在定义好前面发送消息的基础之后,我们调用他们的代码就很简单了:


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


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


  
  
  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()。这里我们采用异步的方式实现:

  
  
  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. public Session getSession() {
  10. return session;
  11. }
在前面的代码里我们先找到同样的topic,然后遍历所有的topic去获得消息。对于消息的处理我们专门通过Listener对象来负责。Listener对象的职责很简单,主要就是处理接收到的消息:


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

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

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


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


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


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

Client侧实现


  
  
  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. //Setup a message producer to send message to the queue the server is consuming from
  10. this.producer = session.createProducer(adminQueue);
  11. this.producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
  12. //Create a temporary queue that this client will listen for responses on then create a consumer
  13. //that consumes message from this temporary queue...for a real application a client should reuse
  14. //the same temp queue for each message to the server...one temp queue per client
  15. Destination tempDest = session.createTemporaryQueue();
  16. MessageConsumer responseConsumer = session.createConsumer(tempDest);
  17. //This class will handle the messages to the temp queue as well
  18. responseConsumer.setMessageListener( this);
  19. //Now create the actual message you want to send
  20. TextMessage txtMessage = session.createTextMessage();
  21. txtMessage.setText( "MyProtocolMessage");
  22. //Set the reply to field to the temp queue you created above, this is the queue the server
  23. //will respond to
  24. txtMessage.setJMSReplyTo(tempDest);
  25. //Set a correlation ID so when you get a response you know which sent message the response is for
  26. //If there is never more than one outstanding message to the server then the
  27. //same correlation ID can be used for all the messages...if there is more than one outstanding
  28. //message to the server you would presumably want to associate the correlation ID with this
  29. //message somehow...a Map works good
  30. String correlationId = this.createRandomString();
  31. txtMessage.setJMSCorrelationID(correlationId);
  32. this.producer.send(txtMessage);
  33. } catch (JMSException e) {
  34. //Handle the exception appropriately
  35. }
  36. }
这里的代码除了初始化构造函数里的参数还同时设置了 两个destination一个是自己要发送消息出去的destination,在这一句设置:

session.createProducer(adminQueue);
  
  
另外一个是自己要接收的消息destination, 通过这两句指定了要接收消息的目的地:

  
  
  1. Destination tempDest = session.createTemporaryQueue();
  2. responseConsumer = session.createConsumer(tempDest);

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

txtMessage.setJMSReplyTo(tempDest);
  
  

指定了这一部分,同时:

txtMessage.setJMSCorrelationID(correlationId);
  
  

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

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


  
  
  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来发送返回的消息:


  
  
  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. //Set the correlation ID from the received message to be the correlation id of the response message
  10. //this lets the client identify which message this is a response to if it has more than
  11. //one outstanding message to the server
  12. response.setJMSCorrelationID(message.getJMSCorrelationID());
  13. //Send the response to the Destination specified by the JMSReplyTo field of the received message,
  14. //this is presumably a temporary queue created by the client
  15. this.replyProducer.send(message.getJMSReplyTo(), response);
  16. } catch (JMSException e) {
  17. //Handle the exception appropriately
  18. }
  19. }
在replyProducer.send()方法里,message.getJMSReplyTo()就得到了要发送消息回去的destination。另外,设置这些发送返回信息的replyProducer的信息主要在构造函数相关的方法里实现了:


  
  
  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. //Delegating the handling of messages to another class, instantiate it before setting up JMS so it
  13. //is ready to handle messages
  14. this.messageProtocol = new MessageProtocol();
  15. this.setupMessageQueueConsumer();
  16. }
  17. private void setupMessageQueueConsumer() {
  18. ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(messageBrokerUrl);
  19. Connection connection;
  20. try {
  21. connection = connectionFactory.createConnection();
  22. connection.start();
  23. this.session = connection.createSession( this.transacted, ackMode);
  24. Destination adminQueue = this.session.createQueue(messageQueueName);
  25. //Setup a message producer to respond to messages from clients, we will get the destination
  26. //to send to from the JMSReplyTo header field from a Message
  27. this.replyProducer = this.session.createProducer( null);
  28. this.replyProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
  29. //Set up a consumer to consume messages off of the admin queue
  30. MessageConsumer consumer = this.session.createConsumer(adminQueue);
  31. consumer.setMessageListener( this);
  32. } catch (JMSException e) {
  33. //Handle the exception appropriately
  34. }
  35. }
总体来说,整个的交互过程并不复杂,只是比较繁琐。

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


  
  
  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在服务端的任何回执。那么问题来了:如果这时服务端已经出现了消息堆积,并且堆积程度已经达到“无法再接收新消息”的极限情况了,那么消息发送方如果知晓并采取相应的策略呢?

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


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


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


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


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


  
  
  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 都有一个索引,所以当你打算在代理中使用数千个队列的时候,不应该使用它。)

<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实例可以访问数据库。选择关系型数据库,通常的原因是企业已经具备了管理关系型数据的专长,但是它在性能上绝对不优于上述消息存储实现


  
  
  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种订阅恢复策略,可以极大程度增强非持久订阅的可用性。也就是说对于持久订阅的消费者是用不到内存存储的。

<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中增加如下查询字符串: 


  
  
  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中,增加如下查询字符串:


  
  
  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都需要单独设定:

"&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模式有关)。


  
  
  1. Message message = sessionMessageQueue.dequeue();
  2. if(message != null){
  3. ack(message);
  4. }
  5. return message

2. 异步消费机制

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


  
  
  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)。以下代码示例了如何在生产者端使用事务发送消息:


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

2. 消息消费者事务

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


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


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

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在超过重发上限后,也移入“死信队列”:


  
  
  1. <policyEntry queue= ">">
  2. <deadLetterStrategy>
  3. <sharedDeadLetterStrategy processNonPersistent= "true" />
  4. </deadLetterStrategy>
  5. </policyEntry>

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


  
  
  1. RedeliveryPolicy redeliveryPolicy = connectionFactory.getRedeliveryPolicy();
  2. // 设置最大重发次数
  3. redeliveryPolicy.setMaximumRedeliveries( 3);

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

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

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

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

猜你喜欢

转载自blog.csdn.net/weixin_42329335/article/details/86605914