公司内部的希望使用云平台ActiveMQ服务的开发者。目前ActiveMQ服务提供并支持Java、C++、Python等几种客户端。
该手册帮助开发者快速理解ActiveMQ技术的适用范围,并对ActiveMQ的使用方法提供一些介绍。再者,该手册帮助开发者方便地使用云平台提供的统一服务。
名词/缩写 |
定义/描述 |
Acknowledge |
消息确认,表示已经成功接收到消息 |
JMS |
Java消息服务 |
JMS Client |
JMS客户端,包括消息生产者(Producer)和消息的消费者(Consumer) |
JMS Broker |
JMS服务提供者,负责与Client通信,是消息生产者和消费者之间的中转站 |
ConnectionFactory |
由客户端使用,用来创建一个连接 |
Connection |
连接,包括Transport和Network两种。 |
Transport Connector |
主要负责Broker和Client之间的通信,支持TCP/NIO/SSH/STOMP等协议 |
Network Connector |
主要负责Brokers之间的通信 |
Session |
用来发送和接收消息的线程上下文 |
MessageProducer |
由Session对象创建的消息生产者,以下简称Producer |
MessageConsumer |
由Session对象创建的消息消费者,以下简称Consumer |
Produce |
Producer发送一个消息 |
Consume |
Consumer接收到一条消息 |
Destination |
一个消息的目的地址或者源地址,这个类是Queue和Topic的超类 |
Queue |
用于端对端(PTP)模式的目的地址 |
Topic |
用于发布订阅(Pub/Sub)模式的目的地址 |
在介绍ActiveMQ之前,首先简要介绍一下JMS规范。
消息中间件(message-oriented middleware ,MOM)是在应用程序之间或者分布式系统中,以一种异步、松散耦合、可靠的方式进行通信的一类软件。由于消息中间件的引入,使消息源端到目的端的两点通信变成了源端、消息中间件、目的端的三点通信。
JMS(Java message service)是一种面向消息中间件的API,同时也是一种技术规范,便于消息系统中的应用程序间进行消息交互。类似于JDBC访问不同厂商的关系数据库一样,JMS提供了与厂商无关的访问消息系统的方法。ActiveMQ就是一种实现了JMS规范的消息中间件产品。
连接工厂是客户用来创建连接的对象,例如 ActiveMQ提供的ActiveMQConnectionFactory,通过这个对象可以创建消息客户端到消息服务提供者之间的连接。
JMS Connection封装了客户到JMS服务提供者之间的一个虚拟的连接。它代表消息客户端到消息服务器之间的逻辑通道。
JMS Session是生产和消费消息的一个单线程上下文。会话用于创建消息生产者(producer)、消息消费者(consumer)和消息(message)等。会话提供了一个事务性的上下文,在这个上下文中,一组发送和接收被组合到了一个原子操作中。
目的地是客户用来指定它生产的消息的目标和它消费的消息的来源的对象。JMS1.1规范中定义了两种消息传递模型:点对点(PTP)消息传递模型和发布/订阅消息传递模型。这两种模式对应于ActiveMQ的两个消息域:在点对点消息传递域中,目的地被称为队列(queue);在发布/订阅消息传递域中,目的地被成为主题(topic)。
点对点消息传递域的特点如下:
发布/订阅消息传递域的特点如下:
JMS Client实现了了MessageProducer和MessageConsumer接口。也就是说,客户端分成了两种类型:用于发送消息的消息生产者(Producer)和用于接收消息的消息消费者(Consumer)。
消息生产者是由会话创建的一个对象,用于把消息发送到一个目的地。
客户端使用MessageProducer来向Destination 发送消息。通过向会话的createProducer()方法传入Queue 或Topic来创建MessageProducer。
客户端也可以不提供目的地来创建消息生产者。在这种情况下,必须在每次发送操作时提供目的地。这种风格的生产者的通常用于使用请求的JMSReplyTo 目的地来发送请求的回复。
客户端可以指定一个缺省的转发模式、优先级和消息的生存时间。它也可以为每个消息指定转发模式、优先级和消息的生存时间。
JMS client使用MessageProducer类来向一个目的地址(destination)发送消息。通常来讲,一个producer的目的地址在producer被建立时已经确认。
MessageProducer producer = session.createProducer();
对于每条消息,也提供了覆盖这个地址的方法:
void MessageProducer.send(Destination destination, Message message) throws JMSException;
消息消费者是由会话创建的一个对象,它用于接收发送到目的地的消息。
客户端使用MessageProducer来向Destination发送消息。通过向Session的createConsumer()方法传入Queue 或Topic来创建MessageConsumer。
可以用消息选择器来(Selector)让broker限制转发到消费者的消息,只有符合选择器的消息才能被转发到该消费者。关于消息选择器请参考:http://activemq.apache.org/selectors.html了解更多内容。
消息的消费可以采用以下两种方法之一:
JMS消息由以下三部分组成:
消息头:包含了客户端和broker用来路由和识别消息的元数据。头字段包括:JMSDestination、JMSDeliveryMode、JMSTimeStamp、JMSCorrelationID、JMSReplyTo、JMSRedelivered、JMSType、JMSExpiration、JMSPriority等。每个消息头字段都有相应的getter 和setter方法。更多细节请参考:http://activemq.apache.org/activemq-message-properties.html
消息属性:除了头字段定义的信息以外,Message 接口包含了内置的设置来支持属性值。因而,这就提供了一种为消息增加可选头信息的机制。通过消息选择器(Selector)可以使客户端让JMS提供者按照应用指定的规则选择消息。
消息体。JMS定义的消息类型有以下五种:
TextMessage:包含了一个java.lang.String
MapMessage:包含了一些列的name-value对,name是String,value是Java primitive类型。消息体中的条目可以被enumerator按照顺序访问,也可以自由访问。
BytesMessage:包含一个不间断的字节流。
StreamMessage:包含了一个Java primitive流对象,这个流可以顺序读取和填充。
ObjectMessage:包含了一个可序列化的Java对象。
JMS基于一套通用的消息概念。每个JMS消息域(PTP和Pub/Sub)也都定义了一套自己概念的接口。JMS通用接口则提供了不依赖于PTP和Pub/Sub消息域的能力。
Classic API 是JMS 1.x 版本的API,Simplified API 是JMS 2.0版本的API。
JMS消息只有在被确认之后,才认为已经被成功地消费了。消息的成功消费通常包含三个阶段:客户接收消息、客户处理消息和消息被确认。
在事务性会话中,当一个事务被提交的时候,确认自动发生。在非事务性会话中,消息何时被确认取决于创建会话时的应答模式(acknowledgement mode)。该参数有以下三个可选值:
Session.AUTO_ACKNOWLEDGE。当客户成功的从receive方法返回的时候,或者从MessageListener.onMessage方法成功返回的时候,会话自动确认客户收到的消息。
Session.CLIENT_ACKNOWLEDGE。客户通过消息的acknowledge方法确认消息。需要注意的是,不要被这个方法的表象所迷惑。在这种模式中,虽然调用的是Message类上的方法,但是确认是在会话层(Session)上进行的。确认一个被消费的消息将自动确认所有已被会话消费的消息。例如,如果一个消息消费者消费了10个消息,然后确认第1个消息,那么所有10个消息都被确认。
Session.DUPS_OK_ACKNOWLEDGE。这个选项让会话延迟发送消息的确认。如果JMS失败,则可能导致消息重复发送,因此只能在消息的消费者容忍重复消息时使用。它的好处在于通过最小化会话确认消息所作的工作来减少了会话的负荷。如果是重复的消息,那么JMS provider必须把消息头的JMSRedelivered字段设置为true。几种消息确认方式性能分析见下表:
表1消息确认方式性能分析
确认模式 |
描述 |
SESSION_TRANSACTED |
可靠的消息确认方式,执行事务提交时,一次确认这个事务中所有以消费的消息,性能良好。 |
CLIENT_ACKNOWLEDGE |
允许客户端一次确认多条以消费消息,性能良好。 |
AUTO_ACKNOWLEDGE |
默认的消息确认机制,性能较慢。 |
DUPS_OK_ACKNOWLEDGE |
当客户端缓冲区达到预取上限的50%时,发出确认。在消息确认的标准方式中,是最快的一种。 |
INDIVIDUAL_ACKNOWLEDGE |
每消费一条消息,确认一次,性能很差。 |
optimizeAcknowledge |
严格意义上不能算作确认方式,它同AUTO_ACKNOWLEDGE确认方式一起工作。当预取上限的65%消息被消费时,发出一条确认。是最快的一种确认方式。 |
【注意】当使用optimizeAcknowledge或者DUPS_OK_ACKNOWLEDGE消息确认模式
当使用optimizeAcknowledge或者DUPS_OK_ACKNOWLEDGE消息确认模式时时,一个消息ACK包含可以包含一系列已经消费的消息,可以显著提高性能。
批处理ACK的缺点是:如果Consumer的connection断开的话,可能会造成消息的重复投递。对于高吞吐量应用,如果重复消息影响不大的话,建议采用批处理确认,可以显著提高性能。同时Consumer端的重复消息探测可以降低接收重复消息的风险。
JMS 支持以下两种消息提交模式:
PERSISTENT:用于严格的不能丢失的消息。broker收到消息后先将消息持久化存储在本地磁盘中,然后对生产者进行确认。然后再分发给消费者。当消息被消费者消费后,会对broker发出确认,broker收到消费者的确认才会从磁盘中删除消息。即使遭遇到系统奔溃,broker重启后也可以从磁盘中恢复所有未被消费的消息。
NON-PERSISTENT:用于可以允许偶尔有消息丢失的场合。消息不需要保存到本地磁盘,消息存储和分发全部是在内存中进行的,处理速度比persistent要快很多。但是当broker系统崩溃时,内存的消息会丢失。由于不涉及消息持久化存储,处理速度要比persistent快很多。
默认情况,queue是persistent的,topic是non-persistent。
根据测试结果:NON-PERSISTENT要比PERSISTENT快2个数量级。所以根据使用场合,定制消息传输模式,对性能至关重要!
可以使用消息优先级来指示JMS provider首先提交紧急的消息。优先级分10个级别,从0(最低)到9(最高)。如果不指定优先级,默认级别是4。需要注意的是,JMS provider并不一定保证(Best Effort)严格按照优先级的顺序提交消息。
在AMQ5.4之后,提供了支持消息优先级的Queue,默认情况下关闭的,如果需要开启,需要配置文件中,设置Per Destination Polices的prioritizedMessages属性。
......
可以设置消息在一定时间后过期,默认是永不过期。
设置消息过期时间的方法有以下两种:
producer.setTimeToLive()
producer.send(Destination, Message, int, int, long)
详情参考: http://activemq.apache.org/how-do-i-set-the-message-expiration.html
Exception : How to set Expiration time for VirtualTopic Queues ?
对于使用VirtulTopic 模式的Queue 。 生产者生产Topic , VirtualTopic.Test . 消费者试图去消费Queue, Consumer.A.VirtualTopic.Test 。 MQ Broker负责创建队列, Consumer.A.VirtualTopic.Test 。 生产者是MQ broker 本身。 无法从Client端进行设置。 该如何配置?
可以通过会话上的createTemporaryQueue 方法和createTemporaryTopic方法来创建临时目的地。它们的存在时间只限于创建它们的连接(Connection)所保持的时间。只有创建该临时目的地的连接上的消息消费者才能够从临时目的地中提取消息。
通常采用临时队列来实现request-response模型,这种种模型中client端在启动时创建一个临时的queue,设置这个会话上所有消息(request message)的返回地址是这个临时队列;当server端处理这些请求后的返回消息(response message)利用消息属性JMSCorrelationID来把每一个response消息和相应的请求消息关联起来。具体实现请参考:http://activemq.apache.org/how-should-i-implement-request-response-with-jms.html
持久/非持久订阅只存在于Topic消息域中。默认情况下,Topic是非持久化发布(NON-DURABLE)消息的。topic收到发布者产生的消息后,只会把它广播给在线的订阅者。如果订阅者迟于发布者连接到broker或者订阅者与broker之间的连接出现中断,那么订阅者将不能接收在离线时间段内这个topic新发布的消息。
如果要想让一个订阅者能够接收一个topic的所有消息,即使离线也能让broker保存消息,直到这个订阅者上线时在分发过来,这时就用到一个新的概念:持久订阅(durable subscription)。持久订阅可以让订阅者离线后也不会丢失任何消息。
设置持久订阅的方法分两步来完成:
第一步:设置ClientID,传递给broker。因为broker会区分每个持久化订阅者的clientID。在默认情况下,cleintID是null,如果采用默认配置,当第一个持久化订阅者连上后,第二个持久化订阅者再次请求连接会收到"You cannot create a durable subscriber without specifying a unique clientID on a Connection"的错误,所以最好在建立连接前,设置好每个客户端的clientID。示例代码如下
connection = connectionFactory.createConnection();
connection.setClientID( "myUniqueCleintID");
connection.start();
第二步:从当前session建立持久化订阅者。示例代码如下:
consumer = session.createDurableSubscriber((Topic)destination, "myDurableSubName");
持久订阅在创建之后会一直保留,直到应用程序调用会话上的unsubscribe方法。
在一个JMS客户端,可以使用本地事务来组合消息的发送和接收。
JMS Session 接口提供了commit()和rollback()方法。事务提交意味着生产的所有消息被发送,消费的所有消息被确认;事务回滚意味着生产的所有消息被销毁,消费的所有消息被恢复并重新提交,除非它们已经过期。
事务性的会话总是牵涉到事务处理中,commit或rollback方法一旦被调用,一个事务就结束了,而另一个事务被开始。关闭事务性会话将回滚其中的事务。需要注意的是,如果使用请求/回复机制,即发送一个消息,同时希望在同一个事务中等待接收该消息的回复,那么程序将被挂起,因为知道事务提交,发送操作才会真正执行。
需要注意的还有一个,消息的生产和消费不能包含在同一个事务中。
ActiveMQ目前支持的transport有:VM Transport、TCP Transport、SSLTransport、Peer Transport、UDP Transport、Multicast Transport、HTTP andHTTPS Transport、Failover Transport、Fanout Transport、DiscoveryTransport、ZeroConf Transport等。以下简单介绍其中的几种,更多请参考http://activemq.apache.org/activemq-4-connection-uris.html
VM transport允许在VM内部通信,从而避免了网络传输的开销。这时候采用的连接不是网络通信,而是JVM内部方法调用。第一个创建VM连接的客户会启动一个embed VM broker,接下来所有使用相同的broker name的VM连接都会使用这个broker。当这个broker上所有的连接都关闭的时候,这个broker也会自动关闭。当Client和Broker部署在同一台机器上时,可以采用这种配置,可以降低消息传输时延。
以下是配置语法:
vm://brokerName?transportOptions
例如:vm://broker1?marshal=false&broker.persistent=false
Transport Options的可选值如下:
选项名称 |
默认值 |
描述 |
marshal |
false |
所有发送到该transport的命令是否进行序列化 |
wireFormat |
default |
WireFormat的名称 |
wireFormat.* |
|
所用WireFormat的属性 |
create |
true |
如果broker不存在,是否创建 |
waitForStart |
-1 |
Broker启动前需要等待的毫秒数 |
broker.* |
|
Broker的属性 |
需要注意的是,如果程序中启动了多个不同名字的VM broker,那么可能会有如下警告:
可以通过在transportOptions中追加 broker.useJmx=false来禁用JMX来避免这个警告。关于VM的更详细配置请查看:http://activemq.apache.org/vm-transport-reference.html
TCP transport允许客户端通过TCP socket连接到broker。以下是配置语法:
tcp://hostname:port?transportOptions
这是broker默认使用的transport协议。适用于大部分场合,采用阻塞I/O操作,降低网络延迟。
例如:tcp://localhost:61616?trace=false
ActiveMQ同时提供了支持NIO协议的transport,它的底层也是通过TCP协议实现的。与TCP协议不同的是,它通过SELECT机制来管理多路非阻塞式连接,降低了系统的线程数。提供很好的垂直扩展性。如果一个broker上面的连接过多时,应该采用这种方式来替代TCP transport协议。配置语法与TCP相同:
nio://hostname:port?transportOptions
TCP transport提供配置底层协议的许多属性,更多配置细节请参考:http://activemq.apache.org/tcp-transport-reference.html
Failover Transport是一种重新连接的机制,它工作于其它transport的上层,用于建立可靠的传输。它的配置语法允许制定任意多个复合的URI。Failover transport会自动选择其中的一个URI来尝试建立连接。如果没有成功,那么会选择一个其它的URI来建立一个新的连接。以下是配置语法:
failover:(uri1,...,uriN)?transportOptions
failover:uri1,...,uriN
例如:failover:(tcp://localhost:61616,tcp://remotehost:61616)?initialReconnectDelay=100
缺省重连策略:随机选取一个broker开始重新连接,首次重连前有10ms延迟,如果没有连接成功,这个延迟时间加倍,直到30000ms之后这个延迟时间不变。不断重连,直至连上。由于failover重连机制的存在,所以即使连接到一个broker,也应该采用failover协议。例如:
failover:(tcp://localhost:61616)
当client与broker之间的连接断开时,会按照重连策略自动重连。
failover协议选取下一次重连地址的策略是:获取所有可用的uri组成一个list;从list中剔除上次重连失败的uri;把list中的元素随机交换n次;把上次失败的uri放入到list的末尾。
使用说明:
1、这个特性是用来配置client端,broker不需要做额外配置。
2、failover协议工作于其它transport协议的上层,所有Client端都应该使用这个协议。
关于failover transport协议的属性设置,请参考:http://activemq.apache.org/failover-transport-reference.html
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.Destination;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ProducerTest {
private static String uri = "tcp://192.168.17.156:61616";
private static String user = "";
private static String password = "";
private static String subject = "TEST.FOO";
private static boolean topic = false;
private static boolean transacted = false;
private static boolean persistent = true;
private static int messageCount = 2000;
private static int messageSize = 100;
private static Connection connection;
private static long messageSend = 0;
public static void main(String[] args) {
Destination destination = null;
try {
// Create connection
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
user, password, uri);
connectionFactory.setProducerWindowSize(16384);
connection = connectionFactory.createConnection();
connection.start();
if (connection == null) {
System.out.println("can't create connection!");
System.exit(-1);
} else
System.out.println("create connection!" + connection);
// Create Session
Session session = connection.createSession(transacted, Session.AUTO_ACKNOWLEDGE);
// Create Topic/Queue
if (topic)
destination = session.createTopic(subject);
else
destination = session.createQueue(subject);
// Create message producer
MessageProducer producer = session.createProducer(destination);
if (persistent)
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
else
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//publish messages
for(int i = 0; i < messageCount; i++) {
TextMessage textMessage = session.createTextMessage(createMessage(i));
System.out.println("send message[" + i + "]");
producer.send(textMessage);
messageSend++;
}
} catch (JMSException e) {
System.err.println("Caught a Excepthion!");
e.printStackTrace();
} finally {
try {
connection.close();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Already send " + messageSend + " messages. Connection closed!");
System.exit(0);
} catch (JMSException e) {
e.printStackTrace();
}
}
}
private static String createMessage(int index) {
StringBuffer buffer = new StringBuffer("Message[" + index + "]");
if (buffer.length() >= messageSize)
return buffer.substring(0, messageSize);
for (int i = buffer.length(); i < messageSize; i++)
buffer.append(i);
return buffer.toString();
}
}
import javax.jms.Connection;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import org.apache.activemq.ActiveMQConnectionFactory;
public class ConsumerTest implements MessageListener, Runnable {
private MessageProducer replyProducer;
private Session session;
static final int messageSize = 100;
private String uri = "tcp://192.168.17.156:61616";
private String user = "";
private String password = "";
private String subject = "TEST.FOO";
private boolean topic = false;
private boolean durable = true;
private boolean transacted = false;
private int messageCount = 2000;
private int sleepTime = 0;
private boolean syncReceive = false;
private long messageReceive = 0;
public static void main(String[] args) {
new ConsumerTest().run(); //simple test
}
@Override
public void onMessage(Message message) {
messageReceive++;
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage)message;
try {
if (textMessage.getText().length() <= 50) {
System.out.println("recieve Message: " + textMessage.getText());
} else {
String string = textMessage.getText().substring(0, 50);
System.out.println("recieve Message: " + string + " ...");
}
//message.acknowledge();
} catch (JMSException e) {
e.printStackTrace();
}
} else {
System.out.println("recieve Message " + message);
}
try {
if(message.getJMSReplyTo() != null) {
replyProducer.send(message.getJMSReplyTo(),
session.createTextMessage("Reply: " + message.getJMSCorrelationID()));
}
} catch (JMSException e) {
e.printStackTrace();
}
try {
//simulate a slow consumer
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
}
@Override
public void run() {
Connection connection = null;
Destination destination = null;
MessageConsumer consumer = null;
try {
// create Connection
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
user, password, uri);
connection = connectionFactory.createConnection();
connection.setClientID("gsw-win7");
connection.start();
// Create Session
session = connection.createSession(transacted, Session.AUTO_ACKNOWLEDGE);
// Create Topic/Queue
if (topic)
destination = session.createTopic(subject);
else
destination = session.createQueue(subject);
replyProducer = session.createProducer(null);
replyProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
if (durable && topic)
consumer = session.createDurableSubscriber((Topic)destination, "myDurableSubName");
else
consumer = session.createConsumer(destination);
//sync receive
if (syncReceive && messageCount > 0) {
for(int i = 0; i < messageCount; i++) {
Message message = consumer.receive(1000);
if (message != null) {
onMessage(message);
}
}
System.out.println("Consumer " + messageReceive + " messages. Close connection!");
consumer.close();
session.close();
connection.close();
} else { //async receive
consumer.setMessageListener(this);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}