上接: 消息中间件–JMS–ActiveMQ–01
消息中间件–JMS–ActiveMQ–02
6、JMS规范
6.1、什么是JMS
JMS,全称Java Message Service,类似于JDBC、JNDI,是JavaEE体系中的一种,规定了两个应用程序之间进行异步通信的API,它为标准协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发。
在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。这个中间产品就是上文介绍的MQ,它有许多中落地的产品,详细的对比可以参见第3节。
6.2、JMS的组成结构和特点
上图是JMS规范的架构图,主要的核心组件一下四个:
Producer:消息生产者,创建和发送JMS消息的客户端应用
Consumer:消息消费者,接收和处理JMS消息的客户端应用
JMS服务器(MQ):实现JMS接口和规范的消息中间件,也就是我们说的MQ服务器
Message:封装消息的实体,由三部分组成,消息头、消息体和消息属性。
消息头:对一条消息进行基本属性的设置,常用的消息头属性有:
JMSDestination:消息发送的目的地,主要指Queue和Topic
JMSDeliveryMode:分发模式,用来设置是否持久化消息,默认持久化
JMSExpiration:消息的过期时间,用于持久化消息的过期设置,过期删除消息,默认不过期
JMSPriority:消息优先级,从0-9十个级别,0-4是普通消息5-9是加急消息。JMS不要求MQ严 格按照这十个优先级发送消息,但必须保证加急消息要先于普通消息到达。默认 是4级。
JMSMessageID:每条消息的唯一标识,通常又由MQ产生,也可以使用自定义的唯一标识。
消息体:消息的正文部分,发送和接收的消息体类型必须一致对应。根据格式可以分为以下5种:
TextMessage:普通字符串消息(常用)
MapMessage:一个Map类型的消息,key为String,value为java的基本数据类型(常用)
BytesMessage:二进制数组信息,包含一个byte[]
StreamMessage:java数据流信息,用标准流操作来顺序填充和读取
ObjectMessage:对象消息,包含一个可序列化的java对象
消息属性:消息的拓展属性,属于对消息头的补充。可以将请求头看成一些基本的消息属性,将消 息属性视为自定义的补充,可以根据实际需要进行k-v键值对的设置。
消息头既可以使用MessageProducer进行全局设置,也可以使用Message进行独立设置;
消息体和消息属性因为是针对具体的消息的,所以只能在Message中进行设置。
6.3、JMS的可靠性
对于JMS而言,它是通过消息中间件完成的信息交流,那么JMS怎么保证消息不丢失、不被重复消费、确认被成功消费呢?换句话说,JMS必须要保证上游到下游的消息能够不丢失,准确送达并且不被重复消费。
为了解决这些问题,JMS使用Persistent(持久化)、Trancaction(事务)和Acknowledge(签收)来落地实现。
6.3.1、消息持久化
Persistent:持久化,指MQ是否需要将消息进行持久化储存,没有进行持久化保存的数据一旦宕机就会丢失。有两种持久化方式:
DeliveryMode.NON_PERSISTENT:1,代表不持久化
DeliveryMode.PERSISTENT:2,代表持久化,Queue默认采用这种方式
持久化代码(都是在消息生产者侧进行设置):
//针对消息生产者进行设置,此生产者生产的消息默认都采用这种持久化方式,如果不设置,采用默认模式
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT)
//针对消息进行设置,对某一条消息特殊化设置持久化方式,如果不设置,则使用消息生产者的传递模式
textMessage.setDeliveryMode(DeliveryMode.NON_PERSISTENT)
下面分Queue和Topic进行代码演示:
Queue:
生产者:
public class JmsProduce {
private static final String ACTIVEMQ_URL = "tcp://192.168.234.133:61616";
private 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();
//3.创建会话session
//两个参数transacted=事务,acknowledgeMode=确认模式(签收)
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(具体是队列queue还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
//5.创建消息的生产者,并统一设置传递模式
MessageProducer messageProducer = session.createProducer(queue);
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
//6.启动连接
connection.start();
//7.通过使用消息生产者,生产三条消息,发送到MQ的队列里面
for (int i = 0; i < 3; i++) {
//8.创建消息
TextMessage textMessage = session.createTextMessage("msg---hello" + i);//理解为一个字符串
//9.通过messageProducer发送给MQ队列
messageProducer.send(textMessage);
}
//10.关闭资源
messageProducer.close();
session.close();
System.out.println("****消息发布到MQ队列完成");
}
}
消费者代码不变,参考5.1;
Topic:
生产者:
package com.demo.activemq.persist;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 持久化Topic生产者
*/
public class JmsProducer_Topic_Persist {
private static final String ACTIVEMQ_URL = "tcp://192.168.10.130:61616";
private static final String ACTIVEMQ_TOPIC_NAME = "Topic-Persist";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂,按照给定的URL,采用默认的用户名密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2.通过连接工厂,持久化的topic必须在生产者创建并设置持久化完成后调用start
Connection connection = activeMQConnectionFactory.createConnection();
//3.创建会话session
//两个参数transacted=事务,acknowledgeMode=确认模式(签收)
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(具体是队列queue还是主题topic)
Topic topic = session.createTopic(ACTIVEMQ_TOPIC_NAME);
//5.创建消息的生产者
MessageProducer messageProducer = session.createProducer(topic);
//6.设置生产者生产持久化的Topic
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
//7.启动连接(启动连接必须在设置完传递模式后)
connection.start();
//8.通过使用持久化Topic消息生产者,生产三条消息,发送到MQ的队列里面
for (int i = 0; i < 3; i++) {
//9.通过session创建消息
TextMessage textMessage = session.createTextMessage("msg-persist" + i);
//10.使用指定好目的地的消息生产者发送消息
messageProducer.send(textMessage);
}
//11.关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("****TOPIC_NAME消息发布到MQ完成");
}
}
消费者:
/**
* 持久化Topic消费者
*/
public class Jms_Topic_Consumer_Persist {
private static final String ACTIVEMQ_URL = "tcp://192.168.10.130:61616";
private static final String ACTIVEMQ_TOPIC_NAME = "Topic-Persist";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是3号消费者王五");
//1.创建连接工厂,按照给定的URL,采用默认的用户名密码
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2.通过连接工厂,获得connection,设置connectionID
Connection connection = activeMQConnectionFactory.createConnection();
//必须要进行id的设置,否则会报错,因为需要区别每一个订阅者
connection.setClientID("王五");
//3.创建会话session
//两个参数transacted=事务,acknowledgeMode=确认模式(签收)
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(具体是队列queue还是主题topic)
Topic topic = session.createTopic(ACTIVEMQ_TOPIC_NAME);
//5.通过session创建持久化订阅
//注意:与非持久化操作不同的是,此处使用的不是MessageConsumer,而是TopicSubscriber
//因为Topic采用的是发布/订阅模式,当订阅者完成订阅,并且暂时离线的情况下,MQ应当在订阅者上线后将持久化的Topic消息推送给订阅者。
//MessageConsumer和TopicSubscriber的区别在于,前者只能消费在线期间生产者推送的消息,后者不仅可以消费在线期间生产者推送的消息,也能在重新上线时重新消费不在线期间生产者推送的消息,但是前提是生产者设置了持久化传递模式。
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "我是王五");
//6.启动连接
connection.start();
//7.接收消息
topicSubscriber.setMessageListener(message -> {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("收到的持久化订阅消息: " + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
/**
* 一定要先运行一次消费者,类似于像MQ注册,我订阅了这个主题
* 然后再运行主题生产者
* 无论消费着是否在线,都会接收到,在线的立即接收到,不在线的等下次上线把没接收到的接收
*/
}
}
Queue和Topic持久化总结:
持久化影响的是MQ对消息的储存方式,默认请款修改Queue和Topic方式的都是采用持久化方式。持久化的消息在MQ关机后不会丢失,非持久化的消息在MQ关机后会丢失,类似于Redis。
两种模式的持久化方式是一致的:
//针对消息生产者进行设置,此生产者生产的消息默认都采用这种持久化方式,如果不设置,采用默认模式
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT)
//针对消息进行设置,对某一条消息特殊化设置持久化方式,如果不设置,则使用消息生产者的传递模式
textMessage.setDeliveryMode(DeliveryMode.NON_PERSISTENT)
对于Queue来讲,它是一种一对一的推送模式,对生产者和消费者的存在顺序没有要求,无论是否持久化,只要数据没有丢失,MQ都会在消费者可用的时候将消息推送过去。因为未持久化的消息会保存在MQ的内存中,不会丢失。类似于给朋友发短信。并且当消费者确认签收之后,MQ就会将该条消息删除。
而对于Topic来讲,它是一种一对多的推送模式,要求订阅者必须在生产者之前存在,没有被订阅的Topic消息会被认定为废消息,从而被删除。类似于微信公众号。订阅者订阅该主题有两种方式:
使用MessageConsumer订阅:暂时订阅,此订阅仅在消费者在线期间有效,也只会接收生产者在其在线期间推送的消息。此时是否持久化Topic都不影响消费者的消费能力。
使用TopicSubscriber订阅:持久化订阅,此订阅永久有效,即使MQ宕机也不会失效,消费者向MQ注册一个身份ID识别号,MQ做持久化保存,当消费者离线时,订阅不会被取消,MQ为此ID保存所有离线期间收到的Topic消息,当消费者重新上线后,MQ会把储存的消息重新推送给消费者,所以这种订阅模式可以接收订阅之后生产者推送的所有消息(在线期间+离线期间),但是如果生产者没有设置Topic消息的持久化,订阅者就不能获得其离线期间生产者推送的消息,因为非持久化的Topic消息不会向Queue那样被MQ保存在内存中,没有被订阅的Topic消息无论是否持久化,都会被认定为废消息,从而被删除。所以,**TopicSubscriber必须配合Topic的持久化才能发挥最大效能。**Topic的消息签收之后也不会被删除。
注意:无论哪一种模式的订阅,都要保证订阅者先启动!!!
为了保证JMS的可靠性,Queue通常采用持久化传递方式,Topic通常采用持久化传递+TopicSubscriber订阅方式,这样可以保证信息不丢失,并且可以被消费者完全消费,保证了JMS的可靠性。
6.3.2、消息事务和消息签收
消息事务:指客户端(生产者/消费者)与MQ服务器的一次会话期间,发送或者接收消息的一连串操作是一个整体,要么全部成功要么全部失败。
消息签收:指消费者从MQ服务器接收到消息并成功处理之后,给MQ服务器的一个反馈,类似于快递包裹的前后,代表着一次完整消息链的完结,MQ在收到签收消息之后,将释放消息资源,防止重复消费。
消息事务可以应用在生产者端,也可以应用在消费者端;而消息签收只应用在消费者端。
如果在消费者端开启了事务,则消息的签收与否与消息签收的设置无关,完全取决于事务是否被提交。
设置事务和签收的代码:
Session session = connection.createSession(true, Session.CLIENT_ACKNOWLEDGE);
无论是在消费者端还是生产者端,事务和签收的设置都是在创建session的时候设置的,它们的取值有一下几种:
transacted:
true ---------- 开启事务,需要手动提交
fasle ---------- 不开启事务,自动提交
ack_mode:
Session.SESSION_TRANSACTED --------- 0,表示根据事务是否提交决定是否签收
Session.AUTO_ACKNOWLEDGE --------- 1,表示自动签收
Session.CLIENT_ACKNOWLEDGE --------- 2,表示手动签收
Session.DUPS_OK_ACKNOWLEDGE --------- 3,表示允许重复签收
下面展示生产者端和消费者端的代码:
生产者端:
package com.demo.activemq.acknowledge;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class Jms_Transaction_AUTOACK_Producer {
private static final String ACTIVEMQ_URL = "tcp://192.168.234.133: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();
//开启事务,因为生产者端不涉及到消息的获取,所以签收模式可以任意,没有实际意义
Session session = connecti