上一讲,我们简单的写了一个ActiveMQ的入门案例。这一节,我们主要学习一下什么是JMS。
1.JMS简介
什么是JMS?JMS(java message service)就是Java消息服务,指两个应用程序之间进行异步通信的API,它为标准协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发。在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。
2.JMS组成结构和特点
JMS Provider
实现JMS借口和规范的消息中间件,也就是我们说的MQ的服务器。
JMS Producer
消息生产者/发布者,创建和发送消息的客户端应用;
JMS Consumer
消息消费者/订阅者,接收和处理JMS消息的客户端应用;
JMS Message
消息。消息又由消息头、消息头和消息属性组成。
消息头:
-JMSDestination:消息发送的目的地;
-JMSDeliveryMode:消息的发送模式。一种是持久模式,指消息在被发送时JMS服务器奔溃,则该条消息不会被丢弃,当服务器恢复之后,会再次传递。另一种是非持久模式,JMS服务器奔溃时,消息会被丢失。
-JMSExpiration:消息的过期机制;默认是永不过期;消息过期时间等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期。如果发送后,在消息过期时间之后还没有被发送到目的地,则该消息被清除。
-JMSPriority:消息优先级;从0-9十个级别,0-4是普通消息5-9是加急消息。JMS不要求MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。
-JMSMessageID:唯一标识每个消息,由MQ服务器产生。
消息体:
1)封装消息的具体内容。
2)有五种消息格式:
-TxtMessage:普通字符串消息,包含一个String;
-MapMessage:一个Map类型的消息,key为String类型,而值为java基本类型。
-BytesMessage:二进制数组类型,包含一个byte[];
-SteamMessage:java数据流消息,用标准六操作,来顺序填充和读取。
-ObjectMessage:对象消息,包含一个可序列化的java对象。
3)发送和接收的消息体类型必须保持一致。
消息属性:
如果需要除消息字段以外的值,那么可以使用消息属性
3.JMS的可靠性:
1)持久化persistent
消息的持久化是指当MQ服务器宕机后,消息也不会丢失。当服务器恢复后,消息依然可以被消费到。
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT)
消息的非持久化是指当MQ服务器宕机后,消息会丢失。
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT)
代码演示:
生产者
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 演示队列的持久化和非持久化
* @author sj
*/
public class QueueProducer1 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) throws JMSException {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("queue1");
MessageProducer producer = session.createProducer(destination);
Message message = session.createTextMessage("这是一个持久化消息");
producer.send(message);
producer.close();
session.close();
connection.close();
}
}
我们先启动生产者:
现在有一条消息等待消费,然后我们手动停止MQ服务,通过命令:
[root@CentOS122 bin]# ./activemq stop
发现服务器已停止,然后重启:
[root@CentOS122 bin]# ./activemq restart
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
ActiveMQ not running
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
INFO: Starting - inspect logfiles specified in logging.properties and log4j.properties to get details
INFO: pidfile created : '/usr/local/activemq/data/activemq-CentOS122.pid' (pid '13455')
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
ActiveMQ is running (pid '13455')
再去控制台看消息情况:
发现消息依然存在,未被消费,也没有丢弃。
消费者:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
/**
* @author sj
*/
public class QueueConsumer1 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) throws JMSException, IOException {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD,MQ_BROKETURL);
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("queue1");
MessageConsumer consumer = session.createConsumer(destination);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if(message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage);
}
}
});
System.in.read();
consumer.close();
session.close();
connection.close();
}
}
启动消费者,发现可以获取到消息。
注意:
持久化和非持久化状态下,只要MQ服务器没有宕机,即使没有消费者消费消息,消息也不会丢失。Queue默认的采用持久化。
2)事务
消费者和生产者的事务,完全没有关联,各自是各自的事务。
(1)生产者开启事务后,执行commit方法,这批消息才真正的被提交。不执行commit方法,这批消息不会提交。执行rollback方法,之前的消息会回滚掉。
生产者的事务机制,要高于签收机制,当生产者开启事务,签收机制不再重要。
代码演示:
消费者:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueProducer2 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = null;
Session session = null;
Destination destination;
MessageProducer producer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
destination = session.createQueue("queue2");
producer = session.createProducer(destination);
Message message = session.createTextMessage("这是一个事务消息");
producer.send(message);
// for (int i = 0; i < 3; i++){
// if(i == 1){
// throw new RuntimeException("这里异常了!");
// }
// }
session.commit();
} catch (JMSException e) {
try {
session.rollback();
} catch (JMSException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
try {
producer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
当我们没有开启异常时,启动结果如下:
当我们把异常代码放开,在执行时,得到结果如下:
还是只有一条消息。说明事务回滚了,这条消息没有发送成功。
3)签收
(1)签收的几种方式
自动签收:Session.AUTO_ACKNOWLEDGE。这种方式是默认的。这种方式无线消费者做任何操作,框架都会帮我们自动签收消息。
手动签收:Session.CLIENT_ACKNOWLEDGE。这种方式需要消费者客户端手动调用Message.acknowledge()方法,来签收消息。如果不签收消息,消息会被消费者反复消费,知道被签收。
允许重复消息:Session.DUPS_OK_ACKNOWLEDGE。多线程或者多个消费者同事消费一个消息时,因为线程不安全,可能会重复消费。这种方式比较少用到。
事务下签收:Session.SESSION_TRANSACTED。开始事务的情况下,可以使用这种方式。也比较少用到。
代码演示:
生产者:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueProducer3 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = null;
Session session = null;
Destination destination;
MessageProducer producer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
destination = session.createQueue("queue3");
producer = session.createProducer(destination);
Message message = session.createTextMessage("这是一个需要手动签收的消息");
producer.send(message);
} catch (JMSException e) {
try {
session.rollback();
} catch (JMSException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
try {
producer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
消费者:
首先不签收消息:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
public class QueueConsumer3 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = null;
Session session = null;
Destination destination;
MessageConsumer consumer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
destination = session.createQueue("queue3");
consumer = session.createConsumer(destination);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if(message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage);
// try {
// textMessage.acknowledge();
// } catch (JMSException e) {
// e.printStackTrace();
// }
}
}
});
System.in.read();
} catch (JMSException | IOException e) {
e.printStackTrace();
}finally {
try {
consumer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
当消费者接受到消息未手动签收时,查看控制台,发现消息显示被消费:
而MQ控制台显示消息却没有出队,
当我们再次启动消费者,发现消息依旧可以被消费。所以当我们开启手动签收的方式,消费者却没有签收时,消息被重复消费了。
现在我们开启消费者签收。调用textMessage.acknowledge()方法。
发现消息就可以正常消费,并且没有重复消费的情况。
(2)事务和签收的关系
事务性会话中,当一个事务被成功提交则消息被自动签收(针对于消费者);如果事务回滚,则消息会被再次传送。事务优先于签收,开始事务后,签收机制不在起作用;
非事务性会话中,消息何时被确认取决于创建回话时的应答模式;
生产事务开启,只有commit后才能将全部消息变为已消费。
事务偏向于生产者,签收偏向于消费者;也就是说,生产者使用事务更好一点,消费者使用签收机制更好一些。
4.点对点模式总结
点对点模型是基于队列的,生产者发送消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能。和我们平时给朋友发送短信类似。
1)如果在Session关闭时有部分消息被收到但还没有被签收(acknowledge),那当消费者下次连接到相同的队列时,这些消息还会被再次接收
2)队列可以长久的保存消息直到消费者收到消息。消费者不需要因为担心消息会丢失而时刻和队列保持激活的链接状态,充分体现了异步传输模式的优势
5.发布订阅模式总结
JMS Pub/Sub 模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic
主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息。
主题使得消息订阅者和消息发布者保持互相独立不需要解除即可保证消息的传送。
1)持久订阅
客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ的时候,会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
当持久订阅状态下,不能恢复或重新派送一个未签收的消息。
持久订阅才能恢复或重新派送一个未签收的消息。
2)非持久订阅
非持久订阅只有当客户端处于激活状态,也就是和MQ保持连接状态才能收发到某个主题的消息。
如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到。
通俗来说:先订阅注册才能接受到发布,只给订阅者发布消息。
如何选择:
当所有的消息必须被接收,则用持久订阅。当消息丢失能够被容忍,则用非持久订阅
-