1.简介
1.1. ActiveMQ 由Apache出品的开源消息总线。是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现。
1.2 它的优点是:
支持多种语言编写客户端;
对spring的支持,很容易和spring整合 ;
支持多种传输协议:TCP,SSL,NIO,UDP等 ;
支持AJAX ;
非常成熟,功能强大,在早些年业内大量的公司以及项目中都有应用 ;
单机吞吐量:万级
时效性:ms级
可用性:高,基于主从架构实现高可用性
消息可靠性:有较低的概率丢失数据
功能支持:MQ领域的功能极其完备
1.3 它的缺点是
偶尔会有较低概率丢失消息;
现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本;
1.4 使用场景
主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用
2.安装
2.1 下载安装包
2.2 启动activeMQ
解压缩,进入bin目录,启动/停止/查看状态:./activemq [start|stop|status]
启动信息如下
INFO | Apache ActiveMQ 5.15.9 (localhost, ID:LAPTOP-BRJ9A33I-60490-1563860323722-0:1) is starting
INFO | Listening for connections at: tcp://LAPTOP-BRJ9A33I:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600
INFO | Connector openwire started
INFO | Listening for connections at: amqp://LAPTOP-BRJ9A33I:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600
INFO | Connector amqp started
INFO | Listening for connections at: stomp://LAPTOP-BRJ9A33I:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600
INFO | Connector stomp started
INFO | Listening for connections at: mqtt://LAPTOP-BRJ9A33I:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600
INFO | Connector mqtt started
INFO | Starting Jetty server
INFO | Creating Jetty connector
WARN | ServletContext@o.e.j.s.ServletContextHandler@f107c50{/,null,STARTING} has uncovered http methods for path: /
INFO | Listening for connections at ws://LAPTOP-BRJ9A33I:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600
INFO | Connector ws started
INFO | Apache ActiveMQ 5.15.9 (localhost, ID:LAPTOP-BRJ9A33I-60490-1563860323722-0:1) started
INFO | For help or more information please see: http://activemq.apache.org
INFO | No Spring WebApplicationInitializer types detected on classpath
INFO | ActiveMQ WebConsole available at http://0.0.0.0:8161/
INFO | ActiveMQ Jolokia REST API available at http://0.0.0.0:8161/api/jolokia/
INFO | Initializing Spring FrameworkServlet 'dispatcher'
2.3 查看控制台
进入管理后台查看消息:http://ip:8161/admin 用户名:admin 密码:admin
3.编码实现
3.1 发送端编码
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.3</version>
</dependency>
package com.qhs.mq.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class MsgProducer {
public static void main(String[] args) {
testQueue();
testTopic();
}
private static void testQueue() {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");
Connection connection = null;
Session session = null;
MessageProducer producer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("qhs-queue");
producer = session.createProducer(queue);
TextMessage textMessage = null;
for (int i = 0; i < 50; i++) {
textMessage = session.createTextMessage("字符串queue消息:" + i);
producer.send(textMessage);
}
} catch (JMSException e) {
e.printStackTrace();
} finally {
try {
producer.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
session.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
private static void testTopic() {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");
Connection connection = null;
Session session = null;
MessageProducer producer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic("qhs-topic");
producer = session.createProducer(topic);
TextMessage textMessage = null;
for (int i = 0; i < 50; i++) {
textMessage = session.createTextMessage("字符串topic消息:" + i);
producer.send(textMessage);
}
} catch (JMSException e) {
e.printStackTrace();
} finally {
try {
producer.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
session.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
3.2 接收端编码
package com.qhs.mq.activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class MsgConsumer {
public static void main(String[] args) {
testQueue();
testTopic();
}
public static void testQueue() {
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");
Connection connection = null;
Session session = null;
MessageConsumer queueConsumer = null;
MessageConsumer topicConsumer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("qhs-queue");
queueConsumer = session.createConsumer(queue);
queueConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
try {
System.out.println(((TextMessage)message).getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
Topic topic = session.createTopic("qhs-topic");
topicConsumer = session.createConsumer(topic);
topicConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
try {
System.out.println(((TextMessage)message).getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
System.in.read();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
queueConsumer.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
topicConsumer.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
session.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
public static void testTopic() {
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://192.168.137.1:61616");
Connection connection = null;
Session session = null;
MessageConsumer consumer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
System.in.read();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
consumer.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
session.close();
} catch (JMSException e) {
e.printStackTrace();
}
try {
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
运行程序,先运行接收端,再运行发送端即可(topic消息默认不持久化)。
通过web控制台可以查看到对应的消息。
4.一些特征
- 点对点模式不会造成重复消费,一个生产者的消息只能给一个消费者消费;
- 发布订阅模型和点对点模型不一样,若发布消息之前未启动订阅者,则订阅者启动时,订阅者也无法接收到消息(未持久化状态)。另外,发布订阅模型,消费者是可以重新消费的;
- 消息保序方式是broker维持了一个consumer列表,对于同一session的消息取列表中的第一个来发送消息;
5.实现机制
5.1 可靠性保证
- 自动签收,生产者向队列发送消息后,只要消费者监听了消息队列,消费者将立刻获得消息,不管消费者是否成功取得消息,过程是否抛出异常导致消费者无法获得消息,都不会触发重试机制。--没有事务机制,没有补偿机制
- 事务签收,对于生产者而言,生产者要想向消息队列发送消息,必须提交事务。对于消费者而言,如果消费没有提交事务,则默认表示没有消费,会触发重试机制。-- 双方事务提交
- 手动签收,需要消费者手动签收,如果消费者没有进行签收,则默认消息没有被消费。--单方事务提交
代码范例:
//场景1
//生产者不开启事务,客户端必须有手动签收模式
Session session = createConnection.createSession(Boolean.FALSE, Session.CLIENT_ACKNOWLEDGE);
//消费者不开启事务,客户端必须有手动签收模式
TextMessage textMessage = (TextMessage) createConsumer.receive();
//接受消息
textMessage.acknowledge();
//场景2
//生产者不开启事务,客户端自动签收模式
Session session = createConnection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
//消费者不开启事务,自动签收消息
Session session = createConnection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
//场景3
//在支持事务的session中,producer发送message时在message中带有transactionID。broker收到message后判断是否有transactionID,如果有就把message保存在transaction store中,等待commit或者rollback消息
//事务消息 生产者以事务形式,必须要将消息提交事务,才可以提交到队列中。
Session session = createConnection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
session.commit();
//消费者 消费完后必须提交,不提交生产者不知道消费者消费了
Session session = createConnection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
session.commit();
5.2 事务机制
- 消息事务是在生产者producer到broker或broker到consumer过程中同一个session中发生的,保证几条消息在发送过程中的原子性。(Broker:消息队列核心,相当于一个控制中心,负责路由消息、保存订阅和连接、消息确认和控制事务)
- 消息生产者-异步发送
消息生产者使用持久(persistent)传递模式发送消息的时候,Producer.send() 方法会被阻塞,直到 broker 发送一个确认消息给生产者(ProducerAck),这个确认消息暗示broker已经成功接收到消息并把消息保存到二级存储中。这个过程通常称为同步发送。
如果应用程序能够容忍一些消息的丢失,那么可以使用异步发送。异步发送不会在受到 broker 的确认之前一直阻塞 Producer.send 方法。但有一个例外,当发送方法在一个事务上下文中时,被阻塞的是 commit 方法而不是 send 方法。commit 方法成功返回意味着所有的持久消息都以被写到二级存储中。
想要使用异步,在brokerURL中增加 jms.alwaysSyncSend=false&jms.useAsyncSend=true
如果设置了alwaysSyncSend=true系统将会忽略useAsyncSend设置的值都采用同步
- 当alwaysSyncSend=false时,“NON_PERSISTENT”(非持久化)、事务中的消息将使用“异步发送”;
- 当alwaysSyncSend=false时,如果指定了useAsyncSend=true,“PERSISTENT”类型的消息使用异步发送。如果useAsyncSend=false,“PERSISTENT”类型的消息使用同步发送。
总结:
- 默认情况(alwaysSyncSend=false,useAsyncSend=false),非持久化消息、事务内的消息均采用异步发送;对于持久化消息采用同步发送。
jms.sendTimeout:发送超时时间,默认等于0,如果jms.sendTimeout>0将会忽略(alwaysSyncSend、useAsyncSend、消息是否持久化)所有的消息都是用同步发送!- 即使使用异步发送,也可以通过producerWindowSize来控制发送端无节制的向broker发送消息:
producerWindowSize:窗口尺寸,用来约束在异步发送时producer端允许积压的(尚未ACK)的消息的尺寸,且只对异步发送有意义。每次发送消息之后,都将会导致memoryUsage尺寸增加(+message.size),当broker返回producerAck时,如果达到了producerWindowSize上限,即使是异步调用也会被阻塞,防止不停向broker发送消息。
通过jms.producerWindowSize=。。。来设置
5.3 持久化机制
ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的。在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等,然后试图将消息发送给接收者,发送成功则将消息从存储中删除,失败则继续尝试。消息中心启动以后首先要检查指定的存储位置,如果有未发送成功的消息,则需要把消息发送出去。
5.4 消息幂等性
消费者保证消息幂等性,不被重复消费
网络延迟传输中,会造成进行MQ重试中,在重试过程中,可能会造成重复消费
使用全局MessageID判断消费方使用同一个,解决幂等性
activeMQ提供了textMessageId,使用业务ID(订单号)也可以
String jmsMessageId = textMessage.getJMSMessageID();
网络延迟环境下,第二次请求过来,应该使用全局ID判断该消息是否被使用过
if(jmsMessageId == redis内的id){
//把消息签收掉,否则将继续重试,有些人觉得难理解,我解释一下:
textMessage.acknowledge(); //避免第三次重试
// 消息可以重发,但是消息不能做重复操作,如重复向数据库中做插入操作,造成幂等性问题,
//所以当第二次发送过来的时候,就可能造成重复提交问题,我们使用手动提交(业务中一般也是使用手动提交多)
//手动提交可以把重复发送的消息从队列中移除,那么接下来就不会触发重试了。
}
//将拿到的消息做业务处理,如插入修改操作等...
// 消费成功,把jmsMessageId放入redis
5.5 队列消息过滤收取
不同消息可以放到不同的队列,也可以放到一个队列中,通过增加消息的属性,在发送message的时候,可以设置它的消息属性值,然后在消费端建立消费队列时,传入过滤条件即可。
发送端:
MapMessage message1 = session.createMapMessage();
message1.setString("name","张1");//设置了name值
message1.setIntProperty("age",23);//设置了age属性
接受端:
MessageConsumer consumer = session.createConsumer(queue,"age > 30");
这样,这个consumer就只消费这个queue中age>30的消息。
5.6 消息保序性
利用Activemq的高级特性:consumer之独有消费者(exclusive consumer)
queue = new ActiveMQQueue("TEST.QUEUE?consumer.exclusive=true");
这样会确保consumer列表中,始终取一个主消费者来消费。但是缺点在于将会始终发往这个消费者,导致无法负载均衡。比如,订单消息,希望一个订单内的消息是保序的,但各个订单之间可以并发,这就需要用到下文消息组 messageGroup 的概念。
Message Groups特性是一种负载均衡的机制。在一个消息被分发到consumer之前,broker首先检查消息JMSXGroupID属性。如果存在,那么broker会检查是否有某个consumer拥有这个message group。如果没有,那么broker会选择一个consumer,并将它关联到这个message group。此后,这个consumer会接收这个message group的所有消息,直到:
- Consumer被关闭
- Message group被关闭,通过发送一个消息,并设置这个消息的JMSXGroupSeq为-1
例如:
message.setStringProperty("JMSXGroupID", "constact-20100000002");
.....
message.setIntProperty("JMSXGroupSeq", -1);
5.6 死信队列
在使用Message Queue的过程中,总会由于种种原因而导致消息失败。一个经典的场景是一个生成者向Queue中发消息,里面包含了一组邮件地址和邮件内容。而消费者从Queue中将消息一条条读出来,向指定邮件地址发送邮件。消费者在发送消息的过程中由于种种原因会导致失败,比如网络超时、当前邮件服务器不可用等。这样我们就希望建立一种机制,对于未发送成功的邮件再重新发送,也就是重新处理。重新处理超过一定次数还不成功,就放弃对该消息的处理,记录下来,继续对剩余消息进行处理。
ActiveMQ为我们实现了这一功能,叫做ReDelivery(重新投递)。当消费者在处理消息时有异常发生,会将消息重新放回Queue里,进行下一次处理。当超过重试次数时,会给broker发送一个"Poison ack",这个消息被认为是a poison pill(毒丸),这时broker会将这个消息发送到DLQ。
在以下四种情况中,ActiveMQ消息会被重发给客户端/消费者:
- 在一个事务session中,并且调用了session.rollback()方法。
- 在一个事务session中,session.commit()之前调用了commit.close()。
- 在session中使用CLIENT_ACKNOWLEDGE签收模式,并且调用了session.recover()方法。
- 在session中使用AUTO_ACKNOWLEDGE签收模式,在异步(messageListener)消费消息情况下,如果onMessage方法异常且没有被catch,此消息会被redelivery。
缺省情况下:持久消息过期,会被送到DLQ,非持久消息不会送到DLQ(不会redelivery)。
可以在connectionFactory中注入自定义的redeliveryPolicy来改变缺省参数。
activeMQ默认是发送六次,每次间隔1秒。
6.常见问题
6.1 消息堆积导致服务不可用
- ActiveMQ分为持久化和非持久化。非持久化消息是存储在内存中的,持久化消息是存储在文件中的,它们的最大限制在配置文件的<systemUsage>节点中配置。
- 非持久化消息堆积到内存告急时,ActiveMQ会将内存中的非持久化消息写入临时文件中,以腾出内存。(重启后持久化消息会从文件中恢复,非持久化的临时文件会直接删除)
- 当文件增大到达了配置中的最大限制的时:
- 持久化情况,到达文件限额时,生产者阻塞,消费者可连接可消费,消费后生产者继续运行:
设置2G左右的持久化文件限制,大量生产持久化消息直到文件达到最大限制,此时生产者阻塞,但消费者可正常连接并消费消息,等消息消费掉一部分,文件删除又腾出空间之后,生产者又可继续发送消息,服务自动恢复正常。- 非持久化情况,到达文件限额时,生产者阻塞,消费者可连接但不能消费,系统崩溃:
设置2G左右的临时文件限制,大量生产非持久化消息并写入临时文件,在达到最大限制时,生产者阻塞,消费者可正常连接但不能消费消息,或者原本慢速消费的消费者,消费突然停止。整个系统可连接,但是无法提供服务,就这样挂了。
解决方案:尽量不要用非持久化消息,非要用的话,将临时文件限制尽可能的调大
6.2 消息丢失
6.3 消息持久化慢
默认情况非持久化的消息是异步发送的,持久化的消息是同步发送的,遇到慢一点的硬盘,发送消息的速度很慢。
但是在开启事务的情况下,消息都是异步发送的,效率会有2个数量级的提升。所以在发送持久化消息时,请务必开启事务模式。发送非持久化消息时也建议开启事务,因为根本不会影响性能;
解决方案:消息发送启用事务;
6.4 消息不均匀消费(存在无法处理消息,prefech机制)
ActiveMQ的一个主要的设计目标是:提供一个高性能的消息中间件。它使用了SEDA(Staged Event Driven Architecture)架构及异步传输。为了提供更高的性能,很重要的一点是 尽快地将消息传送给消费者,这样消费者利用消息缓冲区等待处理,而不是等待消息。
然后,这样也有很大风险:不断地向 消费者 传送消息可能使得其消息缓冲溢出,因为传送的速度比消费者真正“消费”消息更快,这是很可能。
因此,ActiveMQ使用了 消息”预取限制“(prefetch limit):表示在某个时间段内,可能向消费者传输的最大消息量,如果达到该上限,那么停止发送,直到ActiveMQ收到消费者的acknowledgements(确认,表示已经处理了该消息)。prefetch limit可以针对每个不同的consumer来设置。
为了获取更高的性能,prefetch limit当然是越大越好,只要consumer有足够大的消息缓冲区(messagevolume)。如果消息的总量非常少,而且每个消息的处理时间非常的长,那么,可以将prefetch设置为1,这样,每次向consumer发送一个消息,等其确认已经处理完毕后,再发送第二个。
特别地,如果prefetch设置为0,表示consumer每次 主动向activeMQ要求传输最大的数据量,而不是被动地接收消息
原因在于ActiveMQ的prefetch机制:每个消费者获取消息时,是批量获取的,相当于预定。因为activeMQ提倡是尽量消费理念,所以会采取消费者批量获取信息的方式来提高消费效率。prefech的值默认是1000,即表示一个消费者在取消息时,会一次取1000条到自己的消息缓冲区中(不溢出),然后再一条一条的处理,每处理完一条,mq就会删除这一条,没处理的在mq中依然可见。
在发送一些消息之后,开启2个消费者去处理消息。会发现一个消费者处理了所有的消息,另一个消费者根本没收到消息。原因在于ActiveMQ的prefetch机制。当消费者去获取消息时,不会一条一条去获取,而是一次性获取一批,默认是1000条。这些预获取的消息,在还没确认消费之前,在管理控制台还是可以看见这些消息的,但是不会再分配给其他消费者,此时这些消息的状态应该算作“已分配未消费”,如果消息最后被消费,则会在服务器端被删除,如果消费者崩溃,则这些消息会被重新分配给新的消费者。但是如果消费者既不消费确认,又不崩溃,那这些消息就永远躺在消费者的缓存区里无法处理。更通常的情况是,消费这些消息非常耗时,你开了10个消费者去处理,结果发现只有一台机器吭哧吭哧处理,另外9台啥事不干。
解决方案:将prefetch设为1,每次处理1条消息,处理完再去取,这样也慢不了多少.
另外,当连接来自一个连接池中,消费消息可能出现一些由于“prefetch”而产生的问题:预取的消息(还未被处理)当连接关闭时会被释放(即,可以在activeMQ中再次读取到该消息)。而连接池中的连接只有在连接池关闭后才真正的销毁。这使得 预取的消息直到连接被重用时才会被处理(或者等连接池关闭,可再次从activeMQ中读取)。这样导致了消息可能丢失,或者当连接池中有多个连接时,消息乱序(out-of-sequence)!
- 消费者则不放入连接池!当然,这样在消费者的性能上,会受到影响(当有多个线程快速的消费消息时,对象被不断的创建销毁)。
- 将消费者的连接池中数量设为1。
- 在使用连接池的情况下,将prefetch设为1或者0。当使用Spring JMS和MessageDrivenPojo时,只能将prefetch设为1,而不能为0;