目录
1、介绍
MQ是消息中间件,是一种在分布式系统中应用程序借以传递消息的媒介,常用的有ActiveMQ,RabbitMQ,kafka。ActiveMQ是Apache下的开源项目,完全支持JMS1.1和J2EE1.4规范的JMS Provider实现。
MQ的产生背景
系统之间直接调用存在的问题?
微服务架构后,链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(或子模块),比如模块A调用模块B,模块B调用模块C,模块C调用模块D。但在大型分布式应用中,系统间的RPC交互繁杂,一个功能背后要调用上百个接口并非不可能,从单机架构过渡到分布式微服务架构的通例。这些架构会有哪些问题?
解决的实际工作中存在的问题
1、系统之间接口耦合比较严重
每新增一个下游功能,都要对上游的相关接口进行改造;
举个例子:如果系统A要发送数据给系统B和系统C,发送给每个系统的数据可能有差异,因此系统A对要发送给每个系统的数据进行了组装,然后逐一发送;
当代码上线后又新增了一个需求:把数据也发送给D,新上了一个D系统也要接受A系统的数据,此时就需要修改A系统,让他感知到D系统的存在,同时把数据处理好再给D。在这个过程你会看到,每接入一个下游系统,都要对系统A进行代码改造,开发联调的效率很低。其整体架构如下图:
2、面对大流量并发时,容易被冲垮
每个接口模块的吞吐能力是有限的,这个上限能力如果是堤坝,当大流量(洪水)来临时,容易被冲垮。
举个例子【秒杀业务】:上游系统发起下单购买操作,就是下单一个操作,很快就完成。然而,下游系统要完成秒杀业务后面的所有逻辑(读取订单,库存检查,库存冻结,余额检查,余额冻结,订单生产,余额扣减,库存减少,生成流水,余额解冻,库存解冻)。
3、等待同步存在性能问题
RPC接口上基本都是同步调用,整体的服务性能遵循“木桶理论”,即整体系统的耗时取决于链路中最慢的那个接口。比如A调用B/C/D都是50ms,但此时B又调用了B1,花费2000ms,那么直接就拖累了整个服务性能。
根据上述的几个问题,在设计系统时可以明确要达到的目标:
- 1,要做到系统解耦,当新的模块接进来时,可以做到代码改动最小;能够解耦
- 2,设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮;能够削峰
- 3,强弱依赖梳理能将非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步
MQ的主要作用
异步
:调用者无需等待。解耦
:解决了系统之间耦合调用的问题。消峰
:抵御洪峰流量,保护了主业务。
MQ的定义
面向消息的中间件(message-oriented middleware)MOM能够很好的解决以上问题。是指利用高效可靠的消息传递机制与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型在分布式环境下提供应用解耦,弹性伸缩,冗余存储、流量削峰,异步通信,数据同步等功能。
大致的过程是这样的:发送者把消息发送给消息服务器,消息服务器将消息存放在若干队列/主题topic中,在合适的时候,消息服务器回将消息转发给接受者。在这个过程中,发送和接收是异步的,也就是发送无需等待,而且发送者和接受者的生命周期也没有必然的关系;尤其在发布pub/订阅sub模式下,也可以完成一对多的通信,即让一个消息有多个接受者。
MQ的特点
1、采用异步处理方式
消息发送者可以发送一个消息而无须等待响应。消息发送者将消息发送到一条虚拟的通道(主题或者队列)上;消息接收者则订阅或者监听该通道。一条消息可能最终转发给一个或者多个消息接收者,这些消息接收者都无需对消息发送者做出同步回应。整个过程都是异步的。
- 案例:
也就是说,一个系统跟另一个系统之间进行通信的时候,假如系统A希望发送一个消息给系统B,让他去处理。但是系统A不关注系统B到底怎么处理或者有没有处理好,所以系统A把消息发送给MQ,然后就不管这条消息的“死活了”,接着系统B从MQ里面消费出来处理即可。至于怎么处理,是否处理完毕,什么时候处理,都是系统B的事儿,与系统A无关。
2、应用系统之间解耦合
- 发送者和接受者不必了解对方,只需要确认消息。
- 发送者和接受者不必同时在线。
3、整体架构
关于ActiveMQ控制台
- 登陆地址:LINUX IP地址:8161/admin
- 默认的登录名和密码:admin
- 提供服务的端口:61616
- 控制台地址:8161
2、ActiveMQ - API
- 基本pom.xml
<!-- activemq 所需要的jar 包-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
<!-- activemq 和 spring 整合的基础包 -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
JMS 编码总体概述
- Destination是目的地。下面拿jvm和mq,做个对比。目的地,我们可以理解为是数据存储的地方。
- Destination分为两种:队列和主题
01、Queue队列
生产者基本代码的实现
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("msg --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
名称 | 注释 | 补充 |
---|---|---|
Number Of Pending Messages | 等待消费的消息 | 这个是未出队列的数量,公式=总接收数-总出队列数。 |
Number Of Consumers | 消费者数量 | 消费者端的消费者数量。 |
Messages Enqueued | 进队消息数 | 进队列的总消息量,包括出队列的。这个数只增不减 |
Messages Dequeued | 出队消息数 | 可以理解为是消费者消费掉的数量。 |
总结:
- 当有一个消息进入这个队列时,等待消费的消息是1,进入队列的消息是1。
- 当消息消费后,等待消费的消息是0,进入队列的消息是1,出队列的消息是1
- 当再来一条消息时,等待消费的消息是1,进入队列的消息就是2。
消费者基本代码的实现
- 两种方式
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消费者
MessageConsumer consumer = session.createConsumer(queue);
- 方式1:
同步阻塞方式 ---> receive()
- 订阅者或接收者调用 consumer 的receive()方法来接受消息,receive()方法在能够接收到消息之前(或超时之前)将一直阻塞
while (true){
// reveive() 一直等待接收消息,在能够接收到消息之前将一直阻塞。 是同步阻塞方式 。和socket的accept方法类似的。
// reveive(Long time) : 等待n毫秒之后还没有收到消息,就是结束阻塞。
// 因为消息发送者是 TextMessage,所以消息接受者也要是TextMessage
TextMessage textMessage = (TextMessage) consumer.receive(4000L);
if (textMessage != null){
System.out.println("消费者接收到消息 ---> " + textMessage.getText());
}else {
break;
}
}
//关闭资源
consumer.close();
session.close();
connection.close();
- 方式2:
异步非阻塞方式 ---> 监听器onMessage()
通过监听的方式来消费消息 - 订阅者或接受着通过 consumer 的 setMessageListener(new MessageListener()) 注册一个消息监听器。当消息到达之后,系统自动调用监听器MessageListener的onMessage(Message message) 方法
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
//instanceof 测试它所指向的对象是否是TextMessage类
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();
}
}
- 输出
消费者接收到消息 ---> msg --> 0
消费者接收到消息 ---> msg --> 1
消费者接收到消息 ---> msg --> 2
消费者的3大情况
-
情况1:只启动消费者1。
结果:消费者1会消费所有的数据。
-
情况2:先启动消费者1,再启动消费者2。
结果:消费者1消费所有的数据。消费者2不会消费到消息。
-
情况3:生产者发布6条消息,在此之前已经启动了消费者1和消费者2。
结果:消费者1和消费者2平摊了消息。各自消费3条消息。
疑问:怎么去将消费者1和消费者2不平均分摊呢?而是按照各自的消费能力去消费。我觉得,现在activemq就是这样的机制。
总结
两种消费方式
- 同步阻塞方式(receive)
订阅者或接收者抵用MessageConsumer的receive()方法来接收消息,receive方法在能接收到消息之前(或超时之前)将一直阻塞。
- 异步非阻塞方式(监听器onMessage())
订阅者或接收者通过MessageConsumer的setMessageListener(MessageListenerlistener)注册一个消息监听器,当消息到达之后,系统会自动调用监听器MessageListener的onMessage(Messagemessage)方法。
02、Topic 主题
介绍
在发布订阅消息传递域中,目的地被称为主题(topic)
发布/订阅消息传递域的特点如下:
- (1)生产者将消息发布到topic中,每个消息可以有多个消费者,属于
1:N
的关系; - (2)生产者和消费者之间有
时间上
的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息
。 - (3)生产者生产时,topic不保存消息它是无状态的不落地,假如无人订阅就去生产,那就是一条废消息,所以,一般先启动消费者再启动生产者。
默认情况下如上所述,但是JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。一句话,好比我们的微信公众号订阅
注意:一定是先订阅,再发送消息
不然发送的消息就是费消息
代码演示:启动3个消费者后。启动一个生产者
消费者基本代码的实现
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String TOPIC_NAME = "wula";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是3号消费者");
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Topic topic = session.createTopic(TOPIC_NAME); <------这行代码变了
//5、创建消费者
MessageConsumer consumer = session.createConsumer(topic);
/*
通过监听的方式来消费消息
*/
//此处用 lambda 表达式
consumer.setMessageListener(message -> {
if (message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者接收到Topic消息 ---> " + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
// 防止下面的关闭资源还没等消费提前关闭
System.in.read();
//关闭资源
consumer.close();
session.close();
connection.close();
}
}
生产者基本代码的实现
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String TOPIC_NAME = "wula";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Topic topic = session.createTopic(TOPIC_NAME); <------这行代码变了
//5、创建主题的生产者
MessageProducer producer = session.createProducer(topic);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("TOPIC_NAME --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***TOPIC_NAME消息发送到MQ完成***");
}
}
- 此时消费者控制台打印输出
我是1号消费者
消费者接收到Topic消息 ---> TOPIC_NAME --> 0
消费者接收到Topic消息 ---> TOPIC_NAME --> 1
消费者接收到Topic消息 ---> TOPIC_NAME --> 2
我是2号消费者
消费者接收到Topic消息 ---> TOPIC_NAME --> 0
消费者接收到Topic消息 ---> TOPIC_NAME --> 1
消费者接收到Topic消息 ---> TOPIC_NAME --> 2
我是3号消费者
消费者接收到Topic消息 ---> TOPIC_NAME --> 0
消费者接收到Topic消息 ---> TOPIC_NAME --> 1
消费者接收到Topic消息 ---> TOPIC_NAME --> 2
先启动消费者3个,再启动1个生产者。可以看到。生产者虽然只生产3条消息。但是3个消费者每个都消费了这3条消息。消息入列为3条,消息出列为9条
消息出列 = 消息入列 * 消费者人数
03、Topic和Queue对比
比较类型 | Topic | Queue |
---|---|---|
工作模式 | 订阅-发布"模式,如果当前没有订阅者,消息将会被云弃。如果有多个订阅者,那么这些订阅者都会收到消息 | “负载均衡"模式,如果当前没有消费者,消息也不会丢弃;如果有多个消费者,那么—条消息也只会发送给其中—个消费者,并且要求消费者ack消息 |
有无状态 | 无状态 | Queue数据默认会在mq服务器上以文件形式保存,比如Active MQ —般保存在 $SAMQ_HOME\data\kr-store\data 下面。也可以配置成BD存储 |
传递完整性 | 如果没有订阅者,消息会被丢弃 | 消息不会丢弃 |
处理效率 | 由于消息要按照订阅者的数量进行复制,所以处理性能会随着订阅者的增加而明显降低,并且还要结合不同消息协议自身的性能差异 | 由于—条消息只发送给—个消费者,所以就算消费者再多,性能也不会有明显降低。当然不同消息协议的具体性能也是有差异的 |
3、JMS规范
什么是Java消息服务?
Java消息服务指的是两个应用程序之间进行异步通信的API,它为标准协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发。在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。
四种消息队列的详细比较
特性 | ActiveMQ | RabbitMQ | Kafka | RocketMQ |
---|---|---|---|---|
PRODUCER-CUMSUMER | 支持 | 支持 | 支持 | 支持 |
PUBLISH-SUBSCRIBE | 支持 | 支持 | 支持 | 支持 |
REQUEST-REPLY | 支持 | 支持 | - | 支持 |
API完备性 | 高 | 高 | 高 | 低(静态配置) |
多语言支持 | 支持,Java优先 | 语言无关 | 支持,Java优先 | 支持 |
单机吞吐量 | 万级 | 万级 | 十万级 | 单机万级 |
消息延迟 | - | 微秒级 | 毫秒级 | - |
可用性 | 高(主从) | 高(主从) | 非常高(分布式) | 高 |
消息丢失 | - | 低 | 理论上不会丢失 | - |
消息重复 | - | 可控制 | 理论上会有重复 | - |
文档的完备性 | 高 | 高 | 高 | 中 |
提供快速入门 | 有 | 有 | 有 | 无 |
首次部署难度 | - | 低 | 中 | 高 |
JMS组成的4大元素
01、消息头
常用:
属性 | 注释 |
---|---|
JMSDestination | 消息目的地 |
JMSDeliveryMode | 消息持久化模式 |
JMSExpiration | 消息过期时间 |
JMSPriority | 消息的优先级 |
JMSMessageID | 消息的唯一标识符。后面我们会介绍如何解决幂等性。 |
//介绍消息头
public class JmsProduce_Message {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String TOPIC_NAME = "wula";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Topic topic = session.createTopic(TOPIC_NAME);
//5、创建主题的生产者
MessageProducer producer = session.createProducer(topic);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("TOPIC_NAME --> " + i);
/*
这里可以指定每个消息的目的地
*/
textMessage.setJMSDestination(topic);
/*
持久模式和非持久模式。
一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递。
一条非持久的消息:最多会传递一次,这意味着服务器出现故障,该消息将会永远丢失。
*/
textMessage.setJMSDeliveryMode(1 );
/*
可以设置消息在一定时间后过期,默认是永不过期。
消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。
如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期。
如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。
*/
textMessage.setJMSExpiration(1000);
/* 消息优先级,从0-9十个级别,0-4是普通消息5-9是加急消息。
JMS不要求MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。
*/
textMessage.setJMSPriority(6);
/*
唯一标识每个消息的标识。MQ会给我们默认生成一个,我们也可以自己指定。
*/
textMessage.setJMSMessageID("ABCD");
// 上面有些属性在send方法里也能设置
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***TOPIC_NAME消息发送到MQ完成***");
}
}
02、消息体
封装具体的消息数据 发送和接收的消息体类型必须一致对应
- 5种
注解 | |
---|---|
TextMessage | 普通字符串消息,包含一个string |
MapMessage | 一个Map类型的消息,key为string类型,而值为Java的基本类型 |
BytesMessage | 二进制数组消息,包含一个byte[] |
StreamMessage | Java数据流消息,用标准流操作来顺序的填充和读取。 |
ObjectMessage | 对象消息,包含一个可序列化的Java对象 |
- 消息提供者
public class JmsProduce_Message {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建主题的生产者
MessageProducer producer = session.createProducer(queue);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("textMessage--> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
// 发送MapMessage 消息体。set方法: 添加,get方式:获取
MapMessage mapMessage = session.createMapMessage();
mapMessage.setString("k1","MapMessage --> " + "v" + i);
producer.send(mapMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
- 消息消费者
public class JmsConsumer_Message {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue= session.createQueue(QUEUE_NAME);
//5、创建消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
通过监听的方式来消费消息
*/
//接收 TextMessage 类型消息
consumer.setMessageListener(message -> {
if (message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者接收到消息 ---> " + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
//接收 MapMessage 类型消息
if (message != null && message instanceof MapMessage){
MapMessage mapMessage = (MapMessage) message;
try {
System.out.println("消费者接收到消息 ---> " + mapMessage.getString("k1"));
} catch (JMSException e) {
e.printStackTrace();
}
}
});
// 防止下面的关闭资源还没等消费提前关闭
System.in.read();
//关闭资源
consumer.close();
session.close();
connection.close();
}
}
- 输出
消费者接收到消息 ---> textMessage--> 0
消费者接收到消息 ---> MapMessage --> v0
消费者接收到消息 ---> textMessage--> 1
消费者接收到消息 ---> MapMessage --> v1
消费者接收到消息 ---> textMessage--> 2
消费者接收到消息 ---> MapMessage --> v2
03、消息属性
如果需要除消息头字段之外的值,那么可以使用消息属性。他是识别/去重/重点标注等操作,非常有用的方法。
他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器。消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。它们还用于暴露消息选择器在消息过滤时使用的数据。
下图是设置消息属性的API:
- 消息提供者
public class JmsProduce_Message {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建主题的生产者
MessageProducer producer = session.createProducer(queue);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("textMessage--> " + i);
// 调用Message的set*Property()方法,就能设置消息属性。根据value的数据类型的不同,有相应的API。
textMessage.setStringProperty("c01","vip");
textMessage.setByteProperty("wula",(byte)1);
textMessage.setBooleanProperty("Invalide",true);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
- 消息消费者
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue= session.createQueue(QUEUE_NAME);
//5、创建消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
通过监听的方式来消费消息
*/
consumer.setMessageListener(message -> {
if (message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者接收到消息 ---> " + textMessage.getText());
System.out.println("消息属性1:" + textMessage.getStringProperty("c01"));
System.out.println("消息属性2:" + textMessage.getByteProperty("wula"));
System.out.println("消息属性3:" + textMessage.getBooleanProperty("Invalide"));
} catch (JMSException e) {
e.printStackTrace();
}
}
});
// 防止下面的关闭资源还没等消费提前关闭
System.in.read();
//关闭资源
consumer.close();
session.close();
connection.close();
}
}
- 输出
消费者接收到消息 ---> textMessage--> 0
消息属性1:vip
消息属性2:1
消息属性3:true
消费者接收到消息 ---> textMessage--> 1
消息属性1:vip
消息属性2:1
消息属性3:true
消费者接收到消息 ---> textMessage--> 2
消息属性1:vip
消息属性2:1
消息属性3:true
04、消息持久化
介绍
在消息生产者将消息成功发送给MQ消息中间件之后。无论是出现任何问题,如:MQ服务器宕机、消费者掉线等。都保证(topic要之前注册过,queue不用)消息消费者,能够成功消费消息。如果消息生产者发送消息就失败了,那么消费者也不会消费到该消息。
参数设置说明
**非持久化 **:当服务器宕机,消息不存在
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
**持久化 **:当服务器宕机,消息依然存在
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
Queue
- 持久化:当生产者成功发布消息之后,MQ服务端宕机重启,消息生产者仍然能够收到该消息
- Queue默认就是持久化
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
//设置持久化
producer.setDeliveryMode(DeliveryMode.PERSISTENT); <------
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("MessageListener --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
- 非持久化:当生产者成功发布消息之后,MQ服务端宕机重启,消息生产者就收不到该消息了
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
//设置非持久化
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); <------
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("MessageListener --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
持久化Topic
- 持久化:只要消费者向MQ服务器注册过,所有生产者发布成功的消息,该消费者都能收到,不管是MQ服务器宕机还是消费者不在线。
- topic默认就是非持久化的,因为生产者生产消息时,消费者也要在线,这样消费者才能消费到消息。
注意:
一定要先运行一次消费者,等于向MQ注册,类似我们订阅了这个主题。然后再运行生产者发送消息。此时,无论消费者是否在线,都会接收到,不在线的话,下次连接的时候,会把没有接收过的消息接收下来
- 消费者1 一直运行 receive 同步阻塞方式
public class JmsConsumer_Persist {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String TOPIC_NAME = "wula";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("***zs");
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
// connection.start(); <------ 之前start的位置
connection.setClientID("zs");
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark...");
connection.start(); <------ 现在start的位置
Message message = topicSubscriber.receive();
while (message != null){
TextMessage textMessage = (TextMessage)message;
System.out.println("收到的持久化topic:" + textMessage.getText());
message = topicSubscriber.receive(); //一直运行
}
session.close();
connection.close();
}
}
- 消费者2 此消费者运行3秒后关闭 receive 同步阻塞方式
public class JmsConsumer_Persist {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String TOPIC_NAME = "wula";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("***ls");
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
// connection.start(); <------ 之前start的位置
connection.setClientID("ls");
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remark...");
connection.start(); <------ 现在start的位置
Message message = topicSubscriber.receive();
while (message != null){
TextMessage textMessage = (TextMessage)message;
System.out.println("收到的持久化topic:" + textMessage.getText());
message = topicSubscriber.receive(3000L); //运行3秒
}
session.close();
connection.close();
}
}
- 消息提供者
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
// connection.start(); <------ 之前start的位置
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Topic topic = session.createTopic(TOPIC_NAME);
//5、创建主题的生产者
MessageProducer producer = session.createProducer(topic);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
connection.start(); <------ 现在start的位置
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("TOPIC_NAME --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***TOPIC_NAME消息发送到MQ完成***");
}
}
- 消费者1输出 一直运行
***zs
收到的持久化topic:TOPIC_NAME --> 0
收到的持久化topic:TOPIC_NAME --> 1
收到的持久化topic:TOPIC_NAME --> 2
- 消费者2输出 运行3s后关闭
***ls
收到的持久化topic:TOPIC_NAME --> 0
收到的持久化topic:TOPIC_NAME --> 1
收到的持久化topic:TOPIC_NAME --> 2
假如先运行消费者1,然后查看控制台,发现此时消费者1处于活跃持久主题订阅者状态。然后关闭,此时消费者1处于离线持久主题订阅者状态。然后运行消息提供者,再运行消费者1,发现正常输出.此时控制台如下所示
05、事务
事务偏向于生产者。签收偏向于消费者
producer提交时的事务:
-
false 只要执行send,就会进入到队列中。关闭事务,那第二个牵手参数的设置需要有效
-
true 先执行send再执行commit,消息才能真正的被提交到队列中。消息需要批量处理,需要缓冲处理
-
(1) 生产者开启事务后,执行commit方法,这批消息才真正的被提交。不执行commit方法,这批消息不会提交。执行rollback方法,之前的消息会回滚掉。生产者的事务机制,要高于签收机制,当生产者开启事务,签收机制不再重要。
-
(2)消费者开启事务后,执行commit方法,这批消息才算真正的被消费。不执行commit方法,这些消息不会标记已消费,下次还会被消费。执行rollback方法,是不能回滚之前执行过的业务逻辑,但是能够回滚之前的消息,回滚后的消息,下次还会被消费。消费者利用commit和rollback方法,甚至能够违反一个消费者只能消费一次消息的原理。
-
(3)问:消费者和生产者需要同时操作事务才行吗?
答:消费者和生产者的事务,完全没有关联,各自是各自的事务。
-
生产者
public static void main(String[] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
//开启事务
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); <------
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
try {
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("MessageListener --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
session.commit(); <------提交事务
System.out.println("消息发送完成");
}catch(Exception e){
System.out.println("出现异常,消息回滚");
session.rollback();
}fiinally{
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
}
- 消费者
public class JmsConsumer_TX {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
//开启事务
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); <------
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
同步阻塞方式 receive()
*/
while (true){
TextMessage textMessage = (TextMessage) consumer.receive(4000L);
if (textMessage != null){
System.out.println("消费者接收到消息 ---> " + textMessage.getText());
}else {
break;
}
}
//关闭资源
consumer.close();
session.commit(); <------ 提交事务
session.close();
connection.close();
}
}
06、签收
签收的几种方式
- 自动签收(
Session.AUTO_ACKNOWLEDGE
):该方式是默认的。该种方式,无需我们程序做任何操作,框架会帮我们自动签收收到的消息。 - 手动签收(
Session.CLIENT_ACKNOWLEDGE
):手动签收。该种方式,需要我们手动调用Message.acknowledge(),来签收消息。如果不签收消息,该消息会被我们反复消费,只到被签收。 - 允许重复消息(
Session.DUPS_OK_ACKNOWLEDGE
):多线程或多个消费者同时消费到一个消息,因为线程不安全,可能会重复消费。该种方式很少使用到。 - 事务下的签收(
Session.SESSION_TRANSACTED
):开始事务的情况下,可以使用该方式。该种方式很少使用到。
事务和签收的关系
- 在事务性会话中,当一个事务被成功提交则消息被自动签收。如果事务回滚,则消息会被再次传送。事务优先于签收,开始事务后,签收机制不再起任何作用。
- 非事务性会话中,消息何时被确认取决于创建会话时的应答模式。
- 生产者事务开启,只有commit后才能将全部消息变为已消费。
- 事务偏向生产者,签收偏向消费者。也就是说,生产者使用事务更好点,消费者使用签收机制更好点。
非事务
下的消费者如何使用手动签收的方式
- 生产者
public class JmsProduce_TX {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String [] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//参数1:事务是否开启 参数2:签收的方式
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
// 此处可以用父接口 Destination来接收。有Queue和Topic两个子接口
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("MessageListener --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
- 消费者
- 一定要注意加上
textMessage.acknowledge();
public class JmsConsumer_TX {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
//手动签收
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); <------
// 4 创建目的地(两种 :队列/主题)。
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
同步阻塞方式 receive()
*/
while (true){
TextMessage textMessage = (TextMessage) consumer.receive(4000L);
if (textMessage != null){
System.out.println("消费者接收到消息 ---> " + textMessage.getText());
/*
设置为Session.CLIENT_ACKNOWLEDGE后,要调用该方法,标志着该消息已被签收(消费)。
如果不调用该方法,该消息的标志还是未消费,下次启动消费者或其他消费者还会收到改消息。
*/
textMessage.acknowledge(); <------
}else {
break;
}
}
//关闭资源
consumer.close();
session.close();
connection.close();
}
}
事务
下的消费者如何使用手动签收的方式事务优先于签收
- 生产者
public class JmsProduce_TX {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String [] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); <-----开启事务
// 4 创建目的地(两种 :队列/主题)。
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("MessageListener --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.commit(); <------提交事务
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
- 消费者
- 事务开启了。并且commit。acknowledge()方法可以忽略
public class JmsConsumer_TX {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
Session session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE); //手动签收 带事务
// 4 创建目的地(两种 :队列/主题)。
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
同步阻塞方式 receive()
*/
while (true){
TextMessage textMessage = (TextMessage) consumer.receive(4000L);
if (textMessage != null){
System.out.println("消费者接收到消息 ---> " + textMessage.getText());
/*
标志着该消息已被签收(消费)。
*/
textMessage.acknowledge(); <------ 事务开启了。并且commit。acknowledge()方法可以忽略
}else {
break;
}
}
//关闭资源
consumer.close();
session.commit(); <------提交事务
session.close();
connection.close();
}
}
07、JMS发布订阅小结
(1)JMS的发布订阅总结
- JMS Pub/Sub 模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic。
- 主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息。
- 主题使得消息订阅者和消息发布者保持互相独立不需要解除即可保证消息的传送
(2)非持久订阅
- 非持久订阅只有当客户端处于激活状态,也就是和MQ保持连接状态才能收发到某个主题的消息。
- 如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到。
- 一句话:先订阅注册才能接受到发布,只给订阅者发布消息。
(3)持久订阅
- 客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ的时候,会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
- 当持久订阅状态下,不能恢复或重新派送一个未签收的消息。
- 持久订阅才能恢复或重新派送一个未签收的消息。
(4)非持久和持久化订阅如何选择
- 当所有的消息必须被接收,则用持久化订阅。当消息丢失能够被容忍,则用非持久订阅。
4、ActiveMQ的broker
介绍
相当于一个ActiveMQ服务器实例。说白了,Broker其实就是实现了用代码的形式启动ActiveMQ将MQ嵌入到Java代码中,以便随时用随时启动
,在用的时候再去启动这样能节省了资源,也保证了可用性。这种方式,我们实际开发中很少采用,因为他缺少太多了东西,如:日志,数据存储等等。
启动broker时指定配置文件
启动broker时指定配置文件,可以帮助我们在一台服务器上启动多个broker。实际工作中一般一台服务器只启动一个broker。
//1、在activemq的conf文件里拷贝一份activemq.xml ---> active02.xml
[root@localhost conf]# cp activemq.xml activemq02.xml
//2、回到bin文件下查看路径
[root@localhost bin]# pwd
/usr/local/ActiveMQ/apache-activemq-5.15.9/bin
//3、启动 加上刚才的 active02.xml的路径
[root@localhost bin]# ./activemq start xbean:file:/usr/local/ActiveMQ/apache-activemq-5.15.9/conf/activemq02.xml
NFO: Loading '/usr/local/ActiveMQ/apache-activemq-5.15.9//bin/env'
INFO: Using java '/usr/local/java/jdk1.8.0_261/bin/java'
INFO: Starting - inspect logfiles specified in logging.properties and log4j.properties to get details
INFO: pidfile created : '/usr/local/ActiveMQ/apache-activemq-5.15.9//data/activemq.pid' (pid '10221')
//4、查看activemq的默认端口号61616的进程号 显示为启动时打印的 10221
[root@localhost bin]# lsof -i:61616
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 10221 root 129u IPv6 116501 0t0 TCP *:61616 (LISTEN)
嵌入式的broker启动
-
用ActiveMQ Broker作为独立的消息服务器来构建Java应用。
-
ActiveMQ也支持在vm中通信基于嵌入的broker,能够无缝的集成其他java应用
-
注意:此时的生产者和消费者的访问地址都应该改为localhost:61616
例如: public static final String ACTIVEMQ_URL = "tcp://localhost:61616";
public class EmbedBroker {
public static void main(String[] args) throws Exception {
//ActiveMQ也支持在vm中通信基于嵌入的broker
BrokerService brokerService = new BrokerService();
brokerService.setUseJmx(true);
brokerService.addConnector("tcp://localhost:61616");
brokerService.start();
}
}
5、Spring整合ActiveMQ
- xml
<dependencies>
<!-- 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>
<!--spring相关依赖包-->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>4.15</version>
</dependency>
<!-- 嵌入式activemq的broker所需要的依赖包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
<!-- spring支持jms的包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
</dependencies>
同步方式启动
同步阻塞方式 ---> receive()
- 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.wulawula.activemq"></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.31.143:61616"/>
</bean>
</property>
<property name="maxConnections" value="100"/>
</bean>
<!-- 这个是队列目的地,点对点的Queue -->
<bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
<!-- 通过构造注入Queue名 -->
<constructor-arg index="0" value="spring-activemq-queue"/>
</bean>
<!-- 这个是队列目的地, 发布订阅的主题Topic-->
<bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg index="0" value="spring-activemq-topic"/>
</bean>
<!-- Spring提供的JMS工具类,他可以进行消息发送,接收等 -->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<!-- 传入连接工厂 -->
<property name="connectionFactory" ref="connectionFactory"/>
<!-- 传入目的地 -->
<!-- 此处的ref改成 destinationQueue 就是队列形式 -->
<property name="defaultDestination" ref="destinationTopic"/>
<!-- 消息自动转换器 -->
<property name="messageConverter">
<bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
</property>
</bean>
</beans>
- 生产者
@Service
public class Spring_ActiveMQ_Produce {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//相当于: Spring_ActiveMQ_Produce produce1 = new Spring_ActiveMQ_Produce();
Spring_ActiveMQ_Produce produce = (Spring_ActiveMQ_Produce) ctx.getBean("spring_ActiveMQ_Produce");
produce.jmsTemplate.send(session -> {
TextMessage textMessage = session.createTextMessage("spring和ActiveMQ的整合case wula123 ......");
return textMessage;
});
System.out.println("******send task over******");
}
}
- 消费者
@Service
public class Spring_ActiveMQ_Consumer {
@Autowired
private JmsTemplate jmsTemplate;
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Spring_ActiveMQ_Consumer consumer = (Spring_ActiveMQ_Consumer) ctx.getBean("spring_ActiveMQ_Consumer");
String retvalue = (String) consumer.jmsTemplate.receiveAndConvert();
System.out.println("******消费者得到的消息:" + retvalue);
}
}
监听器方式
异步非阻塞方式 ---> 监听器onMessage()
- 消费者配置了自动监听,就相当于在spring后台运行,有消息就运行我们实现监听类里面的方法
在原有基础上增加如下内容
- applicatonContext.xml
<!-- 配置Jms消息监听器 -->
<bean id="defaultMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<!-- Jms连接的工厂 -->
<property name="connectionFactory" ref="connectionFactory"/>
<!-- 设置默认的监听目的地 -->
<!-- 注意此处的监听 -->
<property name="destination" ref="destinationTopic"/>
<!-- 指定自己实现了MessageListener的类 -->
<property name="messageListener" ref="myMessageListener"/>
</bean>
- 监听器类
@Component
public class MyMessageListener implements MessageListener {
@Override
public void onMessage(Message message) {
if (message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
- 输出 【使用监听器方式只需要执行
生产者
即可。】
******send task over******
spring和ActiveMQ的整合case wula123......
6、SpringBoot整合ActiveMQ
- xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<!-- SpringBoot + ActiveMQ 整合包-->
<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>
01、Queue
- application.yml
server:
port: 7777
spring:
activemq:
broker-url: tcp://192.168.31.143:61616 # 自己的MQ服务器地址
user: admin
password: admin
jms:
pub-sub-domain: false # false为Queue true为Topic
#自定义的队列名称
myQueue: boot-activemq-queue
生产者
- config
@Component
@EnableJms //开启jms适配的注解
public class ConfigBean {
//注入ActiveMQ的自定义名字
@Value("${myQueue}")
private String myQueue;
@Bean
public Queue queue(){
return new ActiveMQQueue(myQueue);
}
}
- Prouduce 下面是普通发送消息 和 间隔发送消息
@Scheduled(fixedDelay = 3000)
@Component
public class Queue_Produce {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Queue queue; //注入ConfigBean的queue
//发送消息
public void produceMsg(){
// 一参是目的地,二参是消息的内容
jmsMessagingTemplate.convertAndSend(queue,"****** :" + UUID.randomUUID().toString().substring(0,6));
}
// 演示定时任务。每3秒执行一次。
@Scheduled(fixedDelay = 3000)
public void produceMsgScheduled(){
produceMsg();
System.out.println("******produceMsgScheduled send ok");
}
}
- Main
@SpringBootApplication
@EnableScheduling //开启定时定投
public class Main_App_Produce {
public static void main(String[] args) {
SpringApplication.run(Main_App_Produce.class,args);
}
}
- 输出
******produceMsgScheduled send ok
******produceMsgScheduled send ok
******produceMsgScheduled send ok
- 测试普通发送消息
// 加载主类
@SpringBootTest(classes = Main_App_Produce.class)
// 加载spring的junit
@RunWith(SpringJUnit4ClassRunner.class)
// 加载web
@WebAppConfiguration
public class TestActiveMQ {
@Autowired
private Queue_Produce queue_produce;
@Test
public void testSend(){
queue_produce.produceMsg();
}
}
消费者
- pom.xml和application.yml文件和前面一样。唯一不同就是下面代码
@Component
public class Queue_Consumer {
@JmsListener(destination = "${myQueue}")
public void receive(TextMessage textMessage) throws JMSException {
System.out.println("******消费者收到消息:" + textMessage.getText());
}
}
- 输出
******消费者收到消息:****** :5cc98f
******消费者收到消息:****** :b9a4de
******消费者收到消息:****** :r45d5f
02、Topic
演示 两个消费者订阅同一个生产者 消费者:端口号5555 端口号5566
消费者
- application.xml
server:
# port: 5555
port: 5566
spring:
activemq:
broker-url: tcp://192.168.31.143:61616 # 自己的MQ服务器地址
user: admin
password: admin
jms:
pub-sub-domain: true # false为Queue true为Topic
#自定义的队列名称
MyTopic: boot-activemq-topic
- Consumer
@Component
public class Topic_Consumer {
@JmsListener(destination = "${MyTopic}")
public void receive(TextMessage textMessage) throws JMSException {
System.out.println("******Topic 消费者收到消息:" + textMessage.getText());
}
}
生产者
- application.yml
server:
port: 7777
spring:
activemq:
broker-url: tcp://192.168.31.143:61616 # 自己的MQ服务器地址
user: admin
password: admin
jms:
pub-sub-domain: true # false为Queue true为Topic
#自定义的队列名称
MyTopic: boot-activemq-topic
- config
@Component
@EnableJms //开启jms适配的注解
public class ConfigBean {
//注入ActiveMQ的自定义名字
@Value("${MyTopic}")
private String myTopic;
@Bean
public Topic topic(){
return new ActiveMQTopic(myTopic);
}
}
- Produce
@Component
public class Topic_Produce {
@Autowired
private JmsMessagingTemplate jmsMessagingTemplate;
@Autowired
private Topic topic; //注入ConfigBean的topic
//发送消息
// 演示定时任务。每3秒执行一次。
@Scheduled(fixedDelay = 3000)
public void produceTopic(){
// 一参是目的地,二参是消息的内容
jmsMessagingTemplate.convertAndSend(topic,"***主题消息*** :" + UUID.randomUUID().toString().substring(0,6));
System.out.println("***produceTopic***");
}
}
输出
- 生产者
***produceTopic***
***produceTopic***
***produceTopic***
- 消费者1
******Topic 消费者收到消息:***主题消息*** :0a1cd8
******Topic 消费者收到消息:***主题消息*** :43ffe9
******Topic 消费者收到消息:***主题消息*** :033761
- 消费者2
******Topic 消费者收到消息:***主题消息*** :0a1cd8
******Topic 消费者收到消息:***主题消息*** :43ffe9
******Topic 消费者收到消息:***主题消息*** :033761
7、ActiveMQ的传输协议
01、总览
ActiveMQ支持的client-broker通讯协议有:TVP、NIO、UDP、SSL、Http(s)、VM。其中配置Transport Connector的文件在ActiveMQ安装目录的conf/activemq.xml中的标签之内。
- vim /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>
在上文给出的配置信息中,URI描述信息的头部都是采用协议名称:例如
- 描述amqp协议的监听端口时,采用的URI描述格式为“amqp://······”;
- 描述Stomp协议的监听端口时,采用URI描述格式为“stomp://······”;
- 唯独在进行openwire协议描述时,URI头却采用的“tcp://······”。这是因为ActiveMQ中默认的消息协议就是openwire
ActiveMQ支持的网络协议
协议 | 描述 |
---|---|
TCP | 默认的协议,性能相对可以 |
NIO | 基于TCP协议之上的,进行了扩展和优化,具有更好的扩展性 |
UDP | 性能比TCP更好,但是不具有可靠性 |
SSL | 安全链接 |
HTTP(S) | 基于HTTP或者HTTPS |
VM | VM本身不是协议,当客户端和代理在同一个Java虚拟机(VM)中运行时,他们之间需要通信,但不想占用网络通道,而是直接通信,可以使用该方式 |
TCP
- 1.这是默认的Broker配置,TCP的Client监听端口61616
- 2.在网络传输数据前,必须要先序列化数据,消息是通过一个叫wire protocol的来序列化成字节流。
- 3.TCP连接的URI形式如:tcp://HostName:port?key=value&key=value,后面的参数是可选的。
- 4.TCP传输的的优点:
- TCP协议传输可靠性高,稳定性强
- 高效率:字节流方式传递,效率很高
- 有效性、可用性:应用广泛,支持任何平台
NIO
- 1New I/O API Protocol(NIO)
- 2NIO协议和TCP协议类似,但NIO更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的client调用和服务器端有更多的负载。
- 3适合使用NIO协议的场景:
- 可能有大量的Client去连接到Broker上,一般情况下,大量的Client去连接Broker是被操作系统的线程所限制的。因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议。
- 可能对于Broker有一个很迟钝的网络传输,NIO比TCP提供更好的性能。
- NIO连接的URI形式:nio://hostname:port?key=value&key=value
AMQP
一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件限制。
STOMP
是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息中间件)设计的简单文本协议。
QMTT
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当作传感器和致动器(比如通过Twitter让房屋联网)的通信协议。
02、NIO协议案例
ActiveMQ这些协议传输的底层默认都是使用BIO网络的IO模型。只有当我们指定使用nio才使用NIO的IO模型。
如果你不特别指定ActiveMQ的网络监听端口,那么这些端口都讲使用BIO网络IO模型
所以为了首先提高单节点的网络吞吐性能,我们需要明确指定ActiveMQ网络IO模型。
如下所示:URI格式头以“nio”开头,表示这个端口使用以TCP协议为基础的NIO网络IO模型。
- 修改配置文件 activemq.xml
<transportConnectors>
<transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />
</transportConnectors>
测试的时候只需要把 "tcp://192.168.31.143:61616"
改成 "nio://192.168.31.143:61618"
就好了
03、NIO协议案例增强
上面是Openwire协议传输底层使用NIO网络IO模型。 如何让其他协议传输底层也使用NIO网络IO模型呢?
URI格式以"nio"开头,代表这个端口使用TCP协议为基础的NIO网络模型。但是这样的设置方式,只能使这个端口支持Openwire协议。
我们怎么能够让这个端口既支持NIO网络模型,又让他支持多个协议呢?
解决:
- 使用auto关键字
- 使用“+”符号来为端口设置多种特性
- 如果我们既需要某一个端口支持NIo网络lo模型,又需要它支持多个协议
<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"/>
<!--注释掉配置的 NIO-->
<!-- <transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" /> -->
<!--新增配置-->
<transportConnector name="auto+nio" uri="auto+nio://0.0.0.0:61608?maximumConnections=1000&wireFormat.maxFrameSize=104857600&org.apache.activemq.transport.nio.SelectorManager.corePoolSize=20&org.apache.activemq.transport.nio.Se1ectorManager.maximumPoo1Size=50"/>
</transportConnectors>
"tcp://192.168.31.143:61608"
和 "nio://192.168.31.143:61608"
都可以使用
8、消息的存储和持久化
- 此处的持久化和之前的持久化的区别
- MQ高可用:事务、可持久、签收,是属于MQ自身特性,
自带
的。 - 这里的持久化是
外力,是外部插件
。意思就是MQ本身有一份数据。然后别的地方持久化一份数据
介绍
持久化是什么?一句话就是:ActiveMQ宕机了,消息不会丢失的机制。
说明:为了避免意外宕机以后丢失信息,需要做到重启后可以恢复消息队列,消息系统一半都会采用持久化机制。ActiveMQ的消息持久化机制有JDBC,AMQ,KahaDB和LevelDB
,无论使用哪种持久化方式,消息的存储逻辑都是一致的。就是在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等
。再试图将消息发给接收者,成功
则将消息从存储中删除
,失败
则继续尝试尝试发送
。消息中心启动以后,要先检查指定的存储位置是否有未成功发送的消息,如果有,则会先把存储位置中的消息发出去
。
01、AMQ
基于文件的存储机制,是以前的默认机制,现在不再使用。
AMQ是一种文件存储形式,它具有写入速度快和容易恢复的特点。消息存储再一个个文件中文件的默认大小为32M,当一个文件中的消息已经全部被消费,那么这个文件将被标识为可删除,在下一个清除阶段,这个文件被删除。AMQ适用于ActiveMQ5.3之前的版本
02、KahaDB
-
默认持久化机制为
KahaDB
-
config/activemq.xml下有以下配置
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
- 日志文件存储在 activemq安装目录/data/kahadb下
[root@localhost kahadb]# pwd
/usr/local/ActiveMQ/apache-activemq-5.15.9/data/kahadb
[root@localhost kahadb]# ll
total 612
-rw-r--r--. 1 root root 33554432 Oct 30 16:04 db-1.log
-rw-r--r--. 1 root root 90112 Oct 30 16:38 db.data
-rw-r--r--. 1 root root 69760 Oct 30 16:38 db.redo
-rw-r--r--. 1 root root 8 Oct 30 16:04 lock
- KahaDB是目前默认的存储方式,可用于任何场景,提高了
性能和恢复能力
。 - 消息存储使用
一个事务日志
和仅仅用一个索引文件
来存储它所有
的地址。 - KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化。
- 数据被追加到data logs中。当不再需要log文件中的数据的时候,log文件会被丢弃。
存储原理
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里面存储消息。通过data去找log
-
3、
db.free
当问当前db.data文件里面哪些页面是空闲的,文件具体内容是所有空闲页的ID。
方便以后建立索引的连续性,且没有碎片
-
4、
db.redo
用来进行消息恢复,如果KahaDB消息存储再强制退出后启动,用于恢复BTree索引。
宕机时用于恢复数据记录log和索引data
-
5、
lock
文件锁,表示当前kahadb独写权限的broker。
是哪个线程丢的消息,用来锁线程,类似悲观锁(读锁、写锁)
03、JDBC
- 此处配置模拟两台机器:Windows + Linux
1、先将mysql-connector-java-5.1.7-bin.jar 拷贝到 activemq 下的lib
文件夹下
2、jdbcPersistenceAdapter配置
<!--
<persistenceAdapter>
<kahaDB directory="${activemq.data}/kahadb"/>
</persistenceAdapter>
-->
<!--注意:第一次createTableOnStartup="true" 然后改成false-->
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="true"/>
</persistenceAdapter>
3、数据库连接池配置
- 添加下面的xml到conf/activemq.xml配置。注意添加的位置
<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://mysql数据库URL/activemq?relaxAutoCommit=true"/>
<property name="username" value="mysql数据库用户名"/>
<property name="password" value="mysql数据库密码"/>
<property name="maxTotal" value="200"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
<import resource="jetty.xml"/>
4、需要建一个数据库,名为activemq
。新建的数据库要采用latin1 或者ASCII编码。
CREATE DATABASE activemq
5、重启activemq。会自动生成如下3
张表。如果没有自动生成,需要我们手动执行SQL。
表1
ACTIVEMQ_MSGS
- 消息表:缺省表名为ACTIVEMQ MSGS ,queue和topic都存在里面
属性名 | 类型 | 注释 |
---|---|---|
ID | INTEGER | 自增的数据库主键 |
CONTAINER | VARCHAR(250) | 消息的Destination |
MSGID_PROD | VARCHAR(250) | 消息发送者的主键 |
MSG_SEQ | INTEGER | 是发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID |
EXPIRATION | BIGINT(20) | 消息的过期时间,存储的是从1970-01-01到现在的毫秒数 |
MSG | BLOB | 消息本体的Java序列化对象的二进制数据 |
PRIORITY | BIGINT(20) | 优先级,从0-9,数值越大优先级越高 |
表2
ACTIVEMQ_ACKS
- 用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存。数据库字段如下:
属性名 | 类型 | 注释 |
---|---|---|
CONTAINER | VARCHAR(250) | 消息的Destination |
SUB_DEST | VARCHAR(250) | 如果是使用Static集群,这个字段会有集群其他系统的信息 |
CLIENT_ID | VARCHAR(250) | 用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存。数据库字段如下:消息的Destination |
SUB_NAME | VARCHAR(250) | 订阅者名称 |
SELECTOR | VARCHAR(250) | 选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性AND和OR操作 |
LAST_ACKED_ID | INTEGER | 记录消费过的消息的ID |
表3
ACTIVEMQ_LOCK
- 此表在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,其他的只能作为备份等待MasterBroker不可用,才可能成为下一个Master Broker。这个表用于记录哪个Broker是当前的Master Broker。
属性名 | 类型 | 注释 |
---|---|---|
ID | INTAGER | |
Broker NAME | VARCHAR(250) |
- 如果不能自动生成说明操作有问题。如果实在不行,下面是手动建表的SQL:
-- 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);
此处遇到了大坑。
- 1、主机没有关闭防火墙
- 2、数据库应为 utf-8格式
- 3、如下所示:数据库有3张表。但是8161无法访问前台管理页面。且61616端口启动不了
2020-10-31 01:24:17,331 | WARN | Could not create JDBC tables; they could already exist. Failure was: INSERT INTO ACTIVEMQ_LOCK(ID) VALUES (1) Message: Cannot execute statement: impossible to writ
e to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ
COMMITTED or READ UNCOMMITTED. SQLState: HY000 Vendor code: 1665 | org.apache.activemq.store.jdbc.adapter.DefaultJDBCAdapter | main
2020-10-31 01:24:17,334 | WARN | Failure details: Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited t
o row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED. | org.apache.activemq.store.jdbc.JDBCPersistenceAdapter | main
java.sql.SQLException: Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB
is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.
- 解决 加上
useDatabaseLock="false"
加入还报错则加上transactionIsolation="4"
<persistenceAdapter>
<jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="false" useDatabaseLock="false"/>
</persistenceAdapter>
- 补充:默认是的dbcp数据库连接池,如果要换成其他数据库连接池,需要将该连接池jar包,也放到lib目录下。
Queue
在点对点类型中
- 当
DeliveryMode
设置为NON_PERSISTENCE
时,消息被保存在内存中; - 当
DeliveryMode
设置为PERSISTENCE
时,消息保存在broker
的相应的文件或者数据库中。 - 点对点类型的消息一旦被
Consumer
消费,就从broker中
删除 - 默认就是
开启持久化的
持久化会将消息持久化数据库。
生产者使用持久化发布消息。数据库MSGS存在消息。消费者消费后。数据库消息就消失了。
非持久化不会将消息持久化到数据库。
生产者不使用持久化发布消息。数据库MSGS不存在消息
Topic
一定要以Topic的持久化
形式开启
经过测试发现:之后再依次重新启动消费者和生产者 ACKS 表的用户消息会被覆盖
。而 MSGS 表的消息记录会叠加
04、JDBC Message Store with ActiveMQ Journal
介绍
- 这种方式克服了JDBC Store的不足,JDBC每次消息过来,都需要去写库读库。
- ActiveMQ Journal,使用
高速缓存
写入技术,大大提高了性能
。 - 当消费者的速度能够及时跟上生产者消息的生产速度时,journal文件能够大大减少需要写入到DB中的消息。
举例:生产者生产了1000条消息,这1000条消息会保存到journal文件,如果消费者的消费速度很快的情况下,在journal文件还没有同步到DB之前,消费者已经消费了90%的以上消息,那么这个时候只需要同步剩余的10%的消息到DB。如果消费者的速度很慢,这个时候journal文件可以使消息以批量方式写到DB。
为了高性能,这种方式使用日志文件存储+数据库存储。先将消息持久到日志文件,等待一段时间再将未消费的消息持久到数据库。
该方式要比JDBC性能要高。
配置
在JDBC原有的配置上增加配置
<persistenceFactory>
<journalPersistenceAdapterFactory
journalLogFiles="4"
journalLogFileSize="32768"
useJournal="true"
useQuickJournal="true"
dataSource="#mysql-ds"
dataDirectory="activemq-data" />
</persistenceFactory>
- 此处又有一个大坑 还是启动不了61616端口和管理页面 还是lock的问题
2020-10-31 12:37:52,407 | INFO | Database lock driver override not found for : [mysql-ab_jdbc_driver]. Will use default imple
mentation.
- 解决 在上述代码中添加
useDatabaseLock="false"
测试 QUEUE 生产者发送消息说明
消息不会第一时间存到数据库。 数据库 MSGS 是没有刚才发送的消息的。它的机制是:先将信息写到缓存日志中,需要等待一段时间,然后才会同步到数据库。但是可以用消费者消费,消费完后, 数据库 MSGS 的消息就不可能有了。
05、LevelDB
这种文件系统是从ActiveMQ5.8之后引进的,它和KahaDB非常相似,也是基于文件的本地数据库存储形式,但是它提供比KahaDB更快的持久性。
但它不使用自定义B-Tree实现来索引独写日志,而是使用基于LevelDB的索引
- 配置如下
- 将config/activemq.xml下的kahaDB改成下面这个
<persistenceAdapter>
<levelDB directory="activemq-data"/>
</persistenceAdapter>
06、总结
持久化消息主要指的是:
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方式的首选数据复制方案。 |
9、ActiveMQ 多节点配置
配置多节点集群原因
面试题:引入消息队列之后该如何保证其高可用性
三种集群方案
- 1、基于shareFileSystem共享文件系统(KahaDB)
- 2、基于JDBC
- 3、基于可复制的LevelDB
对比
LevelDB,5.6版本之后推出了LecelDB的持久化引擎,它使用了自定义的索引代替常用的BTree索引,其持久化性能高于KahaDB,虽然默认的持久化方式还是KahaDB,但是LevelDB可能会是趋势。
在5.9版本还提供了基于LevelDB和Zookeeper的数据复制方式,作为Master-Slave方式的首选数据复制方案。
本次案例
基于zookeeper和LevelDB搭建ActiveMQ集群。集群仅提供主备方式的高可用集群功能,避免单点故障。
原理
使用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讲解过自行复习。
有一个ode要作为观察者存在。当一个新的Master被选中,你需要至少保障一个法定mode在线以能够找到拥有最新状态的ode,这个ode才可以成为新的Master。
因此,推荐运行至少3个replica nodes以防止一个node失败后服务中断。
环境和版本
- CentOS 7
- JDK 1.8.0
- zookeeper-3.4.9
- activemq-5.15.9
- 关闭防火墙
搭建zookeeper集群
三台虚拟机 CentOS7
- IP地址分别为 192.168.31.143 192.168.31.167 192.168.31.170
- zookeeper-3.4.9.tar
1、分别先将虚拟机解压
/usr/local/myzookeeper/zookeeper-3.4.9
2、打开conf 然后把 zoo_sample.cfg 拷贝一份并取名为 zoo.cfg
cp zoo_sample.cfg zoo.cfg
3、修改zoo.cfg文件如下所示(3台虚拟机要求此文件内容一致
)
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/usr/local/myzookeeper/zookeeper-3.4.9/data/
dataLogDir=/usr/local/myzookeeper/zookeeper-3.4.9/log/
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1
server.1=192.168.31.143:2888:3888
server.2=192.168.31.167:2888:3888
server.3=192.168.31.170:2888:3888
4、在zookeeper-3.4.9下新建 data文件夹 和 log文件夹
5、在 data下新建文件 myid 并且对应 zoo.cfg 下的配置文件的三个IP地址 分别输入1、2、3
示例 192.168.31.143 的 myid
[root@1 data]# cat myid
1
6、启动3台虚拟机的zookeeper ./zkServer.sh start
[root@1 data]# ./zkServer.sh start
7、查看3台虚拟机的集群状态是否配置成功 ./zkServer.sh status
显示 Mode: leader / Mode: follower 即为成功
[root@1 bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/myzookeeper/zookeeper-3.4.9/bin/../conf/zoo.cfg
Mode: leader //主机
[root@2 bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/myzookeeper/zookeeper-3.4.9/bin/../conf/zoo.cfg
Mode: follower //从机
[root@3 bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/myzookeeper/zookeeper-3.4.9/bin/../conf/zoo.cfg
Mode: follower //从机
ActiveMQ集群
三台虚拟机 CentOS7
- IP地址分别为 192.168.31.143 192.168.31.167 192.168.31.170
1、更改conf/active.xml(将默认的kahadb
注释掉)添加以下配置
- 虚拟机1 192.168.31.143
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:0"
zkAddress="192.168.31.143:2181,192.168.31.167:2181,192.168.31.170:2181"
hostname="192.168.31.143"
zkPath="/activemq/leveldb-stores"
/>
</persistenceAdapter>
- 虚拟机2 192.168.31.167
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:0"
zkAddress="192.168.31.143:2181,192.168.31.167:2181,192.168.31.170:2181"
hostname="192.168.31.167"
zkPath="/activemq/leveldb-stores"
/>
</persistenceAdapter>
- 虚拟机3 192.168.31.170
<persistenceAdapter>
<replicatedLevelDB
directory="${activemq.data}/leveldb"
replicas="3"
bind="tcp://0.0.0.0:0"
zkAddress="192.168.31.143:2181,192.168.31.167:2181,192.168.31.170:2181"
hostname="192.168.31.170"
zkPath="/activemq/leveldb-stores"
/>
</persistenceAdapter>
2、在zookeeper集群启动的基础上,依次启动activemq
集群需要使用
failover:(tcp://192.168.31.143:61616,tcp://192.168.31.167:61616,tcp://192.168.31.170:61616)
配置多个ActiveMQ来进行测试
10、高级特性 - 简单介绍
01、异步投递
是什么
ActiveMQ支持同步,异步两种发送的模式将消息发送到broker,模式的选择对发送延时有巨大的影响。producer能达到怎么样的产出率(产出率=发送数据总量/时间)主要受发送延时的影响,使用异步发送可以显著提高发送的性能。
ActiveMQ默认使用异步发送的模式
:除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息,这两种情况都是同步发送的。
如果你没有使用事务且发送的是持久化的消息,每一次发送都是同步发送的且会阻塞producer,直到broker返回一个确认,表示消息已经被安全的持久化到磁盘。确认机制提供了消息安全的保障,但同时会阻塞客户端带来了很大的延时。
很多高性能的应用,允许在失败的情况下有少量的数据丢失
。如果你的应用满足这个特点,你可以使用异步发送来提高生产率,即使发送的是持久化的消息。
异步发送
它可以最大化producer端的发送效率。我们通常在发送消息量比较密集的情况下使用异步发送
,它可以很大的提升Producer性能;不过这也带来了额外的问题,
就是需要消耗更多的Client端内存同时也会导致broker端性能消耗增加;
此外它不能有效的确保消息的发送成功。在userAsyncSend=true的情况下客户端需要容忍消息丢失
的可能。
官网配置
3种配置介绍
public class JmsProduce {
/*异步投递 方式1*/ <------方式1
// public static final String ACTIVEMQ_URL = "tcp://localhost:61616?jms.useAsyncSend=true";
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
/*异步投递 方式2*/ <------方式2
// amq.setUseAsyncSend(true);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
/*异步投递 方式3*/ <------方式3
// ((ActiveMQConnection)connection).setUseAsyncSend(true);
connection.start();
//3、创建会话Session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
//开启持久化
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("MessageListener --> " + i);
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
异步投递如何确认消息发送成功
异步发送丢失消息的场景是:生产者设置userAsyncSend=true,使用producer.send(msg)持续发送消息。
如果消息不阻塞,生产者会认为所有send的消息均被成功发送至MQ。
如果MQ突然宕机,此时生产者端内存中尚未被发送至MQ的消息都会丢失
。
所以,正确的异步发送方法是需要接收回调的。
同步发送和异步 发送的区别就在此:
同步发送等send不阻塞
了就表示一定发送成功了,
异步发送需要客户端回执并由客户端再判断一次
是否发送成功
public class JmsProduce_AsyncSend {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
/*异步投递*/
amq.setUseAsyncSend(true); <------开启异步投递
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
// MessageProducer producer = session.createProducer(queue); <===这是之前的
/*异步投递 回调函数 步骤1*/
ActiveMQMessageProducer activeMQMessageProducer = (ActiveMQMessageProducer) session.createProducer(queue); <------
//开启持久化
// activeMQMessageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
TextMessage message = null;
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
message = session.createTextMessage("message --> " + i);
//8、通过 producer 发送给MQ
// activeMQMessageProducer.send(textMessage); <===这是之前的
/*异步投递 回调函数 步骤2*/
message.setJMSMessageID(UUID.randomUUID().toString() + " --- order wula"); <------
String MsgID = message.getJMSMessageID(); <------
activeMQMessageProducer.send(message, new AsyncCallback() { <------
@Override
public void onSuccess() { //成功 <------
System.out.println(MsgID + " 成功"); <------
}
@Override
public void onException(JMSException exception) { //失败 <------
System.out.println(MsgID + " 失败"); <------
}
});
}
//9、关闭资源
activeMQMessageProducer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
- 输出
ID:08633bee-4152-49cd-85a3-f3c5edcebc93 --- order wula 成功
ID:63669874-73cd-4f12-bc70-586da3ff2725 --- order wula 成功
ID:43cd4690-d3da-4b70-94f9-88025a9d1ef8 --- order wula 成功
***消息发送到MQ完成***
02、延迟投递 + 定时投递
- 开启延时投递
conf/activemq.xml
schedulerSupport="true"
- java代码里面封装的辅助消息类型:
ScheduleMessage
- 生产者如下(消费者不用做更改)
public class JmsProduce_yanshitoudi {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
long delay = 5 * 1000; /* 延迟投递的时间*/ <-----
long period = 4 * 1000; /* 重复投递的时间间隔 4s一次 */ <-----
int repeat = 3; /* 重复投递的次数 */ <-----
//6、通过生产者 producer 生产3条消息发送到MQ的队列里
for (int i = 0; i < 3; i++) {
//7、创建消息
TextMessage textMessage = session.createTextMessage("delay message --> " + i);
//延迟时间
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,delay); <-----
//重复投递的时间间隔
textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,period); <-----
//重复投递的次数
textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,repeat); <-----
/* 此处的意思是:该条消息等待5s后,输出一次。之后每4s发送一次且重复发送3次 */
//8、通过 producer 发送给MQ
producer.send(textMessage);
}
//9、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("***消息发送到MQ完成***");
}
}
03、消息消费的重试机制
介绍
是什么: 消费者收到消息,之后出现异常了,没有告诉broker确认收到该消息,broker会尝试再将该消息发送给消费者。尝试n次,如果消费者还是没有确认收到该消息,那么该消息将被放到死信队列重,之后broker不会再将该消息发送给消费者。
具体哪些情况会引发消息重发
- 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(私信队列)。
属性
此处模拟第2种
- 生产者发送3条消息 (代码省略)
- 消费者。
开启事务,却没有commit
。重启消费,前6次都能收到消息,到第7次,不会再收到消息。
public class JmsConsumer_xiaoxichongshi {
public static final String ACTIVEMQ_URL = "tcp://192.168.31.143:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1、创建连接工厂。默认采用的用户名和密码都是admin。该类的其他构造方法可以指定用户名和密码
ActiveMQConnectionFactory amq = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2、通过连接工厂获得连接connection,并启动访问
Connection connection = amq.createConnection();
connection.start();
//3、创建会话Session
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); <-----开启事务
// 4 创建目的地(两种 :队列/主题)。
Queue queue = session.createQueue(QUEUE_NAME);
//5、创建消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
同步阻塞方式 receive()
*/
while (true){
TextMessage textMessage = (TextMessage) consumer.receive(3000L);
if (textMessage != null){
System.out.println("消费者接收到消息 ---> " + textMessage.getText());
}else {
break;
}
}
//关闭资源
// session.commit(); <-----没有提交 会造成重复消费问题
consumer.close();
session.close();
connection.close();
System.out.println("***消费完成***");
}
}
- 输出
//第一次
消费者接收到消息 ---> MessageListener --> 0
消费者接收到消息 ---> MessageListener --> 1
消费者接收到消息 ---> MessageListener --> 2
***消费完成***
//然后再6次。1-6次的输出结果是这样的:
消费者接收到消息 ---> MessageListener --> 0
消费者接收到消息 ---> MessageListener --> 1
消费者接收到消息 ---> MessageListener --> 2
***消费完成***
//第7次的输出结果是这样的:
***消费完成*** 注意:到这一步其实是一共运行了8次
- activemq管理后台。多了一个名为ActiveMQ.DLQ队列,里面多了3条消息。
默认参数是消费重试6次,此处
修改默认参数
为3次
// 修改默认参数,设置消息消费重试3次
RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy();
redeliveryPolicy.setMaximumRedeliveries(3);
amq.setRedeliveryPolicy(redeliveryPolicy);
- 输出
//第一次
消费者接收到消息 ---> MessageListener --> 0
消费者接收到消息 ---> MessageListener --> 1
消费者接收到消息 ---> MessageListener --> 2
***消费完成***
//然后再3次。1-3次的输出结果是这样的:
消费者接收到消息 ---> MessageListener --> 0
消费者接收到消息 ---> MessageListener --> 1
消费者接收到消息 ---> MessageListener --> 2
***消费完成***
//第4次的输出结果是这样的:
***消费完成*** 注意:到这一步其实是一共运行了5次
整合 Spring
04、死信队列
介绍
ActiveMQ中引入了“死信队列”(Dead Letter Queue)的概念。即一条消息再被重发了多次后(默认为重发6次redeliveryCounter==6),将会被ActiveMQ移入“死信队列”。开发人员可以在这个Queue中查看处理出错的消息,进行人工干预
。
-
一般生产环境中在使用MQ的时候设计两个队列:
一个是核心业务队列,一个是死信队列
。 -
核心业务队列,就是比如上图专门用来让订单系统发送订单消息的,然后另外一个死信队列就是用来处理异常情况的。
-
假如第三方物流系统故障了此时无法请求,那么仓储系统每次消费到一条订单消息,尝试通知发货和配送都会遇到对方的接口报错。此时仓储系统就可以把这条消息拒绝访问或者标志位处理失败。一旦标志这条消息处理失败了之后,MQ就会把这条消息转入提前设置好的一个死信队列中。然后你会看到的就是,在第三方物流系统故障期间,所有订单消息全部处理失败,全部会转入死倍队列。然后你的仓储系统得专门有一个后台线程,监控第三方物流系统是否正常,能否请求的,不停的监视。一旦发现对方恢复正常,这个后台线程就从死信队列消费出来处理失败的订单,重新执行发货和配送的通知逻辑。
死信队列的配置(一般采用默认)
1、sharedDeadLetterStrategy
- 不管是queue还是topic,失败的消息都放到这个队列中。下面修改activemq.xml的配置,可以达到修改队列的名字。
2、individualDeadLetterStrategy
- 可以为queue和topic单独指定两个死信队列。还可以为某个话题,单独指定一个死信队列。
3、自动删除过期消息
- 过期消息是值生产者指定的过期时间,超过这个时间的消息。
4、存放非持久消息到死信队列中
05、防止重复调用 幂等性
幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。