activeMQ
1. JMS与消息中间件
1.1 jms介绍
jms是java消息服务接口规范,主要包含四大元素:生产者、消费者、消息、消息服务。
- 生产者:创建消息,并把消息发动到消息服务;
- 消费者:从消息服务接收消息;
- 消息服务:即MQ消息服务(broker),而生产者与消费者相对其均为客服端;
- 消息:整个消息服务的传输对象,消息包含消息头、消息属性、消息体;
常用消息头属性:JMSDestination(消息目的地,如果生产者指定了目的地,在发送时会改为生产者绑定的目的地)、JMSDeliveryMode(是持久还是非持久)、JMSExpiration(过期时间,默认永久)、JMSPriority(优先级,0-9,数值越大优先级越高,默认为4)、JMSMessageId(唯一的消息ID);
消息属性:可视为消息头属性的扩展,通过setXxxProperty(k,v)设置;
消息体:封装消息的具体数据,发送与接收的消息体类型必须一致,消息体类型总共有5种,TextMessage、Mapmessage、BytesMessage、StreamMessage、ObjectMessage;
1.2 jms消息传递模式
jms消息传递模式有如下两种,
点对点消息传递模式(P2P):消息发送到一个特殊队列(queue), 消费者从队列获取消息,一条消息只能被只能被一个消费者消费;
发布/订阅消息传递模式(publish-subscribe):消息被发送到一个主题上(topic),所有订阅了该主题的消费者,都能接收到消息。
1.3 jms编码总体架构
JMS应用程序由如下基本模块组成,
- 连接工厂对象,创建消息客户端(生产者、消费者)与消息服务端的连接(connection);
- 连接对象,创建回话对象(session);
- 会话对象,创建生产者对象(producer)、消费者对象(consumer)以及消息对象(message);
- 目的地(queue/topic),点对点模式下目的地是队列(queue),发布/订阅模式下目的地是主题(topic),生产者把消息发送到目的地,消费者从目的地接收消息
1.4 消息中间件
消息中间件是实现了jms规范的落地产品,目前市场上主流的消息中间件有 ActiveMQ、Kafka、RocketMQ、RabbitMQ等。企业开发中使用消息中间件的主要目的是解决耦合调用、抵御洪峰流量(削峰)等。 以下主要讲解ActiveMQ的使用。
2. ActiveMQ安装并启动
具体安装步骤这里不再详述,可参考官网http://activemq.apache.org。安装成功后,进入安装目录,在bin目录下执行 ./activemq start
命令,即可启动MQ服务,如果启动服务需要指定配置文件,命令为 ./activemq start xbean:file:../conf/myConfig.xml
,不指定默认为conf目录下的activemq.xml。停止MQ服务的命令为 ./activemq stop
。
在conf目录下找到activemq.xml
配置文件打开,里面包含如下内容,
<transportConnectors>
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
这里配置的是MQ服务的各种传输协议连接和默认端口。再往下会发现这行内容<import resource="jetty.xml"/>
,activemq.xml文件中导入了一个名为jetty.xml
的配置文件,在conf目录下找到jetty.xml文件打开,里面配置了访问MQ服务web控制台的一些信息,
<bean id="jettyPort" class="org.apache.activemq.web.WebConsolePort" init-method="start">
<!-- the default port number for the web console -->
<property name="host" value="0.0.0.0"/>
<property name="port" value="8161"/>
</bean>
其中8161为web控制台端口,MQ服务启动后,浏览器中访问http://localhost:8161/admin,输入用户名和密码,默认都为admin,即可看到如下页面,
2.1 docker安装ActiveMQ
1.拉取镜像:docker pull webcenter/activemq
2.查看镜像:docker images
3.创建挂载目录:
mkdir /usr/soft/activemq
mkdir /usr/soft/activemq/log
4.运行activeMQ镜像:
docker run --name='activemq' \
-itd \
-p 8161:8161 \
-p 61616:61616 \
-e ACTIVEMQ_ADMIN_LOGIN=admin \
-e ACTIVEMQ_ADMIN_PASSWORD=123456 \
--restart=always \
-v /usr/soft/activemq:/data/activemq \
-v /usr/soft/activemq/log:/var/log/activemq \
webcenter/activemq:latest
61616是 activemq 的容器使用端口
8161是 web 页面管理端口
/usr/soft/activemq 是将activeMQ运行文件挂载到该目录
/usr/soft/activemq/log是将activeMQ运行日志挂载到该目录
-e ACTIVEMQ_ADMIN_LOGIN=admin 指定登录名
-e ACTIVEMQ_ADMIN_PASSWORD=123456 登录密码
5.浏览器访问IP:8161,即可看到欢迎页,点击登录,输入账号密码,可进入activeMQ后台。
3. 编码实战
ActiveMQ服务启动成功后,可以编写生产者客户端往MQ服务发送消息,消费者客户端从MQ服务获取消息。项目建好之后需要先引入ActiveMQ相关依赖,以gradle为例:
compile group: 'org.apache.activemq', name: 'activemq-all', version: '5.15.9'
3.1 点对点消息
3.1.1 生产者
package com.taicw.code.activemq.start.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueProducer {
private static final String BROKER_URL = "tcp://localhost:61616";
private static final String QUEUE_NAME = "queue001";
public static void main(String[] args) throws JMSException, InterruptedException {
//1、创建连接工厂。这里传入ActiveMQ消息服务连接地址,并使用默认用户名和密码。
// 也可使用ActiveMQConnectionFactory()构造器或者ActiveMQConnectionFactory(String userName, String password, String brokerURL)构造器,连接接信息全部使用默认值或者全部指定
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(BROKER_URL);
//2、通过工厂对象创建连接
Connection connection = connectionFactory.createConnection();
//3、通过连接对象创建会话。第一个参数是否开启事务,第二参数指定签收类型
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4、通过会话对象创建目的地(队列或者主题)。这里创建了一个名为 "queue001" 的队列
Queue queue = session.createQueue(QUEUE_NAME);
//5、通过会话对象创建生产者,并指定目的地
MessageProducer producer = session.createProducer(queue);
//6、连续创建3条消息,并有生产者发送到消息队列
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("队列消息:message" + i);
producer.send(textMessage);
System.out.println(textMessage.getJMSDestination());
System.out.println("发送消息" + textMessage.getText() + "成功");
}
//7、关闭资源
producer.close();
session.close();
connection.close();
}
}
依赖:
<dependencies>
<!--JMS规范的jar依赖-->
<dependency>
<groupId>javax.jms</groupId>
<artifactId>javax.jms-api</artifactId>
<version>2.0.1</version>
</dependency>
<!--activeMQ对jms具体实现的jar依赖-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
<version>5.15.4</version>
</dependency>
</dependencies>
执行main()方法后,进入web控制台可以看到待消费消息有3条,入队消息有3条,说明消息已经成功发送至MQ服务器。
查看页面发现此时如果存在队列名称为queque01有3条未读消息,则表示消息成功发送到了ActiveMQ。
队列表头说明:
表头名称 描述
Name 队列名称。
Number Of Pending Messages 等待消费的消息,这个是未出队列的数量(类似未读消息数量),公式:未出队列的数量=总入队数-总出队数。
Number Of Consumers 消费者数量,消费者端的消费者数量。
Messages Enqueued 进队消息数,进队列的总消息量,包括出队列的。这个数只增不减。
Messages Dequeued 出队消息数,可以理解为是消费者消费掉的数量。
由类的结构图可知,队列(Queue)和主题(Topic)拥有共同的父接口(Destination)
3.1.2 消费者
package com.taicw.code.activemq.start.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueConsumer {
private static final String BROKER_URL = "tcp://localhost:61616";
private static final String QUEUE_NAME = "queue001";
public static void main(String[] args) throws JMSException {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(BROKER_URL);
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer consumer = session.createConsumer(queue);
//在调用receive()方法之前必须要调用start()方法启动连接,否者receive()接收不到消息会被一直阻塞
connection.start();
Message message = consumer.receive();
while (message != null) {
String text = ((TextMessage) message).getText();
System.out.println("接收queue消息:" + text);
message = consumer.receive();
}
session.close();
consumer.close();
connection.close();
}
}
消费者客户端编码过程与生产者基本一致,只不过一个是生产者发送调用send()
方法,一个是消费者接收调用receive()
方法。其中需要注意的是receive()
方法是一个阻塞方法,接收不到消息会一直阻塞等待,并且调用receive()
之前必须调用connection.start()
启动连接,否者接收不到消息。
执行main()方法后,进入web控制台可以看到待消费消息变为0条,出队消息变为3条,并且有一个消费者,说明消息被消费成功。
未出队数量(未读):0
消费者数量:1 (Java程序仍然连接着ActiveMQ)
累计入队数量:3
累计出队数量:3
3.1.3 消息监听器实现异步非阻塞消费消息
上面我们了解到MessageConsumer#receive()
方法是个阻塞方法,实际开发中不可能一直去阻塞等待,可以为消费者对象设置消息监听器来实现异步非阻塞消费消息,修改消费者代码如下:
package com.taicw.code.activemq.start.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueConsumer {
private static final String BROKER_URL = "tcp://localhost:61616";
private static final String QUEUE_NAME = "queue001";
public static void main(String[] args) throws JMSException, InterruptedException {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(BROKER_URL);
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer consumer = session.createConsumer(queue);
//在调用receive()方法之前必须要调用start()方法启动连接,否者receive()接收不到消息会被一直阻塞
// connection.start();
// Message message = consumer.receive();
// while (message != null) {
// String text = ((TextMessage) message).getText();
// System.out.println("接收queue消息:" + text);
// message = consumer.receive();
// }
connection.start();
consumer.setMessageListener(message -> {
try {
String text = ((TextMessage) message).getText();
System.out.println("接收queue消息:" + text);
} catch (JMSException e) {
e.printStackTrace();
}
});
//sleep为了使程序不退出
Thread.sleep(10000000000L);
session.close();
consumer.close();
connection.close();
}
}
setMessageListener()
方法需要传入一个MessageListener
实例对象,并实现onMessage()
,这里使用的是lambda表达式。
3.2 发布/订阅消息
主题特点
生产者将消息发布到topic中,每个消息可以有多个消费者,属于1:N的关系;
生产者和消费者之间有时间上的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息。
生产者生产时,topic不保存消息它是无状态的不落地,假如无人订阅就去生产,那就是一条废消息,所以,一般先启动消费者再启动生产者。
JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。一句话,好比我们的微信公众号订阅
发布订阅消息与上面点对点消息的生产者与消费者编码一致,唯一要改变的是把消息目的地由queue改为topic。
- 生产者
package com.huazai.activemq.demo;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JMSPublisher {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 主题名称
public static final String TOPIC_NAME = "topic-test";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Topic topic = session.createTopic(TOPIC_NAME);
// 可以用父接口Destination接受
// Destination topic = session.createQueue(TOPIC_NAME);
// 5.创建消息的生产者
MessageProducer producer = session.createProducer(topic);
// 6.通过消息生产者生产6条消息发送MQ队列
for (int i = 0; i < 3; i++) {
// 7.创建消息
TextMessage textMessage = session.createTextMessage("msg" + i + ":hello world");
// 8.将消息发送到MQ
producer.send(textMessage);
}
// 9.关闭资源
producer.close();
session.close();
connection.close();
System.out.println("finish");
}
}
点击运行,无异常打印,并且控制台成功打印finish,则表示程序运行成功。
查看页面发现此时如果存在主题名称为topic-test有3条入队消息,则表示消息成功发送到了ActiveMQ。
队列表头说明:
表头名称 描述
Name 主题名称
Number Of Consumers 消费者数量,消费者端的消费者数量。
Messages Enqueued 进队消息数,进队列的总消息量,包括出队列的。这个数只增不减。
Messages Dequeued 出队消息数,可以理解为是消费者消费掉的数量。
- 消费者
同步阻塞
package com.huazai.activemq.demo;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JMSSubscriber {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 主题名称,取消息必须和存消息的主题名称一致
public static final String TOPIC_NAME = "topic-test";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Topic topic = session.createTopic(TOPIC_NAME);
// 5.创建消费者
MessageConsumer consumer = session.createConsumer(topic);
while (true) {
// 接受消息根据生产者发送消息类型强类型转换
TextMessage message = (TextMessage) consumer.receive();
if (message != null) {
String text = message.getText();
System.out.println(text);
} else {
break;
}
}
consumer.close();
session.close();
connection.close();
}
}
消费者消费消息之消息监听器(异步非阻塞)
package com.huazai.activemq.demo;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JMSSubscriberForListener {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 主题名称,取消息必须和存消息的主题名称一致
public static final String TOPIC_NAME = "topic-test";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Topic topic = session.createTopic(TOPIC_NAME);
// 5.创建消费者
MessageConsumer consumer = session.createConsumer(topic);
/*
异步非阻塞式方式监听器(onMessage)
订阅者或消费者通过创建的消费者对象,给消费者注册消息监听器setMessageListener,
当消息有消息的时候,系统会自动调用MessageListener类的onMessage方法
我们只需要在onMessage方法内判断消息类型即可获取消息
*/
consumer.setMessageListener(message -> {
if (message != null && message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("监听器接受到的消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
/*
由于是异步接受消息,会发生监听器没监听到消息之前程序就已经运行完毕,
所以通过此行代码阻塞程序等到监听器监听并回调,可在控制台输入任意字符并回车结束程序运行
*/
System.in.read();
consumer.close();
session.close();
connection.close();
}
}
由Topic的特性可知,先订阅然后再生产消息,否则生产的消息就是“垃圾消息”。
启动JMSSubscriber
和JMSSubscriberForListener
订阅消息,再启动JMSPublisher
发布消息。
当JMSPublisher
消息发布完后,JMSSubscriber
和JMSSubscriberForListener
都同时接受到了相同的消息。
此时,监控信息情况如下:
消费者:2
消息入队:3
消息出队:6(消费者*消息入队)
比较项目 | Topic模式 | Queue模式 |
工作模式 | "订阅-发布"模式,如果当前没有订阅者,消息将会被丢弃,如果有多个订阅者,那么这些订阅者都会收到消息 | "负载均衡"模式,如果当前没有消费者,消息也不会丢弃;如果有多个消费者,那么一条消息也只会发送给其中一个消费者,并且要求消费者ack信息 |
有无状态 | 无状态 | Queue数据默认会在mq服务器上已文件形式保存,比如Active MQ一般保存在$AMQ_HOME\data\kr-store\data下面,也可以配置成DB存储 |
传递完整性 | 如果没有订阅者,消息会被丢弃 | 消息不会被丢弃 |
处理效率 | 由于消息要按照订阅者的数量进行复制,所以处理性能会随着订阅者的增加而明显降低,并且还要结合不同消息协议自身的性能差异 | 由于一条消息只发送给一个消费者,所以就算消费者再多,性能也不会有明显降低。当然不同消息协议的具体性能也是有差异的 |
1.在点对点消息示例中,当同时启动多个消费者时(即同时执行多次main()方法),生产者发布的每条消息只能被其中一个消费者消费一次;
2.在发布/订阅消息示例中,消费者不能消费订阅主题之前的消息,当同时启动多个消费者时,生产者发布的每条消息可以同时被多个消费者消费;
4.ActiveMQ的Broker
1.是什么?
相当于一个内嵌式ActiveMQ服务器实例。其实就是实现了用代码的形式启动ActiveMQ将MQ嵌入到Java代码中,以便随时用随时启动,在用的时候再去启动这样能节省了资源,也保证了可用性。
用ActiveMQ Broker作为独立的消息服务器来构建Java应用。ActiveMQ也支持在vm中通信基于嵌入的broker,能够无缝的集成其他java应用。
换言之,类似SpringBoot内嵌了一个Tomcat服务器。
2.怎么用?
<!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-all -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.xbean/xbean-spring -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>4.15</version>
</dependency>
<!--这是启动broker服务需要依赖的jar包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
package com.huazai.activemq.broker;
import org.apache.activemq.broker.BrokerService;
public class EmbedBroker {
public static void main(String[] args) throws Exception {
// 创建broker服务实例
BrokerService brokerService = new BrokerService();
brokerService.setPopulateJMSXUserID(true);
// 绑定地址,此处不是再使用linux服务器上的ip地址
brokerService.addConnector("tcp://127.0.0.1:61616");
// 启动服务
brokerService.start();
}
}
启动main函数,再命令行上输入jps -l
出现你编写的类名,则表示broker服务启动成功。
3.测试
在连接linux服务器上ActiveMQ的基础上,把连接到linux服务器的ip地址改成本机地址即可。
消息提供者代码如下:
package com.huazai.activemq.broker;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JMSProducer {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
// 消息队列名称
public static final String QUEUE_NAME = "broker-queue";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Queue queue = session.createQueue(QUEUE_NAME);
// 可以用父接口Destination接受
// Destination queue = session.createQueue(QUEUE_NAME);
// 5.创建消息的生产者
MessageProducer producer = session.createProducer(queue);
// 6.通过消息生产者生产6条消息发送MQ队列
for (int i = 0; i < 3; i++) {
// 7.创建消息
TextMessage textMessage = session.createTextMessage("msg" + i + ":hello world");
// 8.将消息发送到MQ
producer.send(textMessage);
}
// 9.关闭资源
producer.close();
session.close();
connection.close();
System.out.println("finish");
}
}
消息消费者代码如下:
package com.huazai.activemq.broker;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JMSConsumer {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
// 消息队列名称,取消息必须和存消息的队列名称一致
public static final String QUEUE_NAME = "broker-queue";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Queue queue = session.createQueue(QUEUE_NAME);
// 5.创建消费者
MessageConsumer consumer = session.createConsumer(queue);
while (true) {
// 接受消息根据生产者发送消息类型强类型转换
TextMessage message = (TextMessage) consumer.receive();
if (message != null) {
String text = message.getText();
System.out.println(text);
message.acknowledge();
// session.commit();
} else {
break;
}
}
consumer.close();
session.close();
connection.close();
}
}
先启动消息提供者,把消息发送到broker,再启动消息消费者,消费者能从队列中成功获取消息,则表示测试成功。
5.Spring整合ActiveMQ
5.1依赖
<!--spring核心依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<!--activemq核心依赖-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.11</version>
</dependency>
<!--activemq连接池依赖-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.15.10</version>
</dependency>
5.2applicationContext.xml 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--开启包的自动扫描-->
<context:component-scan base-package="com.huazai.activemq.spring"></context:component-scan>
<!--配置连接对象-->
<bean id="connectionFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
<property name="connectionFactory">
<!--真正可以生产Connection的ConnectionFactory,由对应的JMS服务商提供-->
<bean class="org.apache.activemq.spring.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.64.129:61616"/>
</bean>
</property>
<!--配置最大连接数-->
<property name="maxConnections" value="100"/>
</bean>
<!--这个是队列目的地,点对点的Queue-->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
<!--通过构造注入队列名称-->
<constructor-arg index="0" value="spring-active-queue"/>
</bean>
<!--这个是队列目的地,发布订阅的主题Topic-->
<bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
<!--通过构造器注入主题名称-->
<constructor-arg index="0" value="spring-active-topic"/>
</bean>
<!--Spring提供的JMS工具类,他可以进行消息发送,接收等,类似JDBCTemplate-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<!--传入连接工厂-->
<property name="connectionFactory" ref="connectionFactory"/>
<!--传入默认目的地,可以是queue,也可以是topic-->
<property name="defaultDestination" ref="destinationTopic"/>
<!--消息自动转换器-->
<property name="messageConverter">
<bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
</property>
</bean>
<!-- 配置Jms消息监听器 -->
<bean id="defaultMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<!-- Jms连接的工厂 -->
<property name="connectionFactory" ref="connectionFactory"/>
<!-- 设置默认的监听目的地 -->
<property name="destination" ref="destinationTopic"/>
<!--传入默认目的地,可以是queue,也可以是topic-->
<property name="messageListener" ref="myMessageListener"/>
</bean>
</beans>
5.3队列消费者代码:
package com.huazai.activemq.spring.queue;
import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
@Service
public class SpringMQConsumer {
@Autowired
private JmsTemplate jmsTemplate;
@Autowired
public void setJmsTemplate(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQConsumer springMQConsumer = applicationContext.getBean(SpringMQConsumer.class);
while(true) {
// 接受消息
String returnValue = (String) springMQConsumer.jmsTemplate.receiveAndConvert();
// 输入exit-退出
if ("exit-".equals(returnValue)) {
System.exit(-1);
}
System.out.println("****消费者收到的消息: " + returnValue);
}
}
}
5.4 队列生产者代码:
package com.huazai.activemq.spring.queue;
import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
import java.util.Scanner;
@Service
public class SpringMQProducer {
static final Scanner input = new Scanner(System.in);
@Autowired
private final JmsTemplate jmsTemplate;
@Autowired
public SpringMQProducer(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQProducer springMQ_producer = applicationContext.getBean(SpringMQProducer.class);
JmsTemplate jmsTemplate = springMQ_producer.jmsTemplate;
// 发送消息
jmsTemplate.send(session -> session.createTextMessage("***Spring和ActiveMQ的整合case....."));
System.out.println("********send task over");
while(true) {
System.out.println("\n请输入要发送的内容");
final String msgText = input.next();
// 输入exit推出程序
if ("exit".equals(msgText)) {
System.exit(-1);
}
if (msgText.startsWith("auto:")) {
int amount;
try {
amount = Integer.parseInt(msgText.substring(5));
} catch (NumberFormatException e) {
System.out.println("auto指令格式错误,正确的应该是:auto:number");
continue;
}
System.out.println("请稍等...");
for (int i = 1; i <= amount; i++) {
final int ii = i;
jmsTemplate.send(session -> session.createTextMessage(ii + ""));
}
System.out.println(msgText + "\t->|\t指令执行完成");
} else {
jmsTemplate.send(session -> session.createTextMessage(msgText));
System.out.println(msgText + "\t->|\t发送完成");
}
}
}
}
启动消费者和订阅者,结果如下:
5.5订阅者代码
package com.huazai.activemq.spring.topic;
import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
import javax.jms.Destination;
@Service
public class SpringMQTopicConsumer {
private final JmsTemplate jmsTemplate;
public SpringMQTopicConsumer(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQTopicConsumer springMQConsumer = applicationContext.getBean(SpringMQTopicConsumer.class);
JmsTemplate jmsTemplate = springMQConsumer.jmsTemplate;
// applicationContext.xml的默认目的地配置为队列,所以此处需要用代码修改默认目的地
jmsTemplate.setDefaultDestination(((Destination) applicationContext.getBean("destinationTopic")));
// jmsTemplate.setDestinationResolver((session, s, b) -> session.createTopic("spring-test-topic"));
while (true) {
String returnValue = (String) jmsTemplate .receiveAndConvert();
if ("exit-".equals(returnValue)) {
System.exit( -1);
}
System.out.println("****消费者收到的消息: " + returnValue);
}
}
}
5.6发布者代码
package com.huazai.activemq.spring.topic;
import org.apache.xbean.spring.context.ClassPathXmlApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
import javax.jms.Destination;
import java.util.Scanner;
@Service
public class SpringMQTopicProducer {
static final Scanner input = new Scanner(System.in);
private final JmsTemplate jmsTemplate;
public SpringMQTopicProducer(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public static void main(String[] args) throws InterruptedException {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQTopicProducer springMQ_topic_producer = applicationContext.getBean(SpringMQTopicProducer.class);
//直接调用application.xml里面创建的destinationTopic这个bean设置为目的地就行了
JmsTemplate jmsTemplate = springMQ_topic_producer.jmsTemplate;
// applicationContext.xml的默认目的地配置为队列,所以此处需要用代码修改默认目的地
jmsTemplate.setDefaultDestination(((Destination) applicationContext.getBean("destinationTopic")));
// jmsTemplate.setDestinationResolver((session, s, b) -> session.createTopic("spring-test-topic"));
jmsTemplate.send(session -> session.createTextMessage("***Spring和ActiveMQ的整合TopicCase111....."));
System.out.println("********send task over");
while(true) {
System.out.println("\n请输入要发送的内容");
final String msgText = input.next();
if ("exit".equals(msgText)) {
System.exit(-1);
} else {
jmsTemplate.send(session -> session.createTextMessage(msgText));
System.out.println(msgText + "\t->|\t发送完成");
}
}
}
}
先启动订阅者,再启动发布者,结果如下:
5.7监听器实现消费者不启动直接消费消息
applicationContext.xml 监听器配置:
<!-- 配置Jms消息监听器 -->
<bean id="defaultMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<!-- Jms连接的工厂 -->
<property name="connectionFactory" ref="connectionFactory"/>
<!-- 设置默认的监听目的地 -->
<property name="destination" ref="destinationTopic"/>
<!--传入默认目的地,可以是queue,也可以是topic-->
<property name="messageListener" ref="myMessageListener"/>
</bean>
5.8 自定义监听器:
package com.huazai.activemq.spring.listener;
import org.springframework.stereotype.Component;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
@Component
public class MyMessageListener implements MessageListener {
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者收到的消息" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
增加了监听器之后,在原来代码基础上
-
只启动队列生产者
-
只启动主题订阅者
以上两个案例实现了不启动消费者通过监听器实现queue和topic消息消费。
6.SpringBoot整合ActiveMQ
6.1依赖
<!--activemq启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!--boot启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--测试启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
6.2application.yml配置:
server:
port: 7777
spring:
activemq:
#你的activemq连接地址
broker-url: tcp://192.168.64.129:61616
#账号
user: admin
#密码
password: admin
jms:
#指定连接的是队列(Queue)还是主题(Topic),false代表队列,true代表主题
pub-sub-domain: false
queue:
name: boot-queue-test
topic:
name: boot-topic-test
6.3 ActiveMQ配置类:
package com.huazai.activemq.springboot.config;
import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.jms.Queue;
@Configuration
public class ActiveConfig {
@Value("${queue.name}")
private String queueName;
@Value("${topic.name}")
private String topicName;
@Bean
public Queue activeQueue() {
return new ActiveMQQueue(queueName);
}
@Bean
public Topic activeTopic() {
return new ActiveMQTopic(topicName);
}
}
6.4 启动类:
package com.huazai.activemq.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.annotation.EnableJms;
@SpringBootApplication
// 开启JMS服务
@EnableJms
public class BootQueueProviderMain {
public static void main(String[] args) {
SpringApplication.run(BootQueueProviderMain.class, args);
}
}
6.5 队列生产者代码
- 消息生产者服务:
package com.huazai.activemq.springboot.queue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
import javax.jms.JMSException;
import javax.jms.Queue;
import java.util.UUID;
@Service
public class QueueProviderService {
/**
* 相当于 {@link JmsTemplate}
*/
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Queue queue;
/**
* 生产消息
*
* @throws JMSException
*/
@JmsListener(destination = "${queue.name}")
public void productMessage() throws JMSException {
// 生产者生产并发送消息,此方法是send方法的加强版
jmsMessagingTemplate.convertAndSend(queue, "消费者发送消息:" + UUID.randomUUID());
}
}
- 队列消息发送测试类:
import com.huazai.activemq.springboot.BootQueueProviderMain;
import com.huazai.activemq.springboot.queue.QueueProviderService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.jms.JMSException;
@SpringBootTest(classes = BootQueueProviderMain.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class QueueProviderTest {
@Autowired
private QueueProviderService queueProviderService;
@Test
public void testSend() throws JMSException {
queueProviderService.productMessage();
}
}
启动测试类,将消息发送到名称为boot-queue-test
队列,结果如下:
6.6 队列消费者代码
- 消息监听消费服务:
package com.huazai.activemq.springboot.queue;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;
import javax.jms.JMSException;
import javax.jms.TextMessage;
@Service
public class QueueConsumerService {
/**
* 监听接收的方法,监听的目的地名称为${queue.name}配置
*/
@JmsListener(destination = "${queue.name}")
public void receive(TextMessage textMessage) throws JMSException {
String text = textMessage.getText();
System.out.println("***消费者收到的消息: " + text);
}
}
启动消费者服务,接受到了之前生产者生产的消息,测试结果如下:
6.7主题代码
- 主题订阅者服务
package com.huazai.activemq.springboot.topic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.jms.Topic;
import java.util.UUID;
@Service
public class TopicProviderService {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Topic topic;
/**
* 每隔3秒定时发布主题消息
*/
@Scheduled(fixedDelay = 3000)
public void productTopic() {
jmsMessagingTemplate.convertAndSend(topic, "发布者发布主题消息:" + UUID.randomUUID());
}
}
- 主题发布者服务
package com.huazai.activemq.springboot.topic;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;
import javax.jms.JMSException;
import javax.jms.TextMessage;
@Service
public class TopicConsumerService {
/**
* 开启监听器监听主题消息
*
* @param textMessage
* @throws JMSException
*/
@JmsListener(destination = "${topic.name}")
public void receive(TextMessage textMessage) throws JMSException {
String text = textMessage.getText();
System.out.println("订阅者订阅到的消息:" + text);
}
}
先启动主题订阅者,再启动主题发布者,主题订阅者会间隔3秒接收到主题发布者的消息,结果如下:
7. 传输协议
7.1 ActiveMQ默认支持的传输协议
ActiveMQ出厂默认支持的传输协议有tcp
、amqp
、stomp
、mqtt
、ws
TCP(Transmission Control Protocol)
TCP是Broker默认配置的协议,默认监听端口是61616。
在网络传输数据前,必须要先序列化数据,消息是通过一个叫wire protocol的来序列化成字节流。默认情况下,ActiveMQ把wire protocol叫做OpenWire,它的目的是促使网络上的效率和数据快速交互。
TCP连接的URI形式如:tcp://HostName:port?key=value&key=value,后面的参数是可选的。
TCP传输的的优点:
TCP协议传输可靠性高,稳定性强
高效率:字节流方式传递,效率很高
有效性、可用性:应用广泛,支持任何平台
NIO(New I/O API Protocol)
NIO协议和TCP协议类似,但NIO更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的client调用和服务器端有更多的负载。
一般情况下,大量的Client去连接Broker是被操作系统的线程所限制的。因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议。
NIO连接的URI形式:nio://hostname:port?key=value&key=value
AMQP(Advanced Message Queuing Protocol)
一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件限制。
STOMP(Streaming Text Orientation Message Protocol)
STOMP是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息中间件)设计的简单文本协议。
SSL(Secure Sockets Layer Protocol)
SSL传输允许客户端使用TCP上的SSL连接到远程ActiveMQ代理。SSL传输允许客户端使用TCP套接字上的SSL连接到远程ActiveMQ代理。
连接的URL形式: ssl://hostname:port?key=value
MQTT(Message Queuing Telemetry Transport)
MQTT,即消息队列遥测传输,是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当作传感器和致动器(比如通过Twitter让房屋联网)的通信协议。
VM
VM本身不是协议。VM传输允许客户机在VM内部彼此连接,而不需要网络通信的开销。所使用的连接不是套接字连接,而是使用直接方法调用来启用高性能嵌入式消息传递系统。
第一个使用VM连接的客户机将引导一个嵌入式代理。后续的连接将连接到同一代理。一旦所有到代理的VM连接都关闭了,嵌入式代理将自动关闭。
在activemq.xml
配置文件可以找到这几种协议的配置,
<transportConnectors>
<!-- DOS protection, limit concurrent connections to 1000 and frame size to 100MB -->
<transportConnector name="openwire" uri="tcp://0.0.0.0:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="amqp" uri="amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="stomp" uri="stomp://0.0.0.0:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="mqtt" uri="mqtt://0.0.0.0:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
<transportConnector name="ws" uri="ws://0.0.0.0:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600"/>
</transportConnectors>
对于java开发后四种协议不经常使用,这里主要说一次tcp协议。tcp协议的client监听端口默认是61616,在网络上传输数据,必须序列化数据,消息是通过一个write protocol来序列化为字节流。默认情况 ActiveMQ会把wire protocol叫做Open Wire,它的目的是促使网络上的效率和数据快速交互。
tcp传输的优点:
- 传输可靠性高、稳定性强
- 高效性:字节流方式传递,效率高
- 有效性、可用性:应用广泛,支持任何平台
tcp连接的URL形式如:tcp://hostname:port?key=value。
7.2 使用NIO传输协议提供更好的性能
使用tcp协议,每一个连接都会创建一个线程,当client连接较多时需要大量的系统开销,nio支持多个连接使用同一个线程,相比tcp需要更少的线程数。
nio协议基于tcp协议之上进行了扩展和优化。要使ActiveMQ支持nio协议,只需要做少量的修改即可。打开activemq.xml
配置文件,在<transportConnectors>
节点内添加,<transportConnector name="nio" uri="nio://0.0.0.0:61617"/>
,同时客户端代码url连接形式要改为 nio://hostname:port?key=value
,后面的可选参数与tcp协议一致。
tcp协议也好nio协议也好,都绑定了特定的端口,如何实现一个端口可以支持多种协议呢?ActiveMQ提供了一个auto协议,类似于一个适配器协议,在不改变端口的情况下可以切换协议。详细配置参考官方文档 http://activemq.apache.org/auto
其他协议配置参见官网文档 http://activemq.apache.org/co...
7.3 NIO协议配置
由ActiveMQ安装目录所在的/conf/activemq.xml的配置文件可知,ActiveMQ默认出厂配置并不是NIO网络模型,而是BIO网络模型,若想使用NIO网络模型,需要transportConnectors标签加入以下配置,端口可以自定义(如果不指定端口,默认使用BIO网络IO模型端口,比如OpenWire、STOMP、AMQP等):
<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />
配置完后,使用命令./activemq restart重启ActiveMQ,打开控制台的Connections看到Connector NIO一栏则表示配置成功。
NIO配置文件修改后,连接ActiveMQ的url由原来的tcp://your ip:61616
改成nio://your ip:61618
即可。
7.4 NIO使用增强
在我们没有配置NIO时,端口使用的是以TCP为协议基础的BIO + TCP
模式,当配置了NIO并且URI格式以“nio”开头后,此时端口使用的是以TCP协议为基础的NIO网络模型,即NIO + TCP
。
怎么能做到让这个端口既支持NIO网络模型,又让他支持多个协议呢(即不仅仅是TCP协议)?
- 配置auto
auto关键字:在NIO上启用自动检测功能(来自官方翻译)
在ActiveMQ安装目录所在的/conf/activemq.xml
的配置文件的transportConnectors
中加入以下一行配置(端口可自行指定)。
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:5678" />
配置完后,启动ActiveMQ,并在控制台访问,在Connections菜单出现Connector auto+nio一栏则表示配置成功。
2. 修改连接服务的URI
连接ActiveMQ服务的URI格式:[协议名称]://[your ip]:5678
由上一步可知,使用auto关键字配置了5678端口,当我们在5678端口使用指定的协议名称时,ActiveMQ会自动使用该协议的NIO网络不行,不再是原来的BIO网络模型,由此达到NIO网络模型支持多协议的作用。
比如当我们在5678使用TCP协议时,则URI应该配置为:tcp://your ip://5678,则此时5678使用的是以TCP协议为基础的NIO网络模型。
注:不同的网络协议使用不同的客户端代码,即jar不同,api也不同。
8 ActiveMQ消息高可用
8.1 消息持久化
8.1.1 持久化编码
如果生产者把消息发送到了MQ消息服务,消费者还没有来得及消费,此时MQ服务停止或意外宕机,那么这些未被消费的消息改怎么处理呢?分为消息非持久化和消息持久化两种情况,消息非持久化这些未被处理的消息直接丢失,消息持久化会把这些未被消费的消息暂时存储起来,当MQ消息服务重新启动时恢复这些消息,消费者可以继续消费。
- 队列消息持久化
基于上面的示例代码,只需要为生产者客户端代码添加一行通过MessageProducer对象设置就可以了。(队列消息默认开启持久化这一行实际上可以省略)
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
- 主题消息持久化
主题消息默认不持久化,支持主题消息持久化,只需要修改消费者客户端代码如下:
...
connection.setClientID("client_0001");
...
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber subscriber = session.createDurableSubscriber(topic, "remark...");
connection.start();
subscriber.setMessageListener(message -> {
...
});
首先必须要通过connection.setClientID("client_0001")
指定订阅者ID,因为如果不指定唯一ID,订阅者(非持久化订阅者)每次连接时都会随机创建一个ID,在消息持久化状态下,订阅者需要保证从离线到重新在线ClientID唯一不变,这样MQ消息服务才能确定主题消息是否被所有持久化订阅者消费了(如果MQ服务停止或宕机时,主题消息未被所有持久化订阅者消费的会被存储起来,已经被所有持久化订阅者消费的主题消息会直接丢弃)。
然后通过session.createDurableSubscriber(topic, "remark...")
创建一个TopicSubscriber对象,告诉MQ服务其订阅的此主题消息要做持久化处理。
8.1.2 持久化存储机制
ActiveMQ的消息持久化机制有JDBC、AMQ、KahaDB和LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的,就是在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等再试图将消息发送给接受者,成功则将消息从存储中删除,失败则继续尝试发送。MQ消息服务启动以后首先要检查指定的存储位置,如果有未发送成功的消息则需要把消息继续发送出去。下面分别介绍一下KahaDB与JDBC持久化机制。
- KahaDB存储
KahaDB是一个基于文件的持久性数据库,消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址。KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。在在ActiveMQ安装目录的conf/activemq.xml
文件配置了ActiveMQ的默认持久化方式。配置文件可查看其配置信息,更多的配置信息可参见官网 http://activemq.apache.org/ka...
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
directory
这里指明了kahadb数据存储路径,默认为ActiveMQ安装目录下/data/kahadb
,其中主要包含4类文件和一个lock:
-
- db-<number>.log:kahaDB存储消息到预定大小(默认32M)的数据记录文件中,文件命名为db-<number>.log,当数据文件已满时,一个新的文件会随之创建,number数值也会随之递增,当不再有引用到数据文件中的消息时,文件会被删除或者归档;
- db.data:改文件包含了持久化的BTree索引,它是消息的索引文件,使用BTree作为索引指向db-<nubmer>.log里面存储的消息;
- db.free:记录当前db.data文件里哪些页面是空闲的,文件具体内容是所有空闲页的ID;
- db.redo:用来进行消息恢复,如果KahaDB消息存储在强制退出后启动,用于恢复BTree索引;
- lock:文件锁,表示当前获得kahaDB读写权限的broker;
- JDBC存储
如果采用JDBC机制存储,需要准备一个第三方数据库,这里以MySql数据库为例,更多信息参考http://activemq.apache.org/jd...
1.首先将mysql数据库的驱动包mysql-connector-java-5.1.41.jar
添加到ActiveMQ安装目录/lib
目录下,用于连接mysql数据库;
2.打开conf/activemq.xml配置文件,中找到persistenceAdapter
标签替换有以下内容:
<persistenceAdapter>
<!--<kahaDB directory="${activemq.data}/kahadb"/> -->
<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="true" />
</persistenceAdapter>
dataSource属性值指定将要引用的持久化数据库的bean名称(后面会配置一个名为mysql-ds的bean);createTablesOnStartup属性值是否在启动的时候创建数据库表,默认是true,这样每次启动都会去创建表了,一般是第一次启动的时候设置为true,然后再去改成false。
3.数据库连接池配置
配置连接池之前先在数据库中建立一个数据库,比如我建立数据库名称为:activemq_test
<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<!-- 数据库驱动名称,此处是mysql驱动名称-->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<!-- 连接数据库url,ip换成自己数据库所在的机器ip,数据库名为新建立数据库的名称-->
<property name="url" value="jdbc:mysql://your ip:3306/your db's name?relaxAutoCommit=true&serverTimezone=GMT"/>
<!-- 连接数据库用户名-->
<property name="username" value="your username"/>
<!-- 连接数据库密码-->
<property name="password" value="your password"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
ActiveMQ默认使用DBCP连接池,并且自带了DBCP连接池相关jar包,如果想要换成C3P0等连接池,需要自行引入相关jar包。其中bean的id属性值一定要和上面的dataSource属性值一样。
4.启动ActiveMQ
运行命令./activemq start
启动服务,不出意外的话,启动成功后,将会在配置的数据库中生成相应的三张表。
表创建后记得修改activemq.xml的jdbcPersistenceAdapter标签的createTablesOnStartup属性值改为false。
ACTIVEMQ_MSGS:消息表,Queue和Topic都存在里面。
列名 类型 字段描述
ID bigint(20) 主键
CONTAINER varchar(250) 消息的Destination
MSGID_PROD varchar(250) 消息发送者的主键
MSGID_SEQ bigint(20) 发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID
EXPIRATION bigint(20) 消息的过期时间,存储的是从1970-01-01到现在的毫秒数,0表示用不过期
MSG blob(0) 消息本体的Java序列化对象的二进制数据
PRIORITY bigint(20) 优先级,从0-9,数值越大优先级越高,默认0
XID varchar(250) -
ACTIVEMQ_ACKS:订阅关系表,如果是持久化Topic,订阅者和服务器的订阅关系保存在这个表。
列名 类型 字段描述
CONTAINER varchar(250)) 消息的Destination
SUB_DEST varchar(250) 如果使用的是Static集群(静态集群),将保存集群其他系统的信息
CLIENT_ID varchar(250) 每个订阅者都必须有一个唯一的客户端ID用以区分
SUB_NAME varchar(250) 订阅者名称
SELECTOR varchar(250) 选择器,可以选择只消费满足条件的消息,条件可以自定义属性实现,可支持多属性AND和OR操作
LAST_ACKED_ID bigint(20) 记录消费过消息的ID
PRIORITY bigint(20) 优先级,从0-9,数值越大优先级越高,默认5
XID varchar(250) -
ACTIVEMQ_LOCK:在集群环境下才有用,只有一个Broker可以获取消息,称为Master Broker,其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master Broker。
列名 | 类型 | 字段描述 |
---|---|---|
ID | bigint(20) | 主键 |
TIME | bigint(20) | timestamp |
BROKER_NAME | varchar(250) | 当前的 master broker名称 |
(意外启动失败则看这里)ActiveMQ启动日志记录在安装目录的data/activemq.log
日志文件中,启动失败的常见原因拒绝连接数据库,因为mysql默认没有开启连接远程连接的权限,比如我启动失败的错误信息如下(启动错误请根据自身情况处理):
解决方案:开启允许远程连接mysql服务,执行脚本如下:
use mysql;
select host, user, authentication_string, plugin from user;
update user set host='%' where user='root';
flush privileges;
此方式是开放所有IP的root访问权限,如果是正式环境还是建议用 开放指定IP的方式。
5.java代码连接测试queue
代码参考:Java编码实现ActiveMQ通讯(Queue)
先启动队列生产者,由于ActiveMQ默认是开启持久化方式,启动成功后,表ACTIVEMQ_MSGS表数据如下:
再启动队列消费者,消息消费成功后,数据表的数据被清空了,结论如下:
1.当DeliveryMode设置为NON_PERSISTENCE时,消息被保存在内存中。
2.当DeliveryMode设置为PERSISTENCE时,消息保存在broker的相应的文件或者数据库中。
3.点对点类型中消息一旦被Consumer消费,就从数据中删除。
4.消费前的队列消息,会被存放到数据库。
如果开启非持久化方式,则消息不会持久化到数据表。
6.java代码连接测试topic
代码参考:Java编码实现ActiveMQ通讯(Topic)
先启动topic订阅者,再启动主题发布者,数据表数据如下:
一定先启动订阅者,表示该topic存在订阅者,否则发布的topic是不会持久化到数据库中,换句话说不存在订阅者的topic就是废消息,没必要持久化到JDBC中。
结论如下:
topic与queue不同,被消费的消息不会在数据表中删除。
7.jdbc持久化方式 + ActiveMQ Journal(日志)
jdbc持久化方式将消息持久化到数据库中虽好,但是JDBC每次消息过来,都需要去写库读库。引入
ActiveMQ Journal,使每次消息过来之后在ActiveMQ和JDBC之间加一层高速缓存,使用高速缓存写入技术,大大提高了性能。
如:当有消息过来后,消息不会立马持久化到数据库中,而是先保存到缓存中,被消费的消息也是先从缓存中读取,经过了指定的时间之后,才把缓存中的数据持久化到数据库中。如果是queue,则只持久化未被消费的消息。
用法:在activemq.xml文件中,将persistenceFactory替换掉persistenceAdapter内容
<!-- <persistenceAdapter> -->
<!--<kahaDB directory="${activemq.data}/kahadb"/> -->
<!--<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="true" /> -->
<!-- </persistenceAdapter> -->
<persistenceFactory>
<journalPersistenceAdapterFactory
journalLogFiles="5"
journalLogFileSize="32768"
useJournal="true"
useQuickJournal="true"
dataSource="#mysql-ds"
dataDirectory="../activemq-data" />
</persistenceFactory>
dataSource属性值指定将要引用的持久化数据库的bean名称,dataDirectory属性指定了Journal相关的日志保存位置。成功启动后则会发现多出一个activemq-data文件夹。
配置成功后,重启ActiveMQ服务。启动队列生产生产消息,消息不会立即同步到数据库中,如果过了一段时间后队列的消息还没被消费,则会自动持久化到数据库中。
8.总结
从最初的AMQ Message Store方案到ActiveMQ V4版本退出的High Performance Journal(高性能事务支持)附件,并且同步推出了关于关系型数据库的存储方案。ActiveMQ5.3版本又推出了对KahaDB的支持(5.4版本后被作为默认的持久化方案),后来ActiveMQ 5.8版本开始支持LevelDB,到现在5.9提供了标准的Zookeeper+LevelDB集群化方案。
ActiveMQ消息持久化机制有:
AMQ 基于日志文件
KahaDB 基于日志文件,从ActiveMQ5.4开始默认使用
JDBC 基于第三方数据库
Replicated LevelDB Store 从5.9开始提供了LevelDB和Zookeeper的数据复制方法,用于Master-slave方式的首选数据复制方案。
8.2 事务
在上面的示例代码中,创建session时传了两个参数,createSession(false, Session.AUTO_ACKNOWLEDGE)
,第一个参数表示是否开启事务,第二个参数表示签收方式。
当开启事务,即第一个参数为true
时,对于生产者而言执行send()
方法后,消息不会直接进入消息队列中(没有真正发送到MQ服务),只有执行session.commit()
消息才会真正发送成功进入消息队列中;对于消费者而言,消费完消息后,只有执行了session.commit()
消息才会从消息队列中出队,如果不执行session.commit()
会导致消息被重复消费。
事务开启的意义在于,对于多条必须同批次传输的消息,如果有一条传输失败,可以将事务回滚,再次传输,保证数据的完整性。
8.3 签收(ack)
签收和事务起到的作用是一样的,事务的优先级高于签收,即如果开启了事务,签收方式不管是哪种都是不起作用的,一般事务倾向于生产者使用,签收倾向于消费者使用。
签收方式总共有4种,AUTO_ACKNOWLEDGE
自动签收,CLIENT_ACKNOWLEDGE
手动签收,DUPS_OK_ACKNOWLEDGE
可重复的签收(不常用),SESSION_TRANSACTED
一般表示开启了事务设置任何签收方式是无效的。
如果签收方式为CLIENT_ACKNOWLEDGE
手动签收,必须执行message.acknowledge()
,消息才能被真正的消费或者发送。
9.docker搭建ActiveMQ集群
安装docker
拉取activemq 镜像
docker pull docker.io/webcenter/activemq:latest
docker run -it -d --name myactivemq1 -p 61617:61616 -p 8171:8161 -v /activemq-master-a.xml:/opt/activemq/conf/activemq.xml -v /usr/share/activemq1/kahadb:/opt/activemq/data/kahadb docker.io/webcenter/activemq:latest
docker run -it -d --name myactivemq2 -p 61618:61616 -p 8181:8161 -v /activemq-slave-a.xml:/opt/activemq/conf/activemq.xml -v /usr/share/activemq2/kahadb:/opt/activemq/data/kahadb docker.io/webcenter/activemq:latest
docker run -it -d --name myactivemq -p 61616:61616 -p 8161:8161 docker.io/webcenter/activemq:latest
配置
activemq-master-a.xml :
10 高级特性
10.1 异步投递
ActiveMQ支持以同步或异步模式向borker发送消息,所使用的模式对发送调用的延迟有很大的影响。由于延迟通常是生产者可以实现的吞吐量中的一个重要因素,因此使用异步发送可以显著提高系统的性能。
ActiveMQ默认以异步模式发送消息,以同步模式发送的情况是除非明确指定使用同步发送或者事务外部发送持久消息(即未使用事务的前提下发送持久化消息)。如果不使用事务,而是发送持久消息,那么每次发送都会同步并阻塞,直到broker向生产者发送确认消息已安全持久存储到磁盘为止,此确认机制提供了消息不会丢失的保证,但由于客户端被阻塞需要付出巨大的延迟代价。
异步投递可以最大化producer端的发送效率。通常在发送消息量比较密集的情况下使用异步发送,它可以很大的提升producer的吞吐量,不过这也带来了额外的问题,就是需要消耗很多的client端内存的同时也会导致broker端性能消耗增加,此外不能有效的确保消失的发送成功。在使用异步投递的情况下客户端需要容忍消息丢失的可能。
- 开启异步投递的三种方式
1.通过Connection URI后面添加参数
cf = new ActiveMQConnectionFactory("tcp://locahost:61616?jms.useAsyncSend=true");
2.通过ConnectionFactory对象属性
((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true);
3.通过Connection对象属性,在此级别配置将覆盖ConnectionFactory级别的设置
((ActiveMQConnection)connection).setUseAsyncSend(true);
- 如何保证一部投递情况下消息不丢失
异步发送消息丢失的情况场景是,UseAsyncSend为true,使用producer.send(message)
持续发送消息,消息不会阻塞,生产者会认为所有的消息均会被发送到了MQ服务,如果MQ服务突然宕机,此时生产者端尚未同步到MQ服务的消息均会丢失。所以,正确的异步发送方法需要接收回调的。
同步发送和异步发送的区别就在于,同步发送send()
不阻塞就代表消息发送成功,异步发送需要接收回调并由客户端再判断一次是否发送。异步投递编码如下,
package com.huazai.activemq.advanced;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import org.apache.activemq.AsyncCallback;
import javax.jms.*;
import java.util.UUID;
/**
* ActiveMQ高级特性之异步投递
*/
public class JmsProduceAsyncSend {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 消息队列名称
public static final String QUEUE_NAME = "queue-async";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 开启异步投递
activeMQConnectionFactory.setUseAsyncSend(true);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Queue queue = session.createQueue(QUEUE_NAME);
// 可以用父接口Destination接受
// Destination queue = session.createQueue(QUEUE_NAME);
// 5.创建消息的生产者,一定要向上转型为ActiveMQMessageProducer类型才有异步投递功能
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
// 6.通过消息生产者生产6条消息发送MQ队列
for (int i = 0; i < 3; i++) {
// 7.创建消息
TextMessage textMessage = session.createTextMessage("异步投递消息" + i + ":hello world");
// 给消息设置一个唯一的id可以知道哪条消息发送成功,哪条消息发送失败
textMessage.setJMSMessageID(UUID.randomUUID().toString());
String msgId = textMessage.getJMSMessageID();
// 8.将消息发送到MQ,并回调判断消息是否发送成功
activeMQMessageProducer.send(textMessage, new AsyncCallback() {
/**
* 消息发送成功回调方法
*/
@Override
public void onSuccess() {
System.out.println("消息成功发送回调方法,成功消息标识:" + msgId);
}
/**
* 消息发送失败回调方法
*
* @param exception
*/
@Override
public void onException(JMSException exception) {
System.err.println("消息发送失败回调方法,失败消息标识:" + msgId);
}
});
}
// 9.关闭资源
activeMQMessageProducer.close();
session.close();
connection.close();
System.out.println("finish");
}
}
运行结果:
10.2 定时与延时投递
有时候我们需要消息在某个时间点发送或者延迟一段时间发送。
ActiveMQ开启定时与延迟投递,首先编辑activemq.xml
配置文件,<broker>
标签内添加属性schedulerSupport
并且设置为true
<broker xmlns="http://activemq.apache.org/schema/core" brokerName="localhost" dataDirectory="${activemq.data}" schedulerSupport="true">
代码中生产者端消息对象message
,需要设置时间调度相关属性,主要属性如下:
属性名称 | 类型 | 描述 |
---|---|---|
AMQ_SCHEDULED_DELAY | long | 延迟投递时间 |
AMQ_SCHEDULED_PERIOD | long | 重复投递时间间隔 |
AMQ_SCHEDULED_REPEAT | int | 重复投递次数 |
AMQ_SCHEDULED_CRON | String | cron表达式 |
消费者代码:
package com.huazai.activemq.advanced;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.time.LocalDateTime;
public class JmsConsumerDelayAndScheduleRecieve {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 消息队列名称,取消息必须和存消息的队列名称一致
public static final String QUEUE_NAME = "queue-delay-schedule";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Queue queue = session.createQueue(QUEUE_NAME);
// 5.创建消费者
MessageConsumer consumer = session.createConsumer(queue);
int i = 0;
while (true) {
// 接受消息根据生产者发送消息类型强类型转换
TextMessage message = (TextMessage) consumer.receive();
if (message != null) {
String text = message.getText();
System.out.print("第" + ++i + "次接受延迟消息:");
System.out.println(text);
int endSecond = LocalDateTime.now().getSecond();
} else {
break;
}
}
consumer.close();
session.close();
connection.close();
}
}
生产者代码:
package com.huazai.activemq.advanced;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import org.apache.activemq.AsyncCallback;
import org.apache.activemq.ScheduledMessage;
import javax.jms.*;
import java.util.UUID;
public class JmsProduceDelayAndScheduleSend {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 消息队列名称
public static final String QUEUE_NAME = "queue-delay-schedule";
/**
* 延迟投递的时间
*/
private static final long DELAY = 3 * 1000;
/**
* 每次投递的时间间隔
*/
private static final long PERIOD = 4 * 1000;
/**
* 投递的次数
*/
private static final int REPEAT = 5;
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Queue queue = session.createQueue(QUEUE_NAME);
// 可以用父接口Destination接受
// Destination queue = session.createQueue(QUEUE_NAME);
// 5.创建消息的生产者,一向上转型为ActiveMQMessageProducer
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
// 6.通过消息生产者生产6条消息发送MQ队列
for (int i = 0; i < 3; i++) {
// 7.创建消息
TextMessage textMessage = session.createTextMessage("延迟投递和定时投递消息" + i + ":hello world\n");
// 给消息设置一个唯一的id可以知道哪条消息发送成功,哪条消息发送失败
textMessage.setJMSMessageID(UUID.randomUUID().toString());
String msgId = textMessage.getJMSMessageID();
// 延迟投递的时间
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, DELAY);
// 每次投递的时间间隔
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, PERIOD);
// 投递的次数
textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, REPEAT);
// 8.将消息发送到MQ,并回调判断消息是否发送成功
activeMQMessageProducer.send(textMessage);
}
// 9.关闭资源
activeMQMessageProducer.close();
session.close();
connection.close();
System.out.println("finish");
}
}
先启动消费者,再启动生产者,然后观察消费者控制台,结果如下:
更多介绍参考官网 http://activemq.apache.org/de...
10.3 消费者消息重试策略
当下列任何一种情况发生时,borker会将消息重新传送至消费端:
- 使用事务会话并调用
rollback()
; - 使用事务会话调用
commit()
之前关闭已处理的会话; - 在手动签收
CLIENT_ACKNOWLEDGE
传递模式下调用session.recover()
; - 客户机连接超时(可能正在执行的代码比配置的超时时间更长)。
默认重发时间间隔为1秒总共重发6次,超过6次即最大重发次数后,消费端会给broker返送一个poison ack
表示这个消息有毒,告诉broker不要再发了,这个时候broker会把这个消息放到DLQ(死信队列),以便稍后对其进行分析并人工干预处理。
消息默认重发时间间隔为1秒,默认重发次数为6。一个消息被redelivedred超过默认的最大重发次数(默认6次)时,消费端会给MQ发一个“poison ack”表示这个消息有毒,告诉broker不要再发了。这个时候broker会把这个消息放到DLQ(死信队列)。
案例演示(演示第2种没有提交事务引发的重试机制)
生产者代码:
package com.huazai.activemq.advanced;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQMessageProducer;
import javax.jms.Connection;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
/**
* ActiveMQ高级特性之重试机制生产者
*/
public class JmsProduceRedeliveryPolicy {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 消息队列名称
public static final String QUEUE_NAME = "queue-redelivery-policy";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,第一个参数为是否开启事务,第二个参数为签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Queue queue = session.createQueue(QUEUE_NAME);
// 可以用父接口Destination接受
// Destination queue = session.createQueue(QUEUE_NAME);
// 5.创建消息的生产者,一定要向上转型为ActiveMQMessageProducer类型才有异步投递功能
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
// 6.通过消息生产者生产消息发送MQ队列
// 7.创建消息
TextMessage textMessage = session.createTextMessage("投递消息:" + ":hello world");
// 8.将消息发送到MQ,并回调判断消息是否发送成功
activeMQMessageProducer.send(textMessage);
// 9.关闭资源
activeMQMessageProducer.close();
session.close();
connection.close();
System.out.println("finish");
}
}
消费者代码:
package com.huazai.activemq.advanced;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* ActiveMQ高级特性之重试机制消费者
*/
public class JmsConsumerRedeliveryPolicy {
// ActiveMQ服务地址
public static final String ACTIVEMQ_URL = "tcp://192.168.64.129:61616";
// 消息队列名称,取消息必须和存消息的队列名称一致
public static final String QUEUE_NAME = "queue-redelivery-policy";
public static void main(String[] args) throws Exception {
// 1.创建给定ActiveMQ服务连接工厂,使用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2.通过连接工厂,创建连接对象并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3.创建会话,true开启事务
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
// 4.创建目的地(队列或者主题)
Queue queue = session.createQueue(QUEUE_NAME);
// 5.创建消费者
MessageConsumer consumer = session.createConsumer(queue);
while (true) {
// 接受消息根据生产者发送消息类型强类型转换
TextMessage message = (TextMessage) consumer.receive();
if (message != null) {
String text = message.getText();
System.out.println(text);
// 注意此处故意不提交事务
// session.commit();
} else {
break;
}
}
consumer.close();
session.close();
connection.close();
}
}
注意消费者代码已开启事务,但故意不提交事务。
启动生产者服务,再启动消费者服务,消费者能成功接收到队列的消息,但是控制台的消息并未出队。
再重新启动消费者服务,发现之前消费的消息还能重复消费。ActiveMQ默认重发次数为6次,当消费者第七次启动消费消息时,发现消费不到消息,查看控制台发现,未消费的消息已出队列,并且多出了一个名为ActiveMQ.DLQ的队列。
ActiveMQ.DLQ即为死信队列,超过重复消费上限次数(6次)的消息被放到了死信队列里。
此时如果我们把代码里的队列名称改成ActiveMQ.DLQ,同样会拿到之前重复消费的消息。
public static final String QUEUE_NAME = "ActiveMQ.DLQ";
常用重发策略配置如下:
属性名称 | 默认值 | 描述 |
---|---|---|
collisionAvoidanceFactor | 0.15 | 设置防止冲突范围的正负百分比,只有启用useCollisionAvoidance参数时才生效。也就是在延迟时间上再加一个时间波动范围 |
initialRedeliveryDelay | 1000L | 初始重发延迟时间 |
maximumRedeliveries | 6 | 最大重发次数,达到最大重发次数后消息进入死信队列。为-1时不限制次数,为0时表示不进行重发 |
maximumRedeliveryDelay | -1 | 最大重发延迟时间,只有useExponentialBackOff为true时有效(v5.5)。假设首次重发间隔为10ms,倍数为2,那么第二次重发时间间隔为20ms,第三次时间间隔为40ms,当重发时间间隔的达到最大传送延迟时间,以后每次重发时间间隔都为最大传送延迟时间。为-1时不限制最大时间间隔 |
redeliveryDelay | 1000L | 重发延迟时间,当initialRedeliveryDelay=0生效 |
useCollisionAvoidance | false | 启用防止冲突功能 |
useExponentialBackOff | false | 启用指数倍数递增的方式增加延迟时间 |
backOffMultiplier | 5 | 重发时间间隔递增倍数,只有值大于1和启用useExponentialBackOff参数时才生效 |
更多重发介绍参考官网 http://activemq.apache.org/re...
10.4 死信队列
ActiveMQ中的默认死信队列名称为ActiveMQ.DLQ
,所有无法交付的消息都将被发送到这个队列,这可能很难管理,因此,你也可以在activemq.xml
配置文件为每个目的地配置单独的死信队列,如下:
<!-- 单独为每个queue目的地设置一个死信队列,前缀为DLQ -->
<destinationPolicy>
<policyMap>
<policyEntries>
<!-- Set the following policy on all queues using the '>' wildcard -->
<policyEntry queue=">">
<deadLetterStrategy>
<!-- Use the prefix 'DLQ.' for the destination name, and make the DLQ a queue rather than a topic -->
<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true"/>
</deadLetterStrategy>
</policyEntry>
</policyEntries>
</policyMap>
</destinationPolicy>
ActiveMQ中引入了“死信队列”(Dead Letter Queue)的概念。即一条消息再被重发了多次后(默认为重发6次,即redeliveryCounter==6) 。将会被ActiveMQ移入“死信队列”。开发人员可以在这个Queue中查看处理出错的消息,进行人工干预。
死信队列应用案例
DLQ-死信队列(Dead Letter Queue)用来保存处理失败或者过期的消息。
一般生产环境中在使用MQ的时候设计两个队列:一个是核心业务队列,一个是死信队列。
核心业务队列,就是比如上图专门用来让订单系统发送订单消息的,然后另外一个死信队列就是用来处理异常情况的。
假如第三方物流系统故障了,此时无法请求,那么仓储系统每次消费到一条订单消息,尝试通知发货和配送都会遇到对方的接口报错。此时仓储系统就可以把这条消息拒绝访问或者标记为处理失败。一旦标记这条消息处理失败了之后,MQ就会把这条消息转入提前设置好的一个死信队列中。然后你会看到的就是,在第三方物流系统故障期间,所有的订单消息全部处理失败,全部都会转入到死信队列。然后你的仓储系统得专门找一个后台线程,监控第三方物流系统是否正常,是否能请求,不停地监视。一旦发现对方恢复正常,这个后台线程就从死信队列消费出来处理失败的订单,重新执行发货和配送通知。
缺省持久消息过期,会被送到死信队列,非持久消息不会送到DLQ。
缺省的死信队列是ActiveMQ.DLQ,如果没有特别指定,死信都会被发送到这个队列中。
死信队列策略
- 共享队列策略(默认)
<!-- “>”表示对所有队列生效,如果需要设置指定队列,则直接写队 列名称-->
<policyEntry queue=">">
<deadLetterStrategy>
<sharedDeadLetterStrategy processNonPersistent="false" processExpired="true" deadLetterQueue="" />
</deadLetterStrategy>
</policyEntry>
processNonPersistent:是否将非持久化消息发送到死信队列,默认false;processExpired:是否将过期消息放入到死信队列中,默认为true
- 独立死信队列策略
<!-- “>”表示对所有队列生效,如果需要设置指定队列,则直接写队 列名称-->
<policyEntry queue=">">
<deadLetterStrategy>
<!--
Use the prefix 'DLQ.' for the destination name, and make
the DLQ a queue rather than a topic
-->
<individualDeadLetterStrategy queuePrefix="DLQ."/>
</deadLetterStrategy>
</policyEntry>
queuePrefix:设置死信队列前缀
死信队列详细介绍参考官网 http://activemq.apache.org/me...
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现