一、上手实例
在开始之前,需要进行如下几个步骤:
- 去Apache官网下载ActiveMQ,下载经典版classic即可;
- 在SpringBoot项目中引入
activemq-all
的maven的依赖包; - 在下载的ActiveMQ中,找到
activemq.bat
启动,然后就可以使用浏览器进行访问了; - 访问地址为
http://localhost:8161
,初始账密为admin/admin
;
(特别说明:本文使用的ActiveMQ版本是5.15.11)
然后,我们就可以开始编写生产者代码了:
public class HelloProducer {
private static final String MQ_URL = "tcp://127.0.0.1:61616";
/**
* 前置操作:
* 1.下载ActiveMQ,并启动;
* 2.引入activemq-all的依赖包;
* */
public static void main(String[] args) throws JMSException {
// 步骤一:创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(MQ_URL);
// 步骤二:创建连接并打开
Connection connection = connectionFactory.createConnection();
connection.start();
// 步骤三:创建会话,第一个参数代表是否启用事务,先不启用;第二个参数代表自动应答;
Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
// 步骤四:创建队列
Destination destination = session.createQueue("firstQueue");
// 步骤五:创建生产者
MessageProducer producer = session.createProducer(destination);
// 步骤六:创建消息
TextMessage message = session.createTextMessage("第一个消息!!!");
// 步骤七:发送消息
producer.send(message);
System.out.println("消息发送完毕!");
// 步骤八:关闭连接
connection.close();
}
}
接着,消费者代码比较类似,如下:
public class HelloConsumer {
private static final String MQ_URL = "tcp://127.0.0.1:61616";
public static void main(String[] args) throws JMSException {
// 步骤一:创建连接工厂
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(MQ_URL);
// 步骤二:创建连接并打开
Connection connection = connectionFactory.createConnection();
connection.start();
// 步骤三:创建会话,第一个参数代表是否启用事务,先不启用;第二个参数代表自动应答;
Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
// 步骤四:创建队列
Destination destination = session.createQueue("firstQueue");
// 步骤五:创建消费者
MessageConsumer consumer = session.createConsumer(destination);
// 步骤六:消费者监听消息
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
TextMessage currentMessage = (TextMessage) message;
try {
System.out.println(currentMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
}
}
此时,我们先运行消费者,再运行生产者,就能看到想要的上手实例结果了。
二、管控台的账密配置
使用浏览器登录管控台的时候,默认账密是admin/admin
,这个是其实是配置在安装目录下的conf/jetty-realm.properties里面,打开可以看到最后三行内容如下:
# username: password [,rolename ...]
admin: admin, admin
user: user, user
第一行是注释,告诉我们各个单词分别代表什么意思,可以看到,默认的账号其实还有user/user
,默认的角色有admin
和user
之分。如果你想新增一个角色,只需要在后面按照格式添加即可。
三、程序收发消息的账密安全控制
如果我们想管控程序代码读写消息呢?当然也需要配置相应的账密信息。
我们在安装目录下的conf/activemq.xml
中的之上,添加如下内容:
如上,代表的意思就是程序代码要通过配置账密才能获取到对应角色的收发消息的权限。
保存退出如上配置文件后,我们再来启动上面的上手实例,发现启动全部失败,报错信息显示为:
Exception in thread "main" javax.jms.JMSSecurityException: User name [null] or password is invalid.
提示我们账密不对,现在我们更改创建连接的代码:
private static final String MQ_USER_NAME = "zhangsan";
private static final String MQ_USER_PASSWORD = "123";
public static void main(String[] args) throws JMSException {
...
// 步骤二:创建连接并打开
Connection connection = connectionFactory.createConnection(MQ_USER_NAME, MQ_USER_PASSWORD);
...
}
需要注意,生产者和消费者需要同步更改,然后再启动消费者、生产者,就能看到正常地收发消息了。
四、切换持久化数据库
ActiveMQ默认采用的kahaDB,我们在配置文件activemq.xml
中可以看到如下的配置:
其实kahaDB个人不是很熟悉,如果我们需要持久化消息的话,更希望是存储到MySQL、PostgreSQL、Oracle这种常见的数据库中。这里我们以PostgreSQL为例,其余的数据持久化配置可以看下文末的参考资料一。
首先,我们需要在配置文件activemq.xml
中注册PostgreSQL的连接信息:
需要注意的是,以上bean的配置需要放在配置文件中
之外,然后我们需要注释默认的kahaDB引用,改为使用PostgreSQL。
因为,我们连接数据库使用了DBCP的数据源,所以需要额外引入如下两个jar,将它们下载下来后,放到安装目录下的lib
文件夹下即可。
- commons-dbcp2-2.7.0
- postgresql-42.2.9
此时,我们再启动activeMQ,就可以显示成功运行了,而且,数据库中会自己多出来三张表:
- activemq_acks,用于存储订阅关系,如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存。
- activemq_lock,用于存储消息,Queue和Topic都存储在这个表中。
- activemq_msgs,在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker。
我们主要关注activemq_msgs
这张表,当我们启动上例中的生产者,就会发现此表中会多一条数据,说明activeMQ默认开启了持久化策略,只有当我们再启动消费者,将该消息消费掉后,该表中的该条信息才会被清空。
关于持久化的详细讲解,下面还会详细讲到,此处可以先略过。
Tips:为了确保后面启动的成功,在确保持久化生效后,将createTablesOnStartup
的值改为false,否则再次启动会报错。
五、收发模型
最常使用的收发模型分为以下两种:
-
P2P
我们在上面的实例中使用的就是P2P收发模型,它的特点就是点对点,一个生产者绑定队列后产生消息发送给一个同样绑定此队列的消费者;
主要表现P2P模型的代码如下:
// 步骤四:创建队列-P2P收发模型 Destination destination = session.createQueue("firstQueue");
当然,我们也可以设置多个生产者和多个消费者都绑定同一个队列。从生产者角度看,大家共同产生同一个队列的消息,该消息可以被任意绑定该队列的消费者消费;从消费者角度看,大家都有机会获得该队列中的某个消息,但是不会存在两个及以上的消费者获得同一个的消息的情况;
-
Topic
主题模型的特点就是一个生产者产生该主题绑定的消息,任意多的订阅该主题的消费者都可以获得一份相同的消息;
主要表现Topic模型的代码如下:
// 步骤四:创建主题 Destination destination = session.createTopic("firstTopic");
当然,我们也可以设置多个生产者绑定同一个主题,效果也是一样的。
六、消息的持久化VS非持久化
-
在P2P模型下,我们通过上面持久化到数据库的案例可以看出,默认就是支持持久化的。
倘若所有该队列的消费者都宕机了,那么生产者产生的尚未被消费的消息会保存到数据库中进行持久化,哪怕activeMQ也宕机了,也不怕消息的丢失,等到重启服务和消费者后,消费者端照样可以收到这些尚未被消费的消息(暂不考虑消息的有效期问题)。
如果想要使用非持久化的方式,那么在生产者创建完的时候,使用如下的方式:
// 步骤五:创建生产者 MessageProducer producer = session.createProducer(destination); // 设置为非持久化 producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
如此,生产者产生的消息如果不被消费者立即消费,那么就暂时保存在内存中,不会持久化到数据库,一旦Active MQ重启,这些未消费的消息就丢失了。
-
在Topic模型下,默认是非持久化的,生产者产生的消息只能保存在的内存中,一旦Active MQ重启,这些未消费的消息就丢失了;而且,保存在内存中的消息只能发送给当前在线的订阅该主题的消费者,当前不在线的消费者后续立即启动订阅该主题也只能收到后续的消息,当前已经发送的消息无法再获取。
// 步骤四:生产者创建主题 Destination destination = session.createTopic("firstTopic");
// 步骤四:消费者订阅主题 Destination destination = session.createTopic("firstTopic");
如果生产者产生的主题消息没有任何一个消费者订阅,那么这些消息理论上就谁也收不到,只能等待消息自己过期或者重启Active MQ的时候清空它们。
当然了,Topic模型也是可以设置成持久化的,此处需要设置的不是生产者,而是消费者:
public static void main(String[] args) throws JMSException { // 步骤一:创建连接工厂 ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(MQ_URL); // 步骤二:创建连接并打开 Connection connection = connectionFactory.createConnection(MQ_USER_NAME, MQ_USER_PASSWORD); // 持久化订阅者需要设置连接ID connection.setClientID("firstTopicConnection_1"); connection.start(); // 步骤三:创建会话,第一个参数代表是否启用事务,先不启用;第二个参数代表自动应答; Session session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE); // 步骤四:创建主题 Topic topic = session.createTopic("firstTopic"); // 步骤五:创建持久订阅者,并且需要指定订阅者名称 MessageConsumer consumer = session.createDurableSubscriber(topic,"firstTopic_subscriber_1"); // 步骤六:消费者监听消息 consumer.setMessageListener(new MessageListener() { @Override public void onMessage(Message message) { TextMessage currentMessage = (TextMessage) message; try { System.out.println("HelloConsumer收到的消息为:" + currentMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } }); }
当我们启动如上的持久化订阅者之后,ActiveMQ就会记住该订阅者,并保存一条记录到数据库表
activemq_acks
中;如果此时我们的持久化订阅者不幸宕机了,并且随后生产者开始发出该主题的消息,那么这些消息都会被自动地存储到数据库表
activemq_msgs
中;当我们地持久化订阅者再次重启后,仍然可以获取到自它宕机以来该主题下的消息;
如果生产者发送消息后,持久化订阅者还没有来得及消费,我们的ActiveMQ宕机了,也不要紧,等重启后,订阅者仍然可以获得这些消息;
Tips:
- Topic模式下,生产者发送的消息是否设置持久化的模式根本不重要,关键在于是否存在持久化的订阅者,只要有持久化的订阅者,那么它自注册以后错过的任何消息都会被持久化,等待它重启后再消费;
- 持久化的订阅者消费完它错过的消息后,这些消息仍然会保存在数据库表中,等待Active MQ下次重启才会进行清理,对于未消费的消息则不会被清理;
七、自动确认VS手动确认
我们在上面的例子中,一直都是使用的自动确认机制,为什么要确认?
只有当消费者发送确认消息给Active MQ的时候,MQ才能确认此条消息已经被消费者正确消费了,就可以从内存/数据库删除了,如果没有收到消费者的确认,那么MQ就会认为消费者没有正确消费该消息,需要等待重发。
在自动确认时,默认情况下,如果消费方在处理消息的时候抛出RuntimeException
,则MQ会以每隔1秒的时间间隔重发6次,者6次还是失败的话,则不再重发。并将该消息置为已经消费(该版本的Active MQ默认是不启用死信队列的,关于死信的讲解后文会详细阐述,此处先忽略)。
// 步骤六:消费者监听消息
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
// 模拟消息接受后处理失败
throw new RuntimeException("bad request!");
}
});
在手动确认的情况下,如果应答前抛出RuntimeException
,则没有重发机制,但是MQ中的消息会显示还未被消费。
// 步骤三:创建会话,第一个参数代表是否启用事务,先不启用;第二个参数代表自动应答;
Session session = connection.createSession(Boolean.FALSE, Session.CLIENT_ACKNOWLEDGE);
......
// 步骤六:消费者监听消息
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
TextMessage currentMessage = (TextMessage) message;
try {
System.out.println("HelloConsumer收到的消息为:" + currentMessage.getText());
HelloConsumer.getRuntimeException();
// 手动确认
currentMessage.acknowledge();
} catch (JMSException e) {
e.printStackTrace();
}
}
});
private static void getRuntimeException(){
throw new RuntimeException("bad request!");
}
八、消息的优先级&过期时间
消息的优先级共有0~9十个级别,其中默认是4,小于4为普通消息,大于4为加急消息;
理论上,加急消息会优先于普通消息投递给消费者,但是并不能确保消息严格按照优先级顺序进行投递;
消息的过期时间则是以毫秒为单位,超过这个时间,还未被消费的消息就会被置为已经消费,此时即使消费者再启动,也无法获取这些消息了(该版本的Active MQ默认是不启用死信队列的,关于死信的讲解后文会详细阐述,此处先忽略)。
// 步骤七:发送消息,第一个参数为消息,第二个参数为消息是否持久化,第三个参数是消息优先级,第四个参数为消息过期时间10分钟
producer.send(message, DeliveryMode.NON_PERSISTENT, 4, 1000*60*10);
九、死信队列
当前使用的ActiveMQ版本默认是没有开启死信机制的,所以在如上重发6次失败和消息过期的例子中,未被成功消费的信息都只是被置为消费完成,并没有进入死信队列。
如果想要开启死信,需要在配置文件activemq.xml
中的
里面增加如下的配置:
如上非持久化的消息如果不设置,或者设置成false,那么就不会被保存到死信队列;
如上过期的消息如果不设置,或者设置为true,那么默认会保存到死信队列,只有设置为false的时候,才不会被保存到死信队列;
如此,我们再来尝试上述重发失败6次和消息过期的场景,就会发现未被成功消费的消息除了被置为消费完成以外,也会被放入死信队列中。
如上是针对非持久化的消息而言的,如果是持久化的消息呢?
默认情况下,持久化的消息如果异常消费或者过期都是会进入死信队列的,但是通过如上设置processExpired
为false,可以让过期的持久化消息不进入死信队列(过期后,持久化的消息就会被从数据库中清除),但是如果想让异常消费的持久化消息不进入死信队列,需要在
中做如下配置才能生效:
意思就是,取消所有类型的消息进入死信队列。
十、事务的使用
要想启用事务,需要在创建session的时候进行指定:
// 步骤三:创建会话,第一个参数代表是否启用事务,先不启用;第二个参数代表自动应答;
Session session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
然后,调用提交事务:
session.commit();
// 步骤八:关闭连接
connection.close();
那么,在事务commit之前,所有的send操作都不会真的执行,一旦遇到异常,则全部回滚,只有commit之后,消息才会被发送到Active MQ上面。
十一、消费者的选择器
当我们有多个消费者都监听同一个队列的时候,消费者可以根据该队列中消息的property对消息进行筛选,只有符合自己要求的消息才会被接收,从而实现消费者对消息的过滤。
首先,我们需要更改生产者,这里我们改为使用MapMessage,并且对不通的消息设置不同的property。
// 步骤六:创建消息
MapMessage message1 = session.createMapMessage();
message1.setString("name","zhangsan");
message1.setInt("age",29);
message1.setIntProperty("age",29);
MapMessage message2 = session.createMapMessage();
message2.setString("name","lisi");
message2.setInt("age",39);
message2.setIntProperty("age",39);
// 步骤七:发送消息,第一个参数为消息,第二个参数为消息是否持久化,第三个参数是消息优先级,第四个参数为消息过期时间
producer.send(message1);
producer.send(message2);
System.out.println("HelloProducer消息发送完毕!");
// 步骤八:关闭连接
connection.close();
其中,setString和setInt都是设置消息的值,setIntProperty才是设置消息的property,消费者只能根据property对消息进行筛选,所以,倘若只是设置了setInt而不设置setIntProperty的话,消费者是没法筛选消息的。
对应的消费者代码如下:
private static final String SELECTOR_AGE_LT_30 = "age<30";
...
// 步骤四:创建队列
Destination destination = session.createQueue("firstQueue");
// 步骤五:创建消费者,并使用选择器对消息进行过滤
MessageConsumer consumer = session.createConsumer(destination, SELECTOR_AGE_LT_30);
// 步骤六:消费者监听消息
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
MapMessage currentMessage = (MapMessage) message;
try {
System.out.println("HelloConsumer收到的消息为:name-" + currentMessage.getString("name") + " age-" + currentMessage.getInt("age"));
} catch (JMSException e) {
e.printStackTrace();
}
}
});
正常启动后,会发现消费者端只接受到了age为29的消息,并没有接收到age为39的消息。
十二、消息消费的顺序性
有的业务场景下,我们需要保证MQ中的消息严格按照发送时的顺序被消费,倘若此时有多个消费者,那么消息的顺序消费就无法得到保证。目前,有如下几种方式可以实现消息的顺序消费。
方式一:使用选择器实现的独占消费
我们可以在生产者端设定某一种消息的属性,然后在消费者端选定一个消费者,使得只有它可以接收这种属性的消息,如此就可以实现该属性的消息顺序被一个消费者消费的目的。
缺点是消费者宕机后,消息无法被其它消费者消费。
方式二:使用exclusive的独占消费
这种模式就是保证无论有几个消费者,只有一个消费者会接收到消息,从而保证了消息的顺序消费。
// 步骤四:创建队列-设置独占消费队列
Destination destination = session.createQueue("firstQueue?consumer.exclusive=true");
在确保有如上一个独占消费者后,我们可以再创建几个非独占消费者或者独占消费者,实验的结果显示,最终只会有一个独占消费者来顺序消费消息,其它消费者都不会消费消息。
如果当前的独占消费者宕机了,则会有另外一个独占消费者来保证消息的消费;如果所有的独占消费者宕机了,那么就无法再保证只有一个消费者消费消息,也就无法保证消息消费的顺序性了。
缺点是所有的消息都由一个消费者在消费,其它消费者全部空闲,过于浪费资源,而且必须确保至少有一个独占消费者在线。
方式三:使用MessageGroups的消息分组
消息分组的主要逻辑就是在发送消息的时候,将消息进行分组设置(即设置消息的属性),然后同一分组的消息在MQ首次选定某个消费者进行消费后,以后该分组的消息全部发给这个消费者。
生产者做如下的改造:
// 步骤四:创建队列
Destination destination = session.createQueue("firstQueue");
// 步骤五:创建生产者
MessageProducer producer = session.createProducer(destination);
// 步骤六:创建消息
for(int i=0; i< 10; i++){
TextMessage message = session.createTextMessage("这是第" + i + "条消息!");
// 设定消息的属性来进行消息分组
message.setStringProperty("JMSXGroupID","Group001");
// 步骤七:发送消息,第一个参数为消息,第二个参数为消息是否持久化,第三个参数是消息优先级,第四个参数为消息过期时间
producer.send(message);
}
消费者无需做改造,消费者绑定哪个分组的消息都是由MQ进行绑定的。
倘若后续再增加了“Group002”、“Group003”分组的消息,MQ会负载均衡地选定其它消费者来绑定这些分组。
如果某个已经绑定“Group001”消息分组的消费者宕机了,那么MQ会重新选择其它消费者对该消息分组进行绑定,仍然可以保证消息消费的顺序性。
倘若生产者发送的最后一个消息设置message.setIntProperty("JMSXGroupSeq", -1);
,则表示该分组被关闭,那么被绑定该分组的消费者就会被释放,后续再次发送该分组消息时,MQ会重新选择一个消费者进行绑定。
使用MessageGroups的消息分组方式弥补了方式一中,消费者宕机,消息无法被消费的问题;也解决了方式二中所有消息都只被一个消费者消费的问题,实现了一定程度上的负载均衡,同时不需要对消费者进行任何设置,绑定分组由MQ自己完成;也继承了方式二的优点,绑定的消费者宕机或者消费分组关闭后,会重新绑定新的消费者,不会造成消息无法被消费的问题。
十三、消费者的负载均衡
在默认情况下,MQ连接多个消费者时,会自动启动消费者的负载均衡。比如有两个消费者A和B,同时监听队列“QueueA”,那么生产者发送10条消息到队列“QueueA”的时候,消费者A和B会轮发得到各自的5条消息,如果消息有编号的话,消息消费情况为:
A:1,3,5,7,9
B:2,4,6,8,10
需要注意的是,此处只能保证消息消费的负载均衡,但不能保证消息消费的顺序性,比如消息3不一定就早于消息4被消费。
其次,我们还可以通过MessageGroups来实现负载均衡,这种负载均衡就不是消息数量上的负载均衡了,而是以消费分组为单位的负载均衡,可以参考上述十二小节的内容。
十四、消费者的异步消费
在上面的例子中,我们的每个消费者都是一个线程,它在消息的监听、接收、处理、结束整个生命周期中都被占用着,所以称之为消费者的同步消费。
倘若我们为每个消费者构建一个线程池,消费者只负责监听并接收消息,消息的处理交给线程中的线程去处理,如此,单个消费者的吞吐量就能得到提升,如此称之为消费者的异步消费。
十五、与Spring Boot整合
这里只是给出一个最简单的整合实例,复杂的整合与设置不属于本文范畴。
step1:引入maven依赖
org.springframework.boot
spring-boot-starter-activemq
org.apache.activemq
activemq-pool
5.15.0
其中,消息队列连接池不是必须的,有点类似数据库连接池,可以减少创建连接的开销。
step2:开启JMS
在启动类上开启JMS:
@SpringBootApplication
@EnableJms
step3:增加配置文件
在application.properties中增加如下的配置项:
spring.activemq.broker-url=tcp://127.0.0.1:61616
#账密
spring.activemq.user=zhangsan
spring.activemq.password=123
#true 表示使用内置的MQ,false则连接服务器
spring.activemq.in-memory=false
#true表示使用连接池;false时,每发送一条数据创建一个连接
spring.activemq.pool.enabled=true
#连接池最大连接数
spring.activemq.pool.max-connections=10
#空闲的连接过期时间,默认为30秒
spring.activemq.pool.idle-timeout=30000
#强制的连接过期时间,与idleTimeout的区别在于:idleTimeout是在连接空闲一段时间失效,而expiryTimeout不管当前连接的情况,只要达到指定时间就失效。默认为0,never
spring.activemq.pool.expiry-timeout=0
step4:编写生产者代码
@Slf4j
@Component
@EnableScheduling
public class ProducerServiceImpl {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Scheduled(fixedDelay = 2000)
public void sendMessage() {
jmsMessagingTemplate.convertAndSend("secondQueue","这是activemq发送的消息!");
log.info("发送完毕!");
}
}
step5:编写消费者代码
@Component
@Slf4j
public class ConsumerServiceImpl {
@JmsListener(destination = "secondQueue")
public void receiveMessage(String message){
log.info("消费者接收到的消息为:{}", message);
}
}
如此,启动springboot应用后,就可以正常收发消息了。
参考资料
- ActiveMQ 消息持久化到数据库(Mysql、SQL Server、Oracle、DB2等
- Using Apache ActiveMQ
- JMS规范定义了2种消息传输模式:持久传送模式和非持久传输模式
- activemq 关于死信队列的配置应用
- ActiveMQ(22):Consumer高级特性之消息分组(Message Groups)