一、JMS是什么
什么是Java消息服务?
Java消息服务指的是两个应用程序之间进行异步通信的API,它为标准协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发。在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。
二、消息头
常用api:
消息的生产者可以set这些属性,消息的消费者可以get这些属性。
这些属性在send方法里面也可以设置。
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 | 消息的唯一标识符 消息身上挂着永不重复的流水号,如果调用时去库里查,如果有,说明这个消息已经消费了,不要重复调用 |
三、消息体
封装具体的消息数据 |
5种消息体格式 | TextMessage | 普通字符串消息,包含一个String |
MapMessage | 一个Map类型的消息,key为String类型,值为java基本类型 |
BytesMessage | 二进制数组消息,包含一个byte[] |
StreamMessage | java数据流消息,用标准流来操作来顺序的填充和读取 |
ObjectMessage | 对象消息,包含一个可序列化的java对象 |
发送和接收的消息体类型必须一致对应 |
四、消息属性
如果需要除消息头字段之外的值,那么可以使用消息属性。他是识别/去重/重点标注等操作。(有一条是vip)
他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器。消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。它们还用于暴露消息选择器在消息过滤时使用的数据。
// 调用Message的set*Property()方法,就能设置消息属性。根据value的数据类型的不同,有相应的API。
textMessage.setStringProperty("From","ZhangSan@qq.com");
textMessage.setByteProperty("Spec", (byte) 1);
textMessage.setBooleanProperty("Invalide",true);
System.out.println("消息属性:"+textMessage.getStringProperty("From"));
System.out.println("消息属性:"+textMessage.getByteProperty("Spec"));
System.out.println("消息属性:"+textMessage.getBooleanProperty("Invalide"));
五、JMS的可靠性
持久性: | 什么是持久化消息? 保证消息只被传送一次和成功使用一次。在持久性消息传送至目标时,消息服务将其放入持久性数据存储。如果消息服务由于某种原因导致失败,它可以恢复此消息并将此消息传送至相应的消费者。虽然这样增加了消息传送的开销,但却增加了可靠性。 |
事务 | |
Acknowledge:签收 | |
5.1queue消息的非持久和持久 默认持久化
queue非持久,当服务器宕机,消息不存在(消息丢失了)。即便是非持久,消费者在不在线的话,消息也不会丢失,等待消费者在线,还是能够收到消息的。
queue持久化,当服务器宕机,消息依然存在。queue消息默认是持久化的。
持久化消息,保证这些消息只被传送一次和成功使用一次。对于这些消息,可靠性是优先考虑的因素。
可靠性的另一个重要方面是确保持久性消息传送至目标后,消息服务在向消费者传送它们之前不会丢失这些消息。
运行结果证明:当生产者成功发布消息之后,MQ服务端宕机重启,消息生产者就收不到该消息了 |
public class JmsProduce {
// linux 上部署的activemq 的 IP 地址 + activemq 的端口号
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
// 目的地的名称
public static final String QUEUE_NAME = "jdbc01";
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 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
// 非持久化
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
// 6 通过messageProducer 生产 3 条 消息发送到消息队列中
for (int i = 1; i <=3 ; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage("msg--" + i);
// 8 通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9 关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** 消息发送到MQ完成 ****");
}
}
|
持久化的消费者,和之前的代码一样。 下面演示持久化的生产者。 运行结果证明:当生产者成功发布消息之后,MQ服务端宕机重启,消息生产者仍然能够收到该消息 |
public class JmsProduce {
// linux 上部署的activemq 的 IP 地址 + activemq 的端口号
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
// 目的地的名称
public static final String QUEUE_NAME = "jdbc01";
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 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
// 持久化
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 6 通过messageProducer 生产 3 条 消息发送到消息队列中
for (int i = 1; i <=3 ; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage("msg--" + i);
// 8 通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9 关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** 消息发送到MQ完成 ****");
}
} |
5.2topic消息的非持久和持久
topic默认就是非持久化的,因为生产者生产消息时,消费者也要在线,这样消费者才能消费到消息。
topic消息持久化,只要消费者向MQ服务器注册过,所有生产者发布成功的消息,该消费者都能收到,不管是MQ服务器宕机还是消费者不在线。
注意:
- 一定要先运行一次消费者,等于向MQ注册,类似我订阅了这个主题。
- 然后再运行生产者发送消息。
- 之后无论消费者是否在线,都会收到消息。如果不在线的话,下次连接的时候,会把没有收过的消息都接收过来。
持久化topic生产者代码。 |
public class JmsProduce_topic {
// linux 上部署的activemq 的 IP 地址 + activemq 的端口号
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
// 目的地的名称
public static final String TOPIC_NAME = "topic--";
public static void main(String[] args) throws JMSException {
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
Topic topic =(Topic) session.createTopic(TOPIC_NAME);
MessageProducer messageProducer = session.createProducer(topic);
// 设置持久化topic
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 设置持久化topic之后再,启动连接
connection.start();
// 6 通过messageProducer 生产 3 条 消息发送到消息队列中
for (int i = 1; i <=3; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage();
textMessage.setText("TOPIC_NAME" + i);
// 8 通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9 关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("TOPIC_NAME **** 消息发送到MQ完成 ****");
}
} |
持久化topic消费者代码。 |
public class JmsConsummer_topic {
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
public static final String TOPIC_NAME = "topic--";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("****z3");
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.setClientID("z3");//1.谁订阅了我
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic =(Topic) session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber=session.createDurableSubscriber(topic,"remark...");//2.持久化的订阅者(订阅的主题,备注)
//3.张三订阅了这个主题
connection.start();
Message message = topicSubscriber.receive();
while (null!=message){
TextMessage textMessage=(TextMessage)message;
System.out.println("收到的持久化topic"+textMessage.getText());
message=topicSubscriber.receive(1000L);
}
session.close();
connection.close();
}
} |
没启动生产者之前
未启动生产者之前
启动生产者之后
启动生产者之后,1s未收到新消息,就关闭会话,订阅者变为离线状态?(取消关注??)
5.3 事务:事务偏生产者,签收偏消费者
- 生产者开启事务后,执行commit方法,这批消息才真正的被提交。不执行commit方法,这批消息不会提交。执行rollback方法,之前的消息会回滚掉。生产者的事务机制,要高于签收机制,当生产者开启事务,签收机制不再重要。
- 消费者开启事务后,执行commit方法,这批消息才算真正的被消费。不执行commit方法,这些消息不会标记已消费,下次还会被消费。执行rollback方法,是不能回滚之前执行过的业务逻辑,但是能够回滚之前的消息,回滚后的消息,下次还会被消费。消费者利用commit和rollback方法,甚至能够违反一个消费者只能消费一次消息的原理。
- 问:消费者和生产者需要同时操作事务才行吗? 答:消费者和生产者的事务,完全没有关联,各自是各自的事务。
|
public class JmsProduce {
// linux 上部署的activemq 的 IP 地址 + activemq 的端口号
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
// 目的地的名称
public static final String QUEUE_NAME = "jdbc01";
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);
// 4 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
// 持久化
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 6 通过messageProducer 生产 3 条 消息发送到消息队列中
for (int i = 1; i <=3 ; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage("msg--" + i);
// 8 通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9 关闭资源
messageProducer.close();
session.commit();
session.close();
connection.close();
System.out.println(" **** 消息发送到MQ完成 ****");
/* 为什么开启事务,批处理同时成功或者失败
try {
// ok session.commit()
}catch (Exception e){
e.printStackTrace();
//error
session.rollback();
}finally {
if(null!=session){
session.close();
}
}*/
}
}
/* 为什么开启事务,批处理同时成功或者失败
try {
// ok session.commit()
}catch (Exception e){
e.printStackTrace();
//error
session.rollback();
}finally {
if(null!=session){
session.close();
}
}*/ |
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
public static final String QUEUE_NAME = "jdbc01";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是2号消费者");
// 1 按照给定的url创建连接工厂,这个构造器采用默认的用户名密码。该类的其他构造方法可以指定用户名和密码。
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2 通过连接工厂,获得连接 connection 并启动访问。
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3 创建会话session 。第一参数是是否开启事务, 第二参数是消息签收的方式
//false只消费一次消息
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
while (true){
TextMessage textMessage=(TextMessage)messageConsumer.receive(4000L);
if(null!=textMessage){
System.out.println(textMessage.getText());
}else {
break;
}
}
messageConsumer.close();
session.commit();
session.close();
connection.close();
}
} |
没有启动消费者之前
消费者事务开启,但是没有commit,每次都能消费到消息,造成消息的重复消费,依旧如此
5.4 签收
- 自动签收(Session.AUTO_ACKNOWLEDGE):该方式是默认的。该种方式,无需我们程序做任何操作,框架会帮我们自动签收收到的消息。
- 手动签收(Session.CLIENT_ACKNOWLEDGE):手动签收。该种方式,需要我们手动调用Message.acknowledge(),来签收消息。如果不签收消息,该消息会被我们反复消费,只到被签收。
- 允许重复消息(Session.DUPS_OK_ACKNOWLEDGE):多线程或多个消费者同时消费到一个消息,因为线程不安全,可能会重复消费。该种方式很少使用到。
- 事务下的签收(Session.SESSION_TRANSACTED):开始事务的情况下,可以使用该方式。该种方式很少使用到。
- 在事务性会话中,当一个事务被成功提交则消息被自动签收。如果事务回滚,则消息会被再次传送。事务优先于签收,开始事务后,签收机制不再起任何作用。
- 非事务性会话中,消息何时被确认取决于创建会话时的应答模式。
- 生产者事务开启,只有commit后才能将全部消息变为已消费。
- 事务偏向生产者,签收偏向消费者。也就是说,生产者使用事务更好点,消费者使用签收机制更好点。
非事务情况下签收,生产者不涉及到签收,生产者不变 |
public class JmsProduce {
// linux 上部署的activemq 的 IP 地址 + activemq 的端口号
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
// 目的地的名称
public static final String QUEUE_NAME = "jdbc01";
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 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
// 持久化
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
// 6 通过messageProducer 生产 3 条 消息发送到消息队列中
for (int i = 1; i <=3 ; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage("msg--" + i);
// 8 通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9 关闭资源
messageProducer.close();
// session.commit();
session.close();
connection.close();
System.out.println(" **** 消息发送到MQ完成 ****");
}
} |
消费者,如果没有执行textMessage.acknowledge();也会造成消息的重复消费 |
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.8.114:61616";
public static final String QUEUE_NAME = "jdbc01";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是2号消费者");
// 1 按照给定的url创建连接工厂,这个构造器采用默认的用户名密码。该类的其他构造方法可以指定用户名和密码。
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2 通过连接工厂,获得连接 connection 并启动访问。
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3 创建会话session 。第一参数是是否开启事务, 第二参数是消息签收的方式
//false只消费一次消息
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
while (true){
TextMessage textMessage=(TextMessage)messageConsumer.receive(4000L);
if(null!=textMessage){
System.out.println(textMessage.getText());
textMessage.acknowledge();
}else {
break;
}
}
messageConsumer.close();
//session.commit();
session.close();
connection.close();
} }
|
开启事务的签收,如果是Session.AUTO_ACKNOWLEDGE,正常 |
开启事务的签收,如果是Session.CLIENT_ACKNOWLEDGE,没有进行textMessage.acknowledge();没有重复消费;也就是如果开启了事务,是自动签收了,定义CLIENT签收的意义不大 |
消费事务开启,只有commit之后才能将全部消息变为已消费 |
在事务性会话中,当一个事务被成功提交则消息被自动签收 如果事务回滚,则消息会被再次传送 |
非事务性会话中,消息何时被确认取决于创建会话时的应答模式 |
六、JMS的点对点总结
点对点模型是基于队列的,生产者发消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能。和我们平时给朋友发送短信类似。
如果在Session关闭时有部分消息己被收到但还没有被签收(acknowledged),那当消费者下次连接到相同的队列时,这些消息还会被再次接收
队列可以长久地保存消息直到消费者收到消息。消费者不需要因为担心消息会丢失而时刻和队列保持激活的连接状态,充分体现了异步传输模式的优势
七、JMS的发布订阅总结
JMS的发布订阅总结
JMS Pub/Sub 模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic。
主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息。
主题使得消息订阅者和消息发布者保持互相独立不需要解除即可保证消息的传送
非持久订阅
非持久订阅只有当客户端处于激活状态,也就是和MQ保持连接状态才能收发到某个主题的消息。
如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到。
一句话:先订阅注册才能接受到发布,只给订阅者发布消息。
持久订阅
客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ的时候,会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
当持久订阅状态下,不能恢复或重新派送一个未签收的消息。
持久订阅才能恢复或重新派送一个未签收的消息。
非持久和持久化订阅如何选择
当所有的消息必须被接收,则用持久化订阅。当消息丢失能够被容忍,则用非持久订阅。
八、ActiveMQ 的Broker
是什么:
相当于一个ActiveMQ服务器实例。说白了,Broker其实就是实现了用代码的形式启动ActiveMQ将MQ嵌入到Java代码中,以便随时用随时启动,在用的时候再去启动这样能节省了资源,也保证了可用性。
启动broker时指定配置文件:
./activemq start xbean:file:/myactiveMQ/apache-activemq/conf/activemq02.xml
启动broker时指定配置文件,可以帮助我们在一台服务器上启动多个broker。实际工作中一般一台服务器只启动一个broker。
嵌入式的broker启动:
用ActiveMQ Broker作为独立的消息服务器来构建Java应用。
ActiveMQ也支持在vm中通信基于嵌入的broker,能够无缝的集成其他java应用。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
public class EmbedBroker {
public static void main(String[] args) throws Exception {
//用ActiveMQ Broker作为独立的消息服务器来构建Java应用。
BrokerService brokerService=new BrokerService();
brokerService.setUseJmx(true);
brokerService.addConnector("tcp://localhost:61616");
brokerService.start();
}
}