MQ之ActiveMQ
Activemq
前言
首先,很感谢尚硅谷能够公开这些视频课程,这里主要借鉴周阳老师的课件。
1.入门概述
1.1 MQ 的产品种类和对比
MQ 就是消息中间件。MQ 是一种理念,ActiveMQ 是MQ 的落地产品。不管是哪款消息中间件,都有如下一些
技术维度:
(1) kafka
编程语言:scala。
大数据领域的主流MQ。
(2) rabbitmq
编程语言:erlang
基于erlang 语言,不好修改底层,不要查找问题的原因,不建议选用。
(3) rocketmq
编程语言:java
适用于大型项目。适用于集群。
(4) activemq
编程语言:java
适用于中小型项目。
1.2 MQ 的产生背景
1.2.1 系统之间接口耦合比较严重 ----解耦
1.2.2 面对大流量并发时,容易被冲垮 ----- 削峰
1.2.3 等待同步存在性能问题 ----- 异步
1.3 MQ 的主要作用
(1) 异步。调用者无需等待。
(2) 解耦。解决了系统之间耦合调用的问题。
(3) 消峰。抵御洪峰流量,保护了主业务。
1.4 MQ 的定义
1.5 MQ 的特点
2 ActiveMQ 安装和控制台
2.1 ActiveMQ安装
前提条件:linux服务器要安装jdk8以上的版本。
跟随老师讲课版本是5.15.9 安装在linux上。
ActiveMQ官网
官网下载地址
安装步骤:
- 将安装包传到
/opt
目录下,然后进行解压
- 在根目录下创建 文件夹
myactiveMQ
- 启动
- 普通启动
注意有坑
因为主机名不符合规范导致无法启动activemq
参考csdn博客
- 查看启动状态(三种方式)
- 关闭服务
- 带日志方式启动
2.2 ActiveMQ控制台
首先将linux和windows的防火墙给关闭,这样才能进行相互连接。。因为我们将windows做客户端,将linux做服务器端,通过windows浏览器ip地址访问服务器端即可。
防火墙未关闭前:
防火墙关闭后:
注意linxu关闭防火墙服务的时候要区分是cnetos7还是centos6。
Linux关闭防火漆服务:
linux能连接到本机。
windows能ping通linux服务器:
重要
:
ActiveMQ端口号:
61616 :后台端口号
8161:前台端口号
在windows客户端中通过ip地址访问。如下图:(以前是localhost是因为安装就是在本地windows)
登录默认用户名和密码是:admin。
3 入门案例、MQ标准、API详解
3.1 pom导入依赖
<dependencies>
<!--activemq所需要的jar包配置-->
<!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-all -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.xbean/xbean-spring -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
<!--整合spring的时候需要用到的依赖-->
<!--下面是junit/log4j等基础通用配置-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
3.2 JMS 编码总体规范
3.3 Destination 简介
3.4 队列消息生产者的入门案例
生产者
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2 通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3 创建会话session
//两个参数,第一个叫事务/第二个叫签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4 创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
//6 通过使用messageProducer生产3条消息发送到MQ的队列里面
for (int i = 1; i <= 3 ; i++) { //测试消费者的3中案例
//7 创建消息,好比学生按照老师的要求写好的面试题消息
TextMessage textMessage = session.createTextMessage("msg---" + i);//理解为一个字符串
// 8 通过messageProducer 发送给 MQ
messageProducer.send(textMessage);
}
//9 关闭资源 正着生产,倒着关闭
messageProducer.close();
session.close();
connection.close();
System.out.println("********消息发布到MQ完成");
}
}
补充
Queue queue 直接alt+enter是前面这个queue类型,但是我们可以改成destination类型(queue是destination的子接口,功能更加强大)
destination的子接口有queue topic 父接口一般是定义规范。子接口一般是实现更加强大的功能
和集合接口我们常用arraylist接口一样 collection collection = new arrayList;
Destination destination = session.createQueue(QUEUE_NAME);
3.5 ActiveMQ控制台之队列
3.6 队列消费者的入门案例(同步阻塞和异步监听)
消费者
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2 通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
//两个参数,第一个叫事务/第二个叫签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4 创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
/**
*
① 同步阻塞方式(recieve)
订阅者或接收者调用MessageConsumer的recieve()方法来接收消息,recieve方法在能够接收到消息之前(或超时之前)将一直阻塞。
// 6 消费者接收队列中的消息
while (true){
//7 生产者生产的是TextMessage这个类型,所以消费者也要消费同种类型,所以需要进行强制转换
// receive() : 没有任何参数的话, “不见不散”,也就是会死等,消费者会一直等着接收消息,即使当前消息已经消费完了
// receive(long tiemout): 过时不候, 等一定时间。
TextMessage textMessage= (TextMessage)messageConsumer.receive(4000L);
if (null != textMessage){
System.out.println("消费者接收到消息:" + textMessage.getText());
}else{
break;
}
}
// 8 关闭资源
// 顺着申请,倒着关闭资源
messageConsumer.close();
session.close();
connection.close();
*/
/*② 监听方式
通过监听的方式来消费消息 MessageConsumer messageConsumer = session.createConsumer(queue);
是异步非阻塞的方式(监听器onMessage())
订阅者或接受者通过 MessageConsumer的 setMessageListener(MessageListener listener)注册一个消息监听器,
当消息到达之后,系统自动调用MessageListener 的onMessage 方法处理消息
*/
// 使用匿名内部类方式
messageConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (null != message && message instanceof TextMessage){
//强转
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者接收到消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
//防止执行太快,保证控制台不灭
//因为消费者进行连接到消息中间件,会有一系列验证,如果不写Syetem.in.read();程序会马上
//执行完,但是消费者不会接收到任何消息
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
注意看上面的代码,其实我们不难发现消息的消费者有两种模式。
- 第一种是同步阻塞式
订阅者或接收者调用MessageConsumer的recieve()方法来接收消息,recieve方法在能够接收到消息之前(或超时之前)将一直阻塞
- 第二种是异步监听式
我们可以使用MessageListener
来监听消息,通过注册一个监听器,当有消息发送来时,系统自动调用MessageListener的onMessage方法处理消息。
从上面两种方式的区别来看,我们大多数都会使用异步监听式来处理。
3.7 队列小结
3.7.1 队列特点
3.7.2 消费者情况
3.8 Topic 介绍
3.9 生产者 和消费者 案例
其实队列和主题这两种形式简单来说代码改动就在于session.createProducer(topic) / session.createTopic
,session.createProducer(queue) / session.createQueue
。
不信我们来看代码。
- 消费者:
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
public static final String TOPIC_NAME = "topic-atguigu";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是3号消费者");
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2 通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
//两个参数,第一个叫事务/第二个叫签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4 创建目的地(具体是队列还是主题topic)
Topic topic = session.createTopic(TOPIC_NAME);
//5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(topic);
/*
② 通过监听的方式来消费消息 MessageConsumer messageConsumer = session.createConsumer(queue);
是异步非阻塞的方式(监听器onMessage())
订阅者或接受者通过 MessageConsumer的 setMessageListener(MessageListener listener)注册一个消息监听器,
当消息到达之后,系统自动调用MessageListener 的onMessage 方法处理消息
*/
//使用 lambda表达式
messageConsumer.setMessageListener((message -> {
if (null != message && message instanceof TextMessage){
//强转
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者接收到消息topic:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}));
//防止执行太快,保证控制台不灭
//因为消费者进行连接到消息中间件,会有一系列验证,如果不写Syetem.in.read();程序会马上
//执行完,但是消费者不会接收到任何消息
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
注意
先启动订阅者再启动生产者,不然发送的消息是废消息
- 生产者:
public class JmsProducer {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
public static final String TOPIC_NAME = "topic-atguigu";
public static void main(String[] args) throws JMSException, IOException {
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2 通过连接工厂,获得连接connection 并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3 创建会话session
//两个参数,第一个叫事务/第二个叫签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4 创建目的地
Topic topic = session.createTopic(TOPIC_NAME);
//5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(topic);
//6 通过messageProducer生产3条消息发送至MQ的对列里面
for (int i = 1; i <= 3; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage("messageListener---" + i);
//8 通过messageProducer 发送到 MQ
messageProducer.send(textMessage);
}
//9 关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("********消息发布到MQ完成");
}
}
3.10 小总结
3.10.1 两种模式特效
3.10.2 两种模式比较
4 JMS 规范
4.1 JMS 是什么
- JAVAEE是什么?
- 什么是Java 消息服务?
JMS – Java Message Service
(Java消息服务是JavaEE中的一个技术)
Java 消息服务指的是两个应用程序之间进行异步通信的API,它为标准协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java 应用程序开发。在JavaEE 中,当两个应用程序使用JMS 进行通信时,它们之间不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。
- MQ中间件的其他落地产品
MQ几种落地产品对比:
4.2 消息头
JMS 的消息头有哪些属性:
- JMSDestination:消息目的地
消息发送的目的地,主要是指Queue和Topic
- JMSDeliveryMode:消息持久化模式
持久模式和非持久模式。
一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递。
一条非持久的消息:最多会传递一次,这意味着服务器出现故障,该消息将会永远丢失。)
- JMSExpiration:消息过期时间
可以设置消息在一定时间后过期,默认是
永不过期
消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。
如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期。
如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。
- JMSPriority:消息的优先级
消息优先级,从0-9十个级别,0-4是普通消息5-9是加急消息
JMS不要求
MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。
- JMSMessageID:消息的唯一标识符。后面我们会介绍如何解决幂等性。
说明
: 消息的生产者可以set 这些属性,消息的消费者可以get 这些属性。这些属性在send 方法里面也可以设置。
代码示例:
// 这里可以指定每个消息的目的地
textMessage.setJMSDestination(topic);
/*
持久模式和非持久模式。
一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS 提供者出现故障,该消息并不会丢失,它
会在服务器恢复之后再次传递。
一条非持久的消息:最多会传递一次,这意味着服务器出现故障,该消息将会永远丢失。
*/
textMessage.setJMSDeliveryMode(0);
/*
可以设置消息在一定时间后过期,默认是永不过期。
消息过期时间,等于Destination 的send 方法中的timeToLive 值加上发送时刻的GMT 时间值。
如果timeToLive 值等于0,则JMSExpiration 被设为0,表示该消息永不过期。
如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。
*/
textMessage.setJMSExpiration(1000);
/* 消息优先级,从0-9 十个级别,0-4 是普通消息5-9 是加急消息。
JMS 不要求MQ 严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4 级。
*/
textMessage.setJMSPriority(10);
// 唯一标识每个消息的标识。MQ 会给我们默认生成一个,我们也可以自己指定。
textMessage.setJMSMessageID("ABCD");
// 上面有些属性在send 方法里也能设置
messageProducer.send(textMessage);
4.3 消息属性
注意
发送和接收的消息体类型必须一致对应
在日常开发和生产中我们最主要使用的就是TextMessage和MapMessage这两种形式,几乎涵盖了百分之九十五的情况。
4.4 消息属性
如果需要除消息头字段之外的值,那么可以使用消息属性。他是识别/去重/重点标注等操作,非常有用的方法。他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器。消息的属性就像可以分配给一条消息的附加消息头一样。
它们允许开发者添加有关消息的不透明附加信息。它们还用于暴露消息选择器在消息过滤时使用的数据。
下图是设置消息属性的API:
接下来介绍JMS的可靠性:
4.5 消息持久化
4.5.1 什么是持久化消息
4.5.2 queue持久化和非持久化
持久化(默认)
MessageProducer messageProducer = session.createProducer(queue);
//设置通过session创建出来的生产者生产的Queue消息为持久性
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
非持久化:
下面进行案例演示:
非持久化
- 启动生产者,查看消息:
- 将mq宕机:
- 重新启动
- 再启动消费者,发现获取不到消息。消息丢失
- 查看情况,消息不见,丢失。
持久化(默认情况)
重复和上面一样的操作
查看情况,消费者还是能消费到宕机前生产者生产的3条消息。
结论:
持久化消息
这是队列的默认传递模式
,此模式保证这些消息只被传送一次
和成功使用一次
。对于这些消息,可靠性是优先考虑的因素。
可靠性的另一个重要方面是确保持久性消息传送至目标后,消息服务在向消费者传送它们之前不会丢失这些消息
。
4.5.3 topic的持久化和非持久化
对于主题,我们都知道要先消费即订阅之后再生产消息,否则如果先生产消息,在订阅,原来生产的消息,会是废消息,就像微信公众号一样,你没有订阅某个公众号A,那么在你没订阅A之前,你是不会接收到这个A的生产的消息的。如果你订阅了A之后,那么A发布的消息就会被你接收到。所以我们得出结论,对于主题而言,要先消费在生产。所以对于主题而言,非持久化没有任何意义。所以我们主要考虑持久化。
持久化topic
改造生产者:先告诉我们生产的是一个持久化的topic。
订阅者在线
订阅者不在线
改造消费者:
谁订阅了,订阅了哪个主题。
表明有一个z4的用户订阅了,订阅者订阅了这个主题。一定要先消费一次即表明先订阅这个主题,然后在执行生产者生产消息。无论消费者是否在不在线,因为你已经订阅了,所以你一定会收到这些消息。
订阅者在线
订阅者不在线
控制台:
4.6 消息的事务性
代码示例:
生产者
public class JmsConsumer_TX {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
// public static final String ACTIVEMQ_URL = "tcp://localhost:61616";//测试嵌入式broker
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
// System.out.println("我是1号消费者");
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 测试消息重发机制(默认间隔1秒,重发6次)
// 修改默认参数,设置消息消费重试3次
// RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
// redeliveryPolicy.setMaximumRedeliveries(3);
// activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy);
//2 通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
//两个参数,第一个叫事务/第二个叫签收
// Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);//测试事务
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);//非事务下测试签收
// Session session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE);//事务下测试签收
//AUTO_ACKNOWLEDGE:自动签收(默认) CLIENT_ACKNOWLEDGE:手动签收
//4 创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
while (true){
//7 生产者生产的是TextMessage这个类型,所以消费者也要消费同种类型,所以需要进行强制转换
TextMessage textMessage= (TextMessage)messageConsumer.receive(4000L);
if (null != textMessage){
System.out.println("消费者接收到消息broker:" + textMessage.getText());
//手动签收后要设置对消息的反馈
textMessage.acknowledge();//如果开启了手动签收,消费者没有写这句代码的话,会出现重复消费问题
}else{
break;
}
}
messageConsumer.close();
// session.commit();//如果开启了事务,但是没有选择提交的话,会重复消费(mq认为消息没有被消费,出现重复消费问题;提交后,就正常了,
// 即消息只消费一次)
session.close();
connection.close();
}
}
生产者
public class JmsProduce_TX {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
// public static final String ACTIVEMQ_URL = "tcp://localhost:61616";//测试嵌入式broker
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2 通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3 创建会话session
//两个参数,第一个叫事务/第二个叫签收
// Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);//事务 :true
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);//非事务下测试签收
//4 创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
//6 通过使用messageProducer生产3条消息发送到MQ的队列里面
for (int i = 1; i <= 3 ; i++) {
//7 创建消息,好比学生按照老师的要求写好的面试题消息
TextMessage textMessage = session.createTextMessage(" tx msg---" + i);
// 8 通过messageProducer 发送给 MQ
messageProducer.send(textMessage);
}
//9 关闭资源
messageProducer.close();
// session.commit();//关闭session之前手动提交(如果没有提交,则mq认为没有消息产生)
session.close();
connection.close();
System.out.println("********消息发布到MQ完成");
}
}
4.7 消息的签收机制
4.7.1 签收的几种方式
4.7.2 事务和签收的关系
代码示例:(事务情况下)
消息生产者
public class Jms_Transaction_AUTOACK_Producer {
private static final String ACTIVEMQ_URL = "tcp://192.168.10.130:61616";
private static final String ACTIVEMQ_QUEUE_NAME = "Queue-ACK-NoTransaction";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
MessageProducer producer = session.createProducer(queue);
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("Transaction_AUTOACK-msg: " + i);
producer.send(textMessage);
}
session.commit();
System.out.println("发送完成");
producer.close();
session.close();
connection.close();
}
}
控制台
消息消费者
public class Jms_Transaction_CLIENTACK_Consumer {
private static final String ACTIVEMQ_URL = "tcp://192.168.10.130:61616";
private static final String ACTIVEMQ_QUEUE_NAME = "Queue-ACK-Transaction";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//消费者设置了手动签收,就必须自己签收,向服务器发送我已经收到消息了
//开启事务如果不提交,就算手动签收,也是无效的
Session session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE);
Queue queue = session.createQueue(ACTIVEMQ_QUEUE_NAME);
MessageConsumer messageConsumer = session.createConsumer(queue);
messageConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
textMessage.acknowledge();
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
}
}
控制台
4.8 JMS 的点对点总结
点对点模型是基于队列的,生产者发送消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能。和我们平时给朋友发送短信类似。
1:如果在Session关闭时有部分消息被收到但还没有被签收(acknowledge),那当消费者下次连接到相同的队列时,这些消息还会被再次接收
2:队列可以长久的保存消息直到消费者收到消息。消费者不需要因为担心消息会丢失而时刻和队列保持激活的链接状态,充分体现了异步传输模式
的优势。
4.9 JMS 的发布订阅总结
- JMS的发布订阅总结
JMS Pub/Sub 模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic。
主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息。
主题使得消息订阅者和消息发布者保持互相独立不需要解除即可保证消息的传送
- 非持久订阅
非持久订阅只有当客户端处于激活状态,也就是和MQ保持连接状态才能收发到某个主题的消息。
如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到。
一句话:先订阅注册才能接受到发布,只给订阅者发布消息。
- 持久订阅
客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ的时候,会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
当非持久订阅状态下,不能恢复或重新派送一个未签收的消息。
持久订阅才能恢复或重新派送一个未签收的消息。
- 如何选择
当所有的消息必须被接收,则用持久订阅。当消息丢失能够被容忍,则用非持久订阅
5 ActiveMQ 的broker
5.1 broker是什么?
相当于一个ActiveMQ 服务器实例
。说白了,Broker 其实就是实现了用代码的形式启动ActiveMQ 将MQ 嵌入到Java 代码中,以便随时用随时启动,在用的时候再去启动这样能节省了资源,也保证了可用性。这种方式,我们实际开发中很少采用,因为他缺少太多了东西,如:日志,数据存储等等。
5.2 启动broker 时指定配置文件
启动broker 时指定配置文件,可以帮助我们在一台服务器上启动多个broker。实际工作中一般一台服务器只启动一个broker。
配置不同的配置文件和端口,启动不同的MQ实例。
5.3 嵌入式的broker 启动
用ActiveMQ Broker 作为独立的消息服务器来构建Java 应用。ActiveMQ 也支持在vm 中通信基于嵌入的broker,能够无缝的集成其他java 应用。
- 引入依赖
<!--Broker所需依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
- 编写代码
/**linux 下按照不同的conf配置启动activemq:./activemq start xbean:file:/myactiveMQ/apache-activemq-5.15.9/conf/activemq02.xml
*
* 用ActiveMQ Broker 作为独立的消息服务器来构建Java 应用。ActiveMQ 也支持在vm 中通信基于嵌入的broker,能够无缝的集成其他java 应用。
* @author wystart
* @create 2022-08-25 13:03
*/
public class EmbedBroker {
public static void main(String[] args) throws Exception {
//ActiveMQ 也支持在vm 中通信基于嵌入的broker,能够无缝的集成其他java 应用。
//相当于window下的activemq
BrokerService brokerService = new BrokerService();
brokerService.setUseJmx(true);
//"tcp://192.168.249.103:61616" 这个是放在linux服务器上的,我们现在broker是嵌入式mq实例,是在本机上运行的
brokerService.addConnector("tcp://localhost:61616");
brokerService.start();
}
}
运行代码可以看出,在没有运行linux服务器上的mq实例的情况下,仍然能正常启动得到一个mq服务器。
6 Spring 整合ActiveMQ
6.1 依赖引入
基本依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu</groupId>
<artifactId>activemq</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--activemq所需要的jar包配置-->
<!-- https://mvnrepository.com/artifact/org.apache.activemq/activemq-all -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.xbean/xbean-spring -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
<!--spring整合mq-->
<!--activemq对JMS的支持,整合SPringle和Activemq-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<!--activemq所需要的pool包配置-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.15.9</version>
</dependency>
<!--Spring-AOP等相关jar-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.3.23.RELEASE</version>
</dependency>
</dependencies>
6.2 编写applicationContext.xml
applicationContext.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.atguigu"/>
<!--配置生产者-->
<bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
<property name="connectionFactory">
<!--真正可以产生Connection的ConnectionFactory,由对应的JMS服务厂商提供-->
<bean class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.76.100:61616"/>
</bean>
</property>
<!--最大连接数-->
<property name="maxConnections" value="100"></property>
</bean>
<!--这个是队列目的地,点对点的-->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
<!--队列的名字spring-active-queue-->
<constructor-arg index="0" value="spring-active-queue"/>
</bean>
<!--Spring提供的JMS工具类,它可以进行消息发送、接收等-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="jmsFactory"/>
<!--队列-->
<property name="defaultDestination" ref="destinationQueue"/>
<property name="messageConverter">
<bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
</property>
</bean>
</beans>
6.3 编写队列消费者和生产者代码
队列生产者
/**本来还需要写controller层的,此案例中为了简便
*/
@Service
public class SpringMQ_Produce {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQ_Produce produce = (SpringMQ_Produce) ctx.getBean("springMQ_Produce");
/*
//匿名内部类方式
produce.jmsTemplate.send(new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case....");
return textMessage;
}
});
*/
//lambda表达式 ----要求是接口,而且接口中只有一个方法
//口诀:拷贝,小括号,写死,右箭头,落地,大括号{},而且只有一个参数,可以将类型省略
produce.jmsTemplate.send(session -> {
// TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case1111....");
// TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case for topic....");
TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case for MessageListener....");
return textMessage;
});
System.out.println("********send task over");
}
}
队列消费者
@Service
public class SpringMQ_Consumer {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQ_Consumer consumer = (SpringMQ_Consumer) ctx.getBean("springMQ_Consumer");
// Object --- Spring 因为生产者发送的是textMessage,是 string字符串
String retValue = (String) consumer.jmsTemplate.receiveAndConvert();
System.out.println("*********消费者收到的消息:"+ retValue);
}
}
6.4 编写主题消费者和生产者代码
applicationContext.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.atguigu"/>
<!--配置生产者-->
<bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
<property name="connectionFactory">
<!--真正可以产生Connection的ConnectionFactory,由对应的JMS服务厂商提供-->
<bean class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.76.100:61616"/>
</bean>
</property>
<!--最大连接数-->
<property name="maxConnections" value="100"></property>
</bean>
<!--这个是队列目的地,点对点的-->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
<!--队列的名字spring-active-queue-->
<constructor-arg index="0" value="spring-active-queue"/>
</bean>
<!--这个是主题-->
<bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg index="0" value="spring-active-Topic"/>
</bean>
<!--Spring提供的JMS工具类,它可以进行消息发送、接收等-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="jmsFactory"/>
<!--主题-->
<property name="defaultDestination" ref="destinationTopic"/>
<property name="messageConverter">
<bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
</property>
</bean>
</beans>
只有配置文件需要改变,改变目的地,代码不需要改变
主题消费者
@Service
public class SpringMQ_Consumer {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQ_Consumer consumer = (SpringMQ_Consumer) ctx.getBean("springMQ_Consumer");
// Object --- Spring 因为生产者发送的是textMessage,是 string字符串
String retValue = (String) consumer.jmsTemplate.receiveAndConvert();
System.out.println("*********消费者收到的消息:"+ retValue);
}
}
主题生产者
@Service
public class SpringMQ_Produce {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
SpringMQ_Produce produce = (SpringMQ_Produce) ctx.getBean("springMQ_Produce");
/*
//匿名内部类方式
produce.jmsTemplate.send(new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case....");
return textMessage;
}
});
*/
//lambda表达式 ----要求是接口,而且接口中只有一个方法
//口诀:拷贝,小括号,写死,右箭头,落地,大括号{},而且只有一个参数,可以将类型省略
produce.jmsTemplate.send(session -> {
// TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case1111....");
// TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case for topic....");
TextMessage textMessage = session.createTextMessage("*******spring和ActiveMQ的整合case for MessageListener....");
return textMessage;
});
System.out.println("********send task over");
}
}
6.5 配置消费者的监听类
类似于前面setMessageListenner实时间提供消息
只需要在配置文件中加入一个监听类的配置即可。
applicationContext.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.atguigu"/>
<!--配置生产者-->
<bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
<property name="connectionFactory">
<!--真正可以产生Connection的ConnectionFactory,由对应的JMS服务厂商提供-->
<bean class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://192.168.76.100:61616"/>
</bean>
</property>
<!--最大连接数-->
<property name="maxConnections" value="100"></property>
</bean>
<!--这个是队列目的地,点对点的-->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
<!--队列的名字spring-active-queue-->
<constructor-arg index="0" value="spring-active-queue"/>
</bean>
<!--这个是主题-->
<bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg index="0" value="spring-active-Topic"/>
</bean>
<!--Spring提供的JMS工具类,它可以进行消息发送、接收等-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="jmsFactory"/>
<!--主题-->
<property name="defaultDestination" ref="destinationTopic"/>
<property name="messageConverter">
<bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
</property>
</bean>
<!--配置监听程序-->
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="jmsFactory"/>
<property name="destination" ref="destinationTopic"/>
<!--public class MyMessageListener implements MessageListener-->
<property name="messageListener" ref="myMessageListener"/>
</bean>
<!--以配置方式注册MessageListener类进IOC容器-->
<!--<bean id="myMessageListener" class="com.atguigu.spring.MyMessageListener"/>-->
</beans>
监听类
@Component
public class MyMessageListener implements MessageListener {
@Override
public void onMessage(Message message) {
if(null != message && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
7 springboot整合AcitveMQ
7.1 队列
7.1.1 生产者
[1] 创建maven工程并完成包名准备
[2] 引入依赖 — pom.xml
最主要的是要引入spring-boot-starter-activemq
这个依赖。
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>
</project>
[3] 创建application.yml
注意这个application.yml是我们自己创建的哈。
微服务端口+MQ服务器地址+主题/队列+主题/队列名称
#微服务端口
server:
port: 7777
spring:
activemq:
broker-url: tcp://192.168.249.103:61616 #服务器地址
user: admin
password: admin
jms:
#如果是false(默认),表示是队列,如果是true,表示是主题
pub-sub-domain: false
#自定义队列名字(k:v键值对)
#yml配置文件遇到“:” 或者 “-” 后面必须留一个空格,否则报错(切记)
myqueue: boot-activemq-queue
[4] 配置Bean — ConfigBean
类似Spring框架的application.xml文件,依赖注入。从application.yml中取值读出,进行依赖注入。
注意要开启基于Jms的注解,即@EnableJms。
/**类似于spring框架的applicationContext.xml文件
*/
@Component // 让spring 管理的注解,相当于spring 中在 xml 中写了个bean
@EnableJms //开启JMS注解 重要 ----- 开启jms 适配
public class ConfigBean {
// 注入配置文件中的myqueue
@Value("${myqueue}")
private String myQueue;
@Bean //<bean id="" class="" >
public Queue queue(){
return new ActiveMQQueue(myQueue);
}
}
[5] 创建Queue_Produce 生产者(两种投递方式)
*/@Component
public class Queue_Produce {
//JMS 模板
//JmsMessagingTemplate是Springboot的Jms模板,Spring的是JmsTemplate
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
//这个是我们配置的队列的目的地
//把ConfigBean类的Queue注入进来
@Autowired
private Queue queue;
//触发投递(工作中常用)
//发送消息
public void produceMessage(){
// 第一个参数是目的地,第二个参数是消息的内容
jmsMessagingTemplate.convertAndSend(queue,"****" + UUID.randomUUID().toString().substring(0,6));
}
//定时投递(工作中常用)
@Scheduled(fixedDelay = 3000) //间隔3秒钟进行投递
public void produceMessageScheduled(){
// 第一个参数是目的地,第二个参数是消息的内容
jmsMessagingTemplate.convertAndSend(queue,"****Scheduled" + UUID.randomUUID().toString().substring(0,6));
System.out.println("******produceMessageScheduled send ok");
}
JmsMessagingTemplate这个是我们在springboot项中要用的,后面代码都是通过这个模板来进行编写。
从上面的代码我们知道,在工作中常常有两种方式来进行消息的生产,第一种是触发投递
,即要运行生产者
才会进行消息的生产
;第二种是间隔一定时间
进行消息的产生,即间隔定投
。注意,间隔投递要加入 @Scheduled(fixedDelay = 3000), 比如说这样就是每隔3秒进行一次消息的产生。
[6] 主启动类MainApp_Produce
//主启动类
@SpringBootApplication
//定时投递的注解使用了要使用这个注解才行 -----即激活定时投递那个注解
@EnableScheduling
public class MainApp_Produce {
public static void main(String[] args) {
SpringApplication.run(MainApp_Produce.class, args);
}
}
注意主启动类中要加入@EnableScheduling
这个注解才会开启间隔投递。
[7] 进行测试
@SpringBootTest(classes = MainApp_Produce.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class TestActiveMQ {
@Resource // 这个是java 的注解,而 Autowried 是spring 的
private Queue_Produce queue_produce;
@Test
public void testSend() throws Exception{
// queue_produce.produceMessage();
queue_produce.produceMessageScheduled();
}
}
这个地方可进行可不进行。
7.1.2 消费者
[1] 创建相应工程并准备好工作
[2] 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
和上面生产者依赖一模一样。
[3] 创建application.yml
和上面生产者一模一样。
#微服务端口 不要和produce一样
server:
port: 8888
spring:
activemq:
broker-url: tcp://192.168.249.103:61616 #服务器地址
user: admin
password: admin
jms:
#如果是false(默认),表示是队列,如果是true,表示是主题
pub-sub-domain: false
#自定义队列名字(k:v键值对)
#yml配置文件遇到“:” 或者 “-” 后面必须留一个空格,否则报错(切记)
myqueue: boot-activemq-queue
这里注意端口号不要和生产者一样,因为springboot每一个单独项目都是一个个微服务。
[4] 创建Queue_Consumer 消费者
@Component
public class Queue_Consumer {
@JmsListener(destination = "${myqueue}")//以前spring中还需要专门写一个监听类,springboot直接注解实现即可
public void receive(TextMessage textMessage) throws JMSException {
System.out.println("消费者收到消息:"+ textMessage.getText());
}
}
和上面生产者依赖一模一样。
7.2 主题
7.2.1 生产者
[1] 创建工程并准备好工作
[2] 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
和上面队列是一模一样的。
[3] 创建application.yml
#微服务端口
#微服务端口
server:
port: 6666
spring:
activemq:
broker-url: tcp://192.168.249.103:61616 #服务器地址
user: admin
password: admin
jms:
#如果是false(默认),表示是队列,如果是true,表示是主题
pub-sub-domain: true
#自定义队列名字(k:v键值对)
#yml配置文件遇到“:” 或者 “-” 后面必须留一个空格,否则报错(切记)
#将要消费的主题名称
mytopic: boot-activemq-topic
[4] 配置Bean — ConfigBean
@Component
public class ConfigBean {
@Value("${mytopic}")
private String topicName;
@Bean
public Topic topic(){
return new ActiveMQTopic(topicName);
}
}
[5] 创建 Topic_Produce 生产者
@Component
public class Topic_Produce {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Topic topic;
@Scheduled(fixedDelay = 3000)
public void produceTopic(){
jmsMessagingTemplate.convertAndSend(topic,"主题消息:"+ UUID.randomUUID().toString().substring(0,6));
}
}
[6] 主启动类MainApp_TopicProduce
@SpringBootApplication
@EnableScheduling
public class MainApp_TopicProduce {
public static void main(String[] args) {
SpringApplication.run(MainApp_TopicProduce.class,args);
}
}
7.2.2 消费者
[1] 创建工程并准备好工作
[2] 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
和上面队列是一模一样的。
[3] 创建application.yml
#微服务端口
server:
port: 5566 #启动第二个消费者时候记得修改为5566端口号 模拟2个消费者
spring:
activemq:
broker-url: tcp://192.168.249.103:61616 #服务器地址
user: admin
password: admin
jms:
#如果是false(默认),表示是队列,如果是true,表示是主题
pub-sub-domain: true
#自定义队列名字(k:v键值对)
#yml配置文件遇到“:” 或者 “-” 后面必须留一个空格,否则报错(切记)
#将要消费的主题名称
mytopic: boot-activemq-topic
注意这个地方改为要订阅,改为true。
[4] 创建Topic_Consumer
@Component
public class Topic_Consumer {
@JmsListener(destination = "${mytopic}")
public void receive(TextMessage text) throws JMSException{
try {
System.out.println("消费者收到订阅的主题:"+text.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
[5] 主启动类MainApp_TopicConsumer
@SpringBootApplication
public class MainApp5555 {
public static void main(String[] args) {
SpringApplication.run(MainApp5555.class,args);
}
}
8 ActiveMQ 的传输协议
8.1 简介
ActiveMQ 支持的client-broker 通讯协议有:TVP、NIO、UDP、SSL、Http(s)、VM。其中配置TransportConnector 的文件在ActiveMQ 安装目录的conf/activemq.xml 中的标签之内。
activemq 传输协议的官方文档:http://activemq.apache.org/configuring-version-5-transports.html。
另外一份参考
:
8.2 支持的传输协议
个人说明:除了tcp 和nio 协议,其他的了解就行。各种协议有各自擅长该协议的中间件,工作中一般不会使用activemq 去实现这些协议。如: mqtt 是物联网专用协议,采用的中间件一般是mosquito。ws是websocket 的协议,是和前端对接常用的,一般在java 代码中内嵌一个基站(中间件)。stomp 好像是邮箱使用的协议的,各大邮箱公司都有基站(中间件)。
注意:协议不同,我们的代码都会不同。
8.2.1 TCP 协议
Transmission Control Protocol(TCP)
1.这是默认的Broker配置,TCP的Client监听端口61616
2.在网络传输数据前,必须要先序列化数据,消息是通过一个叫wire protocol的来序列化成字节流。
3.TCP连接的URI形式如:tcp://HostName:port?key=value&key=value,后面的参数是可选的。
4.TCP传输的的优点:
(4.1)TCP协议传输可靠性高,稳定性强
(4.2)高效率:字节流方式传递,效率很高
(4.3)有效性、可用性:应用广泛,支持任何平台
5.关于Transport协议的可选配置参数可以参考官网http://activemq.apache.org/configuring-version-5-transports.html
8.2.2 NIO 协议
New I/O API Protocol(NIO)
1.NIO协议和TCP协议类似,但NIO更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的client调用和服务器端有更多的负载。
2.适合使用NIO协议的场景:
(2.1)可能有大量的Client去连接到Broker上,一般情况下,大量的Client去连接Broker是被操作系统的线程所限制的。因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议。
(2.2)可能对于Broker有一个很迟钝的网络传输,NIO比TCP提供更好的性能。
3.NIO连接的URI形式:nio://hostname:port?key=value&key=value
4.关于Transport协议的可选配置参数可以参考官网http://activemq.apache.org/configuring-version-5-transports.html
8.2.3 AMQP 协议
Advanced Message Queuing Protocol
,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件限制。
8.2.4 STOMP 协议
STOP,Streaming Text Orientation Message Protocol
,是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息中间件)设计的简单文本协议。
8.2.5 MQTT 协议
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当作传感器和致动器(比如通过Twitter让房屋联网)的通信协议。
扩展:https://github.com/fusesource/mqtt-client
8.2.6 NIO 协议案例
官网:http://activemq.apache.org/configuring-version-5-transports.html
[1] 修改配置文件 activemq.xml
代码如下:<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true"/>
操作截图:
MQ控制台截图:
[2] 代码
public static final String ACTIVEMQ_URL = "nio://192.168.249.103:61618";//测试nio 协议
public static final String QUEUE_NAME = "Transport";
只需要如上即可。
8.2.7 NIO 协议案例增强
[1] 修改配置activemq.xml文件
官网: http://activemq.apache.org/auto
如下图,只要我们在修改nio协议的位置,将上面新加的注释掉,加入以下代码即可。
修改之后我们可以在代码出,前缀改为tcp或者nio都可以正常运行程序。如下图,控制台也显示。
9 ActiveMQ 的消息存储和持久化
9.1 介绍
官网: http://activemq.apache.org/persistence
9.2 是什么
为了避免意外宕机以后丢失信息,需要做到重启后可以恢复消息队列,消息系统一半都会采用持久化机制
。
ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的。
就是在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等。再试图将消息发给接收者,成功则将消息从存储中删除
,失败则继续尝试尝试发送。
消息中心启动以后,要先检查指定的存储位置是否有未成功发送的消息,如果有,则会先把存储位置中的消息发出去。
一句话:ActiveMQ宕机了,消息不会丢失的机制。
9.3 有哪些
9.4 kahaDB 消息存储
9.4.1 介绍
官网:http://activemq.aache.org/kahadb
9.4.2 说明
首先验证:
KahaDB是目前默认的存储方式,可用于任何场景,提高了性能和恢复能力。
消息存储使用一个
事务日志和仅仅用一个
索引文件来存储它所有的地址
。
KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模型进行了优化。
数据被追加到data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。
9.4.3 存储原理
KahaDB在消息保存的目录中有4类文件和一个lock,跟ActiveMQ的其他几种文件存储引擎相比,这就非常简洁了。
-
1,db-number.log
KahaDB存储消息到预定大小的数据纪录文件中,文件名为db-number.log。当数据文件已满时,一个新的文件会随之创建,number数值也会随之递增,它随着消息数量的增多,如没32M一个文件,文件名按照数字进行编号,如db-1.log,db-2.log······。当不再有引用到数据文件中的任何消息时,文件会被删除或者归档。
-
2,db.data
该文件包含了持久化的BTree索引,索引了消息数据记录中的消息,它是消息的索引文件,本质上是B-Tree(B树),使用B-Tree作为索引指向db-number。log里面存储消息。 -
3,db.free
当问当前db.data文件里面哪些页面是空闲的,文件具体内容是所有空闲页的ID -
4,db.redo
用来进行消息恢复,如果KahaDB消息存储再强制退出后启动,用于恢复BTree索引。 -
5,lock
文件锁,表示当前kahadb独写权限的broker。
9.5 LevelDB消息存储
官网:http://activemq.apache.org/leveldb-store
这种文件系统是从ActiveMQ5.8之后引进的,它和KahaDB非常相似,也是基于文件的本地数据库存储形式,但是它提供比KahaDB更快的持久性。
但它不使用自定义B-Tree实现来索引独写日志,而是使用基于LevelDB的索引。
默认配置如下:
<persistenceAdapter>
<levelDB directory="activemq-data"/>
</persistenceAdapter>
9.6 JDBC消息存储
9.6.1 设置步骤
-
原理图
官网:http://activemq.apache.org/persistence -
添加mysql数据库的驱动包到lib文件夹
将mysql连接的jar包拷贝到mq的lib文件夹下面。
dbcp2 ----官网默认自带的数据库连接池,所以我们导入的时候就只是导入了一个jar包。
注意如果使用德鲁伊或者c3p0这些数据库连接池,导入的时候不止上面单独这一个。切记。和以前导入依赖所必须的一样。
操作截图:
-
修改activemq.xml配置文件
在/myactiveMQ/apache-activemq-5.15.9/conf
路径下进行修改
修改前的KahaDB
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
修改后的jdbcPersisteceAdapter
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds" />
</persistenceAdapter>
dataSource
是指定将要引用的持久化数据库的bean名称。
注意
在dataSource后面还可以设置参数,例如:
createTableOnStartup
是否在启动的时候创建数据库表,默认是true,这样每次启动都会去创建表了,一般是第一次启动的时候设置为true,然后再去改成false。
- 配置数据库连接
先配置如下:
<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.249.103:13306/activemq?relaxAutoCommit=true"/>
<property name="username" value="root"/>
<property name="password" value="5864@WCY"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
这里需要注意:
此处配置需要在 </broker>
和<import >
之间添加
- 建库SQL 和创表说明
当我们设置好上面的步骤之后,先在windows本地计算机上创建一个activemq的数据库。当我们将mq进行重启之后,会自动生成三张表。
建一个名为activemq的数据库
- 三张表的说明:
1. ACTIVEMQ_MSGS
ID:自增的数据库主键
CONTAINER:消息的Destination
MSGID_PROD:消息发送者的主键
MSG_SEQ:是发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID
EXPIRATION:消息的过期时间,存储的是从1970-01-01到现在的毫秒数
MSG:消息本体的Java序列化对象的二进制数据
PRIORITY:优先级,从0-9,数值越大优先级越高
消息表,缺省表名ACTIVEMQ_MSGS,Queue和Topic都存在里面,结构如下
2.ACTIVEMQ_ACKS
3.ACTIVEMQ_LOCK
表ACTIVEMQ_LOCK在集群环境下才有用,只有一个Broker可以获取消息,称为Master Broker,其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master
Broker
- 如果新建数据库ok,上述配置ok,代码运行ok,3张表会自动生成
- 如果表没生成,可能需要自己创建
代码示例如下:
-- auto-generated definition
create table ACTIVEMQ_ACKS
(
CONTAINER varchar(250) not null comment '消息的Destination',
SUB_DEST varchar(250) null comment '如果使用的是Static集群,这个字段会有集群其他系统的信息',
CLIENT_ID varchar(250) not null comment '每个订阅者都必须有一个唯一的客户端ID用以区分',
SUB_NAME varchar(250) not null comment '订阅者名称',
SELECTOR varchar(250) null comment '选择器,可以选择只消费满足条件的消息,条件可以用自定义属性实现,可支持多属性AND和OR操作',
LAST_ACKED_ID bigint null comment '记录消费过消息的ID',
PRIORITY bigint default 5 not null comment '优先级,默认5',
XID varchar(250) null,
primary key (CONTAINER, CLIENT_ID, SUB_NAME, PRIORITY)
)
comment '用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存';
create index ACTIVEMQ_ACKS_XIDX
on ACTIVEMQ_ACKS (XID);
-- auto-generated definition
create table ACTIVEMQ_LOCK
(
ID bigint not null
primary key,
TIME bigint null,
BROKER_NAME varchar(250) null
);
-- auto-generated definition
create table ACTIVEMQ_MSGS
(
ID bigint not null
primary key,
CONTAINER varchar(250) not null,
MSGID_PROD varchar(250) null,
MSGID_SEQ bigint null,
EXPIRATION bigint null,
MSG blob null,
PRIORITY bigint null,
XID varchar(250) null
);
create index ACTIVEMQ_MSGS_CIDX
on ACTIVEMQ_MSGS (CONTAINER);
create index ACTIVEMQ_MSGS_EIDX
on ACTIVEMQ_MSGS (EXPIRATION);
create index ACTIVEMQ_MSGS_MIDX
on ACTIVEMQ_MSGS (MSGID_PROD, MSGID_SEQ);
create index ACTIVEMQ_MSGS_PIDX
on ACTIVEMQ_MSGS (PRIORITY);
create index ACTIVEMQ_MSGS_XIDX
on ACTIVEMQ_MSGS (XID);
- 启动遇到的问题,可参考如下解决:
9.6.2 验证和数据表变化
代码很简单,就是在原来代码中进行运行即可。我们主要是关注和数据库的变化。
一旦运行生产code
数据库变化:
点到点(queue)
在点对点类型中
当DeliveryMode
设置为NON_PERSISTENCE
时,消息被保存在内存
中
当DeliveryMode
设置为PERSISTENCE
时,消息保存在broker
的相应的文件或者数据库中。
而且点对点类型中消息一旦被Consumer消费,就从数据中删除
消费前的消息,会被存放到数据库
上面的消息被消费后被MQ自动删除
发布/订阅(topic)
设置了持久订阅数据库里面会保存订阅者的信息
我们先启动一下持久化topic 的消费者。看到ACTIVEMQ_ACKS 数据表多了一条消息。
ACTIVEMQ_ACKS 数据表,多了一个消费者的身份信息。一条记录代表:一个持久化topic 消费者。
详细说明:
ACTIVEMQ_ACKS表中的LAST_ACKED_ID记录了CLIENT_ID最后签收的一条消息
而LAST_ACKED_ID和ACTIVEMQ_MSGS的ID字段是外键关联关系,这样就可以实现,Topic的消息保存到ACTIVEMQ_MSGS表内,还能根据ACTIVEMQ_ACKS表中的持久订阅者查到该订阅者上次收到的最后一条消息是什么
值得注意的是,Topic内的消息是不会被删除的,而Queue的消息在被删除后,会在数据库中被删除,如果需要保存Queue,应该使用其他方案解决
9.6.3 小总结
如果是queue
在没有消费者消费的情况下会将消息保存到activemq_msgs表中,只要有任意一个消费者消费了,就会删除消费过的消息
如果是topic,
一般是先启动消费订阅者然后再生产的情况下会将持久订阅者永久保存到qctivemq_acks
,而消息则永久保存在activemq_msgs
,
在acks表中的订阅者有一个last_ack_id对应了activemq_msgs中的id字段
,这样就知道订阅者最后收到的消息是哪一条。
9.6.4 开发有坑
在配置关系型数据库作为ActiveMQ的持久化存储方案时,有坑
-
数据库jar包
注意把对应版本的数据库jar或者你自己使用的非自带的数据库连接池jar包 -
createTablesOnStartup属性
默认为true,每次启动activemq都会自动创建表,在第一次启动后,应改为false,避免不必要的损失。 -
java.lang.IllegalStateException: LifecycleProcessor not initialized
确认计算机主机名名称没有下划线
9.7 JDBC Message store with ActiveMQ Journal
9.7.1 是什么
9.7.2 说明
这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库读库。
ActiveMQ Journal,使用高速缓存写入技术,大大提高了性能。
当消费者的速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。
举个例子:
生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上消息,那么这个时候只需要同步剩余的10%的消息到DB。如果消费者的速度很慢,这个时候journal文件可以使消息以批量方式写到DB。
为了高性能,这种方式使用日志文件存储+数据库存储。先将消息持久到日志文件,等待一段时间再将未消费的消息持久到数据库。该方式要比JDBC 性能要高。
9.7.3 配置
还是在activemq.xml配置文件中进行修改。
代码示例如下:
<persistenceFactory>
<journalPersistenceAdapterFactory
journalLogFiles="4"
journalLogFileSize="32768"
useJournal="true"
useQuickJournal="true"
dataSource="#mysql-ds"
dataDirectory="activemq-data"/>
</persistenceFactory>
总结:
以前是实时写入mysql,在使用了journal后,数据会被journal处理,如果在一定时间内journal处理(消费)完了,就不写入mysql,如果没消费完,就写入mysql,起到一个缓存的作用。
9.8 总结
持久化消息主要指的是:
MQ所在服务器宕机了消息不会丢试的机制。
持久化机制演变的过程:
从最初的 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方式的首选数据复制方案。 |
10 ActiveMQ 多节点集群
10.1 面试题
引入消息中间件后如何保证其高可用?
10.2 是什么?
基于zookeeper和LevelDB搭建ActiveMQ集群。集群仅提供主备方式的高可用集群功能,避免单点故障。
10.3 zookeeper+replicated-leveldb-store的主从集群
10.3.1 三种集群方式对比
官网:http://activemq.apache.org/masterslave
LevelDB,5.6版本之后推出了LecelDB的持久化引擎,它使用了自定义的索引代替常用的BTree索引,其持久化性能高于KahaDB,虽然默认的持久化方式还是KahaDB,但是LevelDB可能会是趋势。
在5.9版本还提供了基于LevelDB和Zookeeper的数据复制方式,作为Master-Slave方式的首选数据复制方案。
本次案例采用ZK+Replicated LevelDB Store。
10.3.2 ShareFileSystem
10.3.3 是什么?
从ActiveMQ5.9开始,ActiveMQ的集群实现方式取消了传统的Masster-Slave方式。增加了基于Zookeeper+LevelDB的Master-Slave实现方式,从5.9版本后也是官网的推荐。
基于Zookeeper和LevelDB搭建ActiveMQ集群,集群仅提供主备方式的高可用集群功能,避免单点故障。
官网:http://activemq.apache.org/replicated-leveldb-store
10.3.4 官网集群原理图
官网:http://activemq.apache.org/replicated-leveldb-store
说明:
使用Zookeeper集群注册所有的ActiveMQ Broker但只有其中一个Broker可以提供服务,它将被视为Master,其他的Broker处于待机状态被视为Slave
。
如果Master因故障而不能提供服务,Zookeeper会从Slave中选举出一个Broker充当Master。Slave连接Master并同步他们的存储状态
,Slave不接受客户端连接。所有的存储操作都将被复制到连接至Maste的Slaves。
如果Master宕机得到了最新更新的Slave会变成Master
。故障节点在恢复后会重新加入到集群中并连接Master进入Slave模式。
所有需要同步的消息操作都将等待存储状态被复制到其他法定节点的操作完成才能完成。
所以,如给你配置了replicas=3,name法定大小是(3/2)+1 = 2。Master将会存储更新然后等待(2-1)=1个Slave存储和更新完成,才汇报success,至于为什么是2-1,阳哥的zookeeper讲解过自行复习。
有一个node要作为观察者存在。当一个新的Master被选中,你需要至少保障一个法定node在线以能够找到拥有最新状态的node,这个node才可以成为新的Master。
因此,推荐运行至少3个replica nodes以防止一个node失败后服务中断。
10.3.5 部署规划和步骤
注意我是使用的三台虚拟主机进行配置集群的,即相同操作要进行三次,在三台虚拟主机上。
- 大纲
- 部署规划表
主机 | ZK集群端口 | AMQ集群bind端口 | AMQTCP端口 | 管理控制端口 |
---|---|---|---|---|
192.168.249.103 (wymq-server) | 2181 | bind=“tcp://0.0.0.0:63631” | 616166 | 8161 |
192.168.249.104 (wymq-server01) | 2181 | bind=“tcp://0.0.0.0:63632” | 61617 | 8162 |
192.168.249.105 (wymq-server02) | 2181 | bind=“tcp://0.0.0.0:63633” | 61618 | 8163 |
因为老师案例演示是在一台Linux服务器上运行3个zookeeper,所以他将zk集群的端口改变,是为了模拟3台机器上的zookeeper
。(也叫作伪集群)
- 就是将当初opt目录解压的activemq文件夹复制到我们新创建的目录下并改名,到时候我们直接在这个地方进行启动。
- 修改管理控制台端口
就是conf目录下的jetty.xml中修改网址登陆端口,即后台管理端口。
- hostname名字映射(如果不映射只需要把mq配置文件的hostname改成当前主机ip)
就是本来我们输入要写网址,但是映射之后就不用写网址了。
- ActiveMQ集群配置
在activemq.xml中讲上面配置的名字修改到borkerName。
持久化配置:
第一台机器
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63631"
zkAddress="192.168.249.103:2181,192.168.249.104:2181,192.168.249.105:2181"
hostname="wymq-server"
sync="local_disk"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
第二台机器
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63632"
zkAddress="192.168.249.103:2181,192.168.249.104:2181,192.168.249.105:2181"
hostname="wymq-server01"
sync="local_disk"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
第三台机器
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:63633"
zkAddress="192.168.249.103:2181,192.168.249.104:2181,192.168.249.105:2181"
hostname="wymq-server02"
sync="local_disk"
zkPath="/activemq/leveldb-stores"/>
</persistenceAdapter>
hostname 需要修改,因为机器不一样,此外zk集群端口不用修改,
因为在3台机器上互不影响
修改个节点的消息端口
如下图,就是连接端口改为61616,61617,61618。
- 先启动zk在启动mq
在启动mq的时候有时候会遇到以下问题。
有时候出现未找到命令,使用
./
开头
- zk集群节点状态说明
在zk集群下的节点即可看见有activemq。通过获取值来判断谁是主,谁是从。
从下面的图片中看出08是master
10.3.6 集群可用性测试
代码修改很简单:
//测试 zookeeper+replicated-leveldb-store的主从集群
public static final String ACTIVEMQ_URL = "failover:(tcp://192.168.249.103:61616,tcp://192.168.249.104:61617,tcp://192.168.249.105:61618)?randomize=false";
public static final String QUEUE_NAME = "queue-cluster";
消费者和生产者都进行修改,然后启动进行测试
宕机一台:
自动切换到8162这个端口,即另一台zk服务器上了。
然后进行测试,一切正常。
11 高级特性及大厂面试题
11.1 异步投递
11.1.1 是什么
官网:http://activemq.apache.org/async-sends
说明:
对于一个Slow Consumer,使用同步发送消息可能出现Producer堵塞的情况,慢消费者适合使用异步发送。
ActiveMQ支持同步,异步两种发送的模式将消息发送到broker,模式的选择对发送延时有巨大的影响。producer能达到怎么样的产出率(产出率=发送数据总量/时间)主要受发送延时的影响,使用异步发送可以显著提高发送的性能。
ActiveMQ默认使用异步发送的模式
:除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息,这两种情况都是同步发送的。
如果你没有使用事务且发送的是持久化的消息
,每一次发送都是同步发送的且会阻塞producer知道broker返回一个确认,表示消息已经被安全的持久化到磁盘。确认机制提供了消息安全的保障,但同时会阻塞客户端带来了很大的延时。
很多高性能的应用,允许在失败的情况下有少量的数据丢失
。如果你的应用满足这个特点,你可以使用异步发送来提高生产率,即使发送的是持久化的消息。
异步发送
它可以最大化producer端的发送效率。我们通常在发送消息量比较密集的情况下使用异步发送
,它可以很大的提升Producer性能;不过这也带来了额外的问题,
就是需要消耗更多的Client端内存同时也会导致broker端性能消耗增加;
此外它不能有效的确保消息的发送成功。在userAsyncSend=true的情况下客户端需要容忍消息丢失的可能。
11.1.2 官网配置
/**测试异步投递 并且能够找出 失败的消息
* @author wystart
* @create 2022-08-24 0:23
*/
public class JmsProduce_Async {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 设置开启异步投递
activeMQConnectionFactory.setUseAsyncSend(true);
//2 通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3 创建会话session
//两个参数,第一个叫事务/第二个叫签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4 创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5 创建消息的生产者 使用更加细粒度化的消息生产者 ----ActiveMQMessageProducer
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue);
//设置持久化,发送消息者要用 DeliveryMode.PERSISTENT 模式且在连接之前设定
activeMQMessageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
//6 通过使用messageProducer生产3条消息发送到MQ的队列里面
// for (int i = 1; i <= 3 ; i++) {
for (int i = 1; i <= 3 ; i++) { //测试消费者的3中案例
//7 创建消息,好比学生按照老师的要求写好的面试题消息
TextMessage textMessage = session.createTextMessage("msg---" + i);//理解为一个字符串
// 给每一条消息设置一个id,在回调函数中通过有无id来判断是否发送成功
textMessage.setJMSMessageID(UUID.randomUUID().toString() + "....测试");
//获得id
String msgId = textMessage.getJMSMessageID();
// 8 通过activeMQMessageProducer 发送给 MQ
// 回调函数 AsyncCallback 判断哪些消息发送失败 和成功
activeMQMessageProducer.send(textMessage, new AsyncCallback() {
@Override
public void onSuccess() {
System.out.println(msgId + "has been ok send");
}
@Override
public void onException(JMSException exception) {
System.out.println(msgId + "fail to send to mq");
}
});
// //测试消息体的5大格式之map格式
// MapMessage mapMessage = session.createMapMessage();
// mapMessage.setString("k1","mapmessage --v1");
// messageProducer.send(mapMessage);
}
//9 关闭资源
activeMQMessageProducer.close();
session.close();
connection.close();
System.out.println("********消息发布到MQ完成");
}
}
JmsProduce_AsyncSend:
异步发送丢失消息的场景是:生产者设置userAsyncSend=true,使用producer.send(msg)持续发送消息。
如果消息不阻塞,生产者会认为所有send的消息均被成功发送至MQ。
如果MQ突然宕机,此时生产者端内存中尚未被发送至MQ的消息都会丢失
。
所以,正确的异步发送方法是需要接收回调的
。
同步发送和异步发送的区别就在此,
同步发送等send不阻塞了就表示一定发送成功了,
异步发送需要客户端回执并由客户端再判断一次是否发送成功
11.2 延迟投递和定时投递
11.2.1 介绍
官网:http://activemq.apache.org/delay-and-schedule-message-delivery.html
11.2.2 修改配置文件并重启
要在activemq.xml中配置schedulerSupport
属性为true
11.2.3 代码实现
public class JmsProduce_DelayAndSchedule {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageProducer messageProducer = session.createProducer(queue);
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
long delay = 3 * 1000; // 延迟投递的时间 延迟3秒钟
long period = 4 * 1000; // 每次投递的时间间隔 时间间隔4秒钟
int repeat = 5; // 投递的次数 重复 5 次
for (int i = 1; i <= 3 ; i++) {
TextMessage textMessage = session.createTextMessage("delay---" + i);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,delay);
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,period);
textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,repeat);
messageProducer.send(textMessage);
}
messageProducer.close();
session.close();
connection.close();
System.out.println("********消息发布到MQ完成");
}
}
public class JmsConsumer_DelayAndSchedule {
public static final String ACTIVEMQ_URL = "tcp://192.168.249.103:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1 创建连接工厂,按照给定的url地址,采用默认的用户名和密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2 通过连接工厂,获得连接connection并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话
//两个参数,第一个叫事务/第二个叫签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4 创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
// 使用匿名内部类方式
messageConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (null != message && message instanceof TextMessage){
//强转
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者接收到消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
//防止执行太快,保证控制台不灭
//因为消费者进行连接到消息中间件,会有一系列验证,如果不写Syetem.in.read();程序会马上
//执行完,但是消费者不会接收到任何消息
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
测试
11.3 消息消费的重试机制
11.3.1 介绍
面试题:
-
具体哪些情况会引发消息重发
1:Client用了transactions且再session中调用了rollback
2:Client用了transactions且再调用commit之前关闭或者没有commit
3:Client再CLIENT_ACKNOWLEDGE的传递模式下,session中调用了recover -
请说说消息重发时间间隔和重发次数
间隔:1
次数:6
每秒发6次
有毒消息Poison ACK
一个消息被redelivedred超过默认的最大重发次数(默认6次)时,消费的回个MQ发一个“poison ack”表示这个消息有毒,告诉broker不要再发了。这个时候broker会把这个消息放到DLQ(私信队列)。
官网:http://activemq.apache.org/redelivery-policy
属性说明:
11.3.2 代码实现
以入门案例中的队列中的消费者和生产者,将消费者中的创建会话改为true,加入事务,但是我们人为干预不提交。进行1+6次,默认重试6次,进行消息重复消费,当到第8次的时候发现无法进行消息消费,即证明消息重试次数为6次。消息最后进入死信队列。
如下图
我们还可以进行自定义重试次数。代码只需在消费者中加入几行代码。
以后整合spring的时候,我们只需在application.xml中进行bean 创建和引用即可,相关配置见下图:
11.4 死信队列
11.5 消息不被重复消费,幂等性
总结
学习三板斧:
理论
实战
总结
学习过程中遇到的问题相关解决参考:
Activemq服务无法启动的报错及原因总结
zookeeper集群报错:Error: JAVA_HOME is not set and java could not be found in PATH.
zookeeper集群报错:Error: JAVA_HOME is not set and java could not be found in PATH.
zookeeper集群报错:Error: JAVA_HOME is not set and java could not be found in PATH.
linux为某个文件赋予所有权限