一、JMS简介
JMS,全称为Java Message Service,即Java消息服务,是JavaEE中13个核心工业规范标准之一。其他的核心工业规范标准是:JDBC、JNDI(Java的命名和目录接口)、EJB、RMI(远程方法调用)、Java IDL/CORBA(Java接口定义语言/公共对象请求代理程序体系结构)、JSP、Servlet、XML(可扩展标记语言)、JTA(Java事务API)、JTS(Java事务服务)、JavaMail、JAF。
什么是Java消息服务呢?Java消息服务指的是两个应用程序之间进行异步通信的API,它为标准消息协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发。在JavaEE中,当两个应用程序使用JMS进行通信时,他们之间并不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦、异步、削峰的目的。
实现了JMS的产品称为消息中间件,目前市场上主流的消息中间件及其对比如下:
特性 | ActiveMQ | RabbitMQ | Kafka | RocketMQ |
---|---|---|---|---|
PRODUCER-CONSUMER | 支持 | 支持 | 支持 | 支持 |
PUBLISH-SUBSCRIBE | 支持 | 支持 | 支持 | 支持 |
REQUEST-REPLY | 支持 | 支持 | - | 支持 |
API完备性 | 高 | 高 | 高 | 低(静态配置) |
多语言支持 | 支持,Java优先 | 语言无关 | 支持,Java优先 | 支持 |
单机吞吐量 | 万级 | 万级 | 十万级 | 万级 |
消息延迟 | - | 微秒级 | 毫秒级 | - |
可用性 | 高(主从) | 高(主从) | 非常高(分布式) | 高 |
消息丢失 | - | 低 | 理论上不会丢失 | - |
消息重复 | - | 可控制 | 理论上会有重复 | - |
文档的完备性 | 高 | 高 | 高 | 中 |
由此可见:常用的消息中间件都支持生产-消费模式和发布-订阅模式,Kafka的单机吞吐量最大、功最为强悍的,通常用于大数据的开发
二、JMS的体系组成
JMS主要有四大核心组件:
JMS Provider:实现JMS接口和规范的消息中间件,也就是MQ服务器
JMS Producer:消息生产者,创建和发送JMS消息的客户端应用
JMS Consumer:消息消费者,接收和处理JMS消息的客户端应用
JMS Message:生产和消费的主体,Message由消息头、消息体和消息属性组成
下面着重介绍下JMS Message:
1、Message的消息头
消息头有以下几个重要属性:
1️⃣JMSDestination:消息发送的目的地,主要有Queue和Topic,Queue和Topic都是Destination的实现
2️⃣JMSDeliveryMode:消息传输模式,有两个模式可选——持久化模式和非持久化模式,持久化的消息在消息服务器宕机后再重启不会丢失,而非持久化的消息则会丢失,可通过将消息设置为持久化来保证消息的可靠性,队列中的消息默认是持久化的,但Topic中的消息默认是非持久化的
3️⃣JMSExpiration:消息的过期时间,默认是永不过期。若给MessageProducer对象设置了timeToLive属性值或者在调用MessageProducer.send()时指定了timeToLive的值,则消息的过期时间等于timeToLive值 + 发送时刻的GMT时间值,即在timeToLive之后消息过期,因为发送时刻的GMT时间值即为当前时间,如果设置的timeToLive值为0,则JMSExpiration被设置为0,即永不过期,也可以给消息设置JMSExpiration属性值指定该消息的过期时间。消息发送后,在消息过期后若还没有被消费则会被清除
4️⃣JMSPriority:消息的优先级,有0-9十个级别,0-4是普通消息,5-9是加急消息。JMS不要求MQ严格按照这十个优先等级发送消息,但必须保证加急消息先于普通消息到达目的地,默认的消息优先级是4级
5️⃣JMSMessageID:唯一识别每个消息的标识,默认有MQ产生,也可以自定义
2、Message的消息体
封装具体的消息数据,有5种消息体格式:TextMessage(字符串,可用来传送Json串)、MapMessage(键值对)、BytesMessage(二进制数组)、StreamMessage(流)、ObjectMessage(可序列化的Java对象),需要注意发送和接收的消息体格式必须一致
3、消息属性
可看作消息头的补充,如果需要除消息头字段以外的值来标识消息,还可以给这个消息设置消息属性,也是键值对的形式——setXxxProperty(k,v),消息属性对于消息的识别、去重、重点标注等非常有用
三、消息的可靠性
消息的可靠性通过三个方面保证:持久化、事务、签收机制。
1、消息的持久化
消息的持久化是通过设置DeliveryMode实现的,DeliveryMode有两种模式:
DeliveryMode.PERSISTENT:持久化,服务器宕机重启后消息依然存在
DeliveryMode.NON_PERSISTENT:非持久化,服务器宕机再重启消息将不存在
Queue中的消息默认是持久化的,若想将其置为非持久化,需要显示的将持久化模式置为DeliveryMode.NON_PERSISTENT。
Topic中的消息默认是非持久化的,因为消息的持久化主要对于消费者端起作用,而对于Topic中的消息消费者端只能接收到订阅时间节点之后的消息(无论是采用MessageConsumer消费消息的方式还是采用TopicSubscriber消费消息的方式都是如此),因此对于消费者端而言持久化订阅之前的垃圾消息是没有意义的,所以Topic中的消息默认是非持久化的。
但是,消费者端采用MessageConsumer和采用TopicSubscriber消费消息是不同的,不同之处在于ActiveMQ并不会将MessageConsumer对象持久化(也就无法记录时间节点),可是会将TopicSubscriber对象持久化,如果将TopicSubscriber持久化的话ActiveMQ是可以记录每个Topic的订阅者的订阅时间节点的,这样的话我们就会有消费所有订阅时间节点之后的消息的需求,而在消费者端订阅了Topic之后是有可能掉线(比如消费者端宕机等)的,那掉线的时间段中产生的消息如果不持久化则订阅者是无法消费的,因此Topic中的消息在消费者端采用TopicSubscriber方式消费消息时是需要持久化的
。
消息的持久化和消息的订阅模式是完全不同的两个概念,它们之间相互是没有任何关系的,只不过消息的持久化是否有意义需要参考消息的消费方式。
Topic中消息的持久化:将Topic中的消息置了持久化
public class MQProducer {
//MQ服务的地址
private static final String MQ_URL = "tcp://192.168.2.107:61616";
//Topic的名称
private static final String TOPIC_NAME = "topic01";
public static void main(String args[]) throws JMSException {
//连接MQ服务
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(MQ_URL);
//创建连接
Connection connection = factory.createConnection();
//开启连接
connection.start();
//创建session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//创建Topic
Topic topic = session.createTopic(TOPIC_NAME);
//创建消息生产者
MessageProducer producer = session.createProducer(topic);
//设置为可持久化
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
//向Topic中发送消息
for (int i = 0; i < 3; i++) {
//生产消息
TextMessage textMessage = session.createTextMessage("msg" + i);
//发送消息
producer.send(textMessage);
}
//关闭资源
producer.close();
session.close();
connection.close();
}
}
消费者端采用主题订阅(TopicSubscriber)的方式消费消息:采用TopicSubscriber的方式时必须给定一个ClientID作为订阅者的唯一标识
public class MQConsumer {
private static final String MQ_URL = "tcp://192.168.2.107:61616";
//主题名称要和生产者一致
private static final String TOPIC_NAME = "topic01";
public static void main(String args[]) throws JMSException {
//连接服务
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(MQ_URL);
//创建连接
Connection connection = factory.createConnection();
//采用TopicSubscriber的方式时必须给定一个ClientID作为订阅者的唯一标识
connection.setClientID(UUID.randomUUID().toString());
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
//创建主题订阅
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remarks");
//监听并消费消息
topicSubscriber.setMessageListener((message) -> {
if (message != null) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
// consumer.close();
// session.close();
// connection.close();
}
}
注意
:
1️⃣采用TopicSubscriber消费消息时需要先订阅主题(先启动消费者端向MQ服务定阅主题),才能收到订阅后的主题中的消息
2️⃣采用TopicSubscriber消费消息时必须给connection指定一个ClientID作为订阅的唯一标识
connection.setClientID(UUID.randomUUID().toString());
3️⃣不管是Queue还是Topic中的消息其持久化模式都可以在两个地方设置:一个是调用生产者的MessageProducer.setDeliveryMode()方法——会改变该生产者生产的所有消息的持久化模式,除非单独为某个消息设置了持久化模式;一个是调用消息的Message.setJMSDeliveryMode(),只设置这一条消息的持久化模式
2、消息的事务
在通过Connection创建Session的时候我们可以通过传参的方式指明这个Session下的消息生产者和消息消费者是否以事务的方式发送和消费消息:
//第一个参数控制事务:true-以事务的方式发送消息 false-以非事务的方式发送消息
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
这里说的事务其实和JDBC中的事务是一个概念,都是将事务中的操作作为一个原子的整体,要么一次性全部提交,要么全部回滚。以事务的方式和以非事务的方式发送和消费消息的不同之处在于:若以事务的方式发送和消费消息,则需要显示的提交和回滚事务,在事务未提交之前是不会生效的;而以非事务的方式发送和消费消息则不需要显示提交事务(此时不存在回滚),此时所做的每一步操作都是直接提交生效的(因此不存在回滚)。举例来说:对于生产者而言,若以事务的方式发送消息到MQ服务器,则在未执行事务提交操作之前在MQ服务器中是看不到消息的,要等到执行提交动作时将整个事务中的操作作为一个原子操作一次性提交后才能看到消息,也就是说事务不提交生产的消息不会发送;对于消费者而言,若以事务的方式消费消息,则在未提交事务之前相当于消息未消费,只有事务提交了才认为消息被消费了,因此在消费者端若以事务的方式消费消息而未将事务提交时(比如事务提交之前出现了异常)就会存在消息的重复消费问题。
示例:
1️⃣生产者以事务的方式发送消息:
public class MQProducer {
private static final String MQ_URL = "tcp://192.168.2.107:61616";
private static final String TOPIC_NAME = "topic01";
public static void main(String args[]) throws JMSException {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(MQ_URL);
Connection connection = factory.createConnection();
connection.start();
//创建session时开启事务
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
//创建消息生产者
MessageProducer producer = session.createProducer(topic);
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
try {
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("msg" + i);
producer.send(textMessage);
}
//提交事务
session.commit();
} catch (Exception e) {
//回滚事务
session.rollback();
} finally {
producer.close();
session.close();
connection.close();
}
}
}
2️⃣消费者以事务的方式消费消息:
public class MQConsumer {
private static final String MQ_URL = "tcp://192.168.2.107:61616";
private static final String TOPIC_NAME = "topic01";
public static void main(String args[]) throws JMSException {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(MQ_URL);
Connection connection = factory.createConnection();
connection.setClientID(UUID.randomUUID().toString());
connection.start();
//以事务的方式消费消息 true
Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, "remarks");
try {
topicSubscriber.setMessageListener((message) -> {
if (message != null) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
//提交事务
session.commit();
} catch (Exception e) {
//回滚事务
session.rollback();
}
// consumer.close();
// session.close();
// connection.close();
}
}
}
3、消息的签收
消息的签收是消息被消费的标志,消息的签收机制一定程度上来说是为了避免消息的重复消费问题,因此消息的签收偏重于消费者端,在生产者端几乎是没有意义的。
消息的签收对消息的影响:
1️⃣对于队列中的消息而言,一旦消息被签收则这条消息的状态就会从待消费状态(Pending Messages )变为已消费状态(Messages Dequeued )而从待消费队列中移除;
2️⃣对于Topic中的消息而言,若采用MessageConsumer的消费模式则消息的签收机制是没有任何意义的,因为MessageConsumer只能消费Topic中自消费者端在MQ服务器注册之后推送到Topic中的消息,至于这之前的消息签收与否MessageConsumer不关心(因为看不到之前的消息),也就是说使用MessageConsumer消费Topic中的消息时是不会存在重复消费的问题的;
3️⃣对于Topic中的消息而言,若采用TopicSubscriber的模式消费消息时签收机制可以避免重复消费消息的作用就凸显出来了,此时消息的签收将会作为某个订阅者(以Connection的ClientID作为标识)已消费过Topic中的某个消息的标志,也就是说消费者每次上线后都只会消费订阅的Topic中未被签收的消息,已签收的消息则不会被重复消费
消息的签收机制有四种:
1️⃣Session.AUTO_ACKNOWLEDGE:值为1,自动签收,消费一条签收一条
2️⃣Session.CLIENT_ACKNOWLEDGE:值为2,客户端手动签收,需显示调用Message.acknowledge()方法完成签收
3️⃣Session.DUPS_OK_ACKNOWLEDGE:值为3,和Session.AUTO_ACKNOWLEDGE差不多,有些说法是该参数的意思是将消息的签收延迟
4️⃣Session.SESSION_TRANSACTED:值为4,以事务的方式签收
事务对消息签收的影响:消息签收是事务控制的一部分
1️⃣若创建Session时是以事务的方式创建的,则对于消息的签收是有影响的,此时只要事务提交就会将所有消息的签收状态置为已签收,无论之前的签收状态是什么;只要事务不提交则消息的签收状态就不起作用(表现上就是队列中已签收的消息不会被移出待消费队列等);
2️⃣若创建Session时是以非事务的方式创建的,则不会对消息的签收有任何影响
示例:手动签收
public class MQConsumer {
private static final String MQ_URL = "tcp://192.168.2.107:61616";
private static final String QUEUE_NAME = "queue01";
public static void main(String args[]) throws JMSException {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(MQ_URL);
Connection connection = factory.createConnection();
connection.start();
//手动签收
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer consumer = session.createConsumer(queue);
consumer.setMessageListener((message) -> {
if (message != null) {
try {
TextMessage textMessage = (TextMessage) message;
//签收
textMessage.acknowledge();
System.out.println(textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
// consumer.close();
// session.close();
// connection.close();
}
}
消息的签收机制虽然解决了Topic在订阅模式下重复消费消息的问题,但是对于队列中重复消费消息的问题并没有解决,举例来说:队列中的消息只能通过MessageConsumer进行消费,在开启事务消费消息的方式下,无论采用哪种签收机制,只要事务提交失败,那就意味着消息消费失败,则就必然会重复消费消息。但是重复消费的消息我们可以借助JMSMessageID来甄别,比如我们将JMSMessageID保存在分布式系统的业务表中,在调用业务方法时先根据JMSMessageID进行校验以避免业务逻辑上的重复操作。