目录
1.什么是JMS
在解释JMS之前,我们先来看什么是JavaaSE和JavaEE
JavaSE是Java这门编程语言的基础语法部分
JavaEE是一套使用Java进行企业级应用开发的大家一致遵循的13个核心规范工业标准,JavaEE平台提供了一个基于组件的方法来加快设计、开发、装配及部署企业级应用程序
1 | JDBC(Java Database) | 数据库连接 |
2 | JNDI(Java Naming and Directory Interfaces) | Java的命名和目录接口 |
3 | EJB (Enterprise JavaBean) | |
4 | RMI(Remote Method Invoke) | 远程方法调用 |
5 | Java IDL(Interface Description Languag)/ CORBA(Common Object Broker Architecture) | 接口定义语言/公用对象请求代理程序体系结构 |
6 | JSP(Java Server Pages) | |
7 | Servlet | |
8 | XML(Extensible Markup Language) | 可扩展性标记语言 |
9 | JMS(Java Message Service) | Java消息服务 |
10 | JTA(Java Transaction API) | Java事务API |
11 | JTS(Java Transaction Service) | Java事务服务 |
12 | JavaMail | |
13 | JAF(JavaBean Activation Framework) |
接下来解释什么是Java消息服务(JMS)?
- Java消息服务指的是两个应用程序之间进行异步通信的API,它为标准消息协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发,在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间并不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步/削峰的效果
2.四种MQ的落地产品的对比
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
单机吞吐量 | 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 | 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 | 10万级,RocketMQ也是可以支撑高吞吐的一种MQ | 10万级别,这是kafka最大的优点,就是吞吐量高。
一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
topic数量对吞吐量的影响 |
|
| topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降
这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic | topic从几十个到几百个的时候,吞吐量会大幅度下降
所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源 |
时效性 | ms级 | 微秒级,这是rabbitmq的一大特点,延迟是最低的 | ms级 | 延迟在ms级以内 |
可用性 | 高,基于主从架构实现高可用性 | 高,基于主从架构实现高可用性 | 非常高,分布式架构 | 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 有较低的概率丢失数据 | 经过参数优化配置,可以做到0丢失 | 经过参数优化配置,消息可以做到0丢失 |
消息重复 | 可能有 | 可控制 | 理论上有重复 | 可能有 |
功能支持 | MQ领域的功能极其完备 | 基于erlang开发,所以并发能力很强,性能极其好,延时很低 | MQ功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 |
优劣势总结 | 非常成熟,功能强大,在业内大量的公司以及项目中都有应用
偶尔会有较低概率丢失消息
而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本
而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用
| erlang语言开发,性能极其好,延时很低;
吞吐量到万级,MQ功能比较完备
而且开源提供的管理界面非常棒,用起来很好用
社区相对比较活跃,几乎每个月都发布几个版本分
在国内一些互联网公司近几年用rabbitmq也比较多一些
但是问题也是显而易见的,RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。
而且erlang开发,国内有几个公司有实力做erlang源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。
而且rabbitmq集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码,很难定制和掌控。 | 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障
日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是ok的,还可以支撑大规模的topic数量,支持复杂MQ业务场景
而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以掌控
社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码
还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ挺好的 | kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展
同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量
而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略
这个特性天然适合大数据实时计算以及日志收集 |
3.JMS的四大组成元素及特点
四大组成:
- JMS provider:实现JMS接口和规范的消息中间件,也就是MQ服务器,如ActiveMQ,RocketMQ等等
- JMS producer:消息生产者,创建和发送JMS消息的客户端应用
- JMS consumer:消息消费者,接收和处理JMS消息的客户端应用
- JMS message:消息
接下来详细介绍JMS message
一个消息由三部分组成:消息头、消息体、消息属性
消息头:
- JMSDestination
- 消息发送的目的地,主要是Queue和Topic
- JMSDeliveryMode
- 持久和非持久模式
- 一条持久性的消息:应该被传送“一次仅仅一次”,这就意味着如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递
- 一条非持久的消息:最多会传送一次,这就意味着服务器出现故障,该消息将永远丢失
- JMSExpiration
- 可以设置消息在一定时间后过期,默认永不过期
- 消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值
- 如果timeToLive值等于0,则JMSExpiration被设为0,表示该消息永不过期
- 如果发送后,在消息过期时间之后消息还没有被发送到目的地,则消息被清除
- JMSPriority
- 消息优先级,从0-9十个级别,0到4是普通消息,5到9是加急消息
- JMS不要求MQ严格按照这10个优先级发送消息,但必须保证加急消息要先于普通消息到达,默认是4级
- JMSMessageID
- 唯一识别每个消息的标识,由MQ产生
消息体:封装消息的数据
- 5种消息体格式
- TextMessage:普通的字符串消息,包含一个String
- MapMessage:一个Map类型的消息,key为String类型,而值为Java的基本类型
- BytesMessage:二进制数组消息,包含一个byte[]
- StreamMessage:Java数据流消息,用标准流操作来顺序的填充和读取
- ObjectMessage:对象消息,包含一个可序列化的Java对象
- 发送和接收的消息体类型必须一致
消息属性:
- 如果需要除消息头字段以外的值,那么可以使用消息属性
- 识别/去重/重点标注等操作非常有用的方法
- 它们是以属性名和属性值对的形式制定的,可以将属性是作为消息头的扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器
- 消息的属性就像可以分配给一条消息的附加消息头一样,它们允许开发者添加有关消息的不透明附加信息,它们还用于暴露消息选择器在消息过滤时使用的数据
- 例如
- 生产者
- 消费者
4.消息可靠性
消息的可靠性有4个角度
- 持久性
- 事务
- 签收
- 多节点集群
4.1 持久性
参数说明:
- 非持久性:
- messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
- 当服务器宕机,消息不存在
- 例如:
- 对于队列,当我们把生产者的持久化模式设置为非持久化模式时,服务器宕机重启后,消息丢失
- 持久性:
- messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
- 当服务器宕机,消息依然存在
持久化的队列:
对于队列,当我们把生产者的持久化模式设置为持久化模式时,服务器宕机重启后,消息仍然存在
当我们不手动设置持久化模式的时候,
- 结论:
- 队列的默认传送模式是持久化模式,此模式保证这些消息只被传送一次和成功使用一次,对于这些消息,可靠性是优先考虑的因素
- 可靠性的另一个重要方面是确保持久性消息传递至目标之后,消息服务在向消费者传送它们之前不会丢失这些数据
持久化的Topic:
生产者
package cn.cqu.activemq.topic;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JmsProducer {
private static final String ActiveMQ_URL = "tcp://192.168.43.63:61616";
private static final String TOPIC_NAME = "Topic_Persist";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂,按照给定的Url地址,采用默认用户名和密码
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
//2.通过连接工厂,获得Connection连接并启动访问(此处为javax.jms.Connection)
Connection connection = connectionFactory.createConnection();
//3.创建会话Session
//两个参数:第一个:事务 第二个:签收
Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(队列还是主题)
Topic topic = (Topic) session.createTopic(TOPIC_NAME);
//5.创建消息的生产者
MessageProducer messageProducer = session.createProducer(topic);
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
//连接启动放在这里是为了启动持久化的topic
connection.start();
//6.通过使用messageProducer生产3条消息发送到MQ的队列当中
for (int i = 0; i < 3; i++) {
//7.通过Session创建Message
TextMessage textMessage = session.createTextMessage("msg----"+i); //可以理解为发送一个字符串给队列
//8.通过messageProducer发送给队列
messageProducer.send(textMessage);
}
//9.关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("消息发布完成");
}
}
消费者
package cn.cqu.activemq.topic;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
public class JmsConsumer01 {
private static final String ActiveMQ_URL = "tcp://192.168.43.63:61616";
private static final String TOPIC_NAME = "Topic_Persist";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("消费者1");
//1.创建连接工厂,按照给定的Url地址,采用默认用户名和密码
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
//2.通过连接工厂,获得Connection连接并启动访问(此处为javax.jms.Connection)
Connection connection = connectionFactory.createConnection();
connection.setClientID("消费者1");
//3.创建会话Session
//两个参数:第一个:事务 第二个:签收
Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(队列还是主题)
Topic topic = session.createTopic(TOPIC_NAME);
TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,"remark...");
connection.start();
Message message = topicSubscriber.receive();
while (null != message){
TextMessage textMessage = (TextMessage) message;
System.out.println("收到的持久化topic:"+textMessage.getText());
message = topicSubscriber.receive(5000L);
}
session.close();
connection.close();
}
}
先启动消费者
再启动生产者
接下来我们演示持久化
先启动消费者
- 这时候消费者订阅了持久化topic
然后关闭消费者
这时启动生产者
- 有一个消费者曾经注册过,生产者发送了3条消息,此时消费者1在离线状态
然后再次启动消费者
- 可以发现消息被消费者重新接收到
总结:
- 对于持久化的topic,只要先运行一次消费者,等于向MQ进行了注册,订阅了这个主题,然后再运行生产者发送消息,无论消费者是否在线,都会接收到,不在线的话,下次连接的时候,会把没有收过的消息都接收下来
- 以上这种就类似于微信公众号,只要你订阅了公众号后发的消息,即便你不在线,在你上线以后消息也会马上推送给你
4.2 事务
- 事务偏生产者,签收偏消费者
- false
- 只要执行send,就进入到队列中
- 关闭事务,那第2个签收参数的设置需要有效
- true
- 先执行send再执行commit,消息才被真正地提交到队列中
- 消息需要批量发送,需要缓冲区处理
生产者的事务:
如果设置为true,没有执行commit
- 可以看到消息即便被send,也没有发送到队列
所以只要为true,一定要使用commit提交
- commit以前,所以send的消息都会放在缓冲当中,在commit时,一起将它们提交,这样就可以实现批量地提交
为什么要加事务?
- 为了保证,批量的send在正常的情况下会一起提交,异常的情况可以回滚,整个操作具有一致性
- 没有事务,就会每次send都提交,无法保证一致性
try {
session.commit();
}catch (Exception e){
session.rollback();
e.printStackTrace();
}finally {
session.close();
}
消费者的事务
如果设置为true,没有执行commit
可以看到明明消费了,在MQ而言,它还是未消费的
当我们关闭消费者再次运行一次
可以发现还会再次消费,即出现了重复消费
所以也要在消费之后,进行提交,保证MQ知道消息已被消费,即提交所有的消费记录给MQ,当有一个没有消费成功,我们也可以进行回滚
而前面将事务置为false,是每消费一个,就会提交给MQ,这样的单个操作本身就是原子的
4.3 签收
以下所使用到的生产者和消费者的代码:
生产者:
package cn.cqu.activemq.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class JmsProducer {
private static final String ActiveMQ_URL = "tcp://192.168.43.63:61616";
private static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂,按照给定的Url地址,采用默认用户名和密码
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
//2.通过连接工厂,获得Connection连接并启动访问(此处为javax.jms.Connection)
Connection connection = connectionFactory.createConnection();
connection.start();
//3.创建会话Session
//两个参数:第一个:事务 第二个:签收
Session session = connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(队列还是主题)
Queue queue = session.createQueue(QUEUE_NAME);
//5.创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
//6.通过使用messageProducer生产3条消息发送到MQ的队列当中
for (int i = 0; i < 3; i++) {
//7.通过Session创建Message
TextMessage textMessage = session.createTextMessage("msg----"+i); //可以理解为发送一个字符串给队列
//8.通过messageProducer发送给队列
messageProducer.send(textMessage);
}
session.commit();
//9.关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println("消息发布完成");
}
}
消费者:
package cn.cqu.activemq.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
public class JmsConsumer {
private static final String ActiveMQ_URL = "tcp://192.168.43.63:61616";
private static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1.创建连接工厂,按照给定的Url地址,采用默认用户名和密码
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(ActiveMQ_URL);
//2.通过连接工厂,获得Connection连接并启动访问(此处为javax.jms.Connection)
Connection connection = connectionFactory.createConnection();
connection.start();
//3.创建会话Session
//两个参数:第一个:事务 第二个:签收
Session session = connection.createSession(true,Session.CLIENT_ACKNOWLEDGE);
//4.创建目的地(队列还是主题)
Queue queue = session.createQueue(QUEUE_NAME);
//5.创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
while (true){
TextMessage textMessage = (TextMessage) messageConsumer.receive(4000L);
if(textMessage != null){
System.out.println("消费者接收消息:"+textMessage.getText());
}else {
break;
}
}
session.commit();
//等待上述代码执行完,当我们在键盘随便输入一个字符的时候,这个方法执行完
messageConsumer.close();
session.close();
connection.close();
}
}
非事务下:
- 签收模式有3种:
- 自动签收(默认):即
- 手动签收:即
- 客户端需要调用acknowledge方法手动签收
- 演示:
- 消费者设置为手动签收
- 先启动生产者
- 再启动消费者
- 关闭消费者再次启动消费者
- 可以看到设置为手动签收,没有调用acknowledge方法的话,会出现重复消费
- 使用acnowledge方法对每条消息进行手动签收
- 再此按照以上步骤,直接看最终结果为:
- 允许重复消息:即
- 自动签收(默认):即
事务下的签收:
- 演示1:
- 有事务并且commit但手动签收,不用acknowledge方法进行签收
- 先启动生产者,再多次启动消费者,同上
- 即没有出现重复消费
- 所以在事务中只要commit后,就会自动签收(即使我们设置了手动签收)
- 演示2:
- 有事务但没有commIt,手动签收,使用acknowledge方法签收
- 使用上述同样的方法,可以发现重复签收
总结:签收和事务的关系
- 在事务性会话中,当一个事务被成功提交则消息被自动签收。如果事务回滚,则消息会被再次传送
- 在非事务性会话中,消息何时被确认取决于创建会话时的签收模式
4.4 多结点集群
见我的后续博文,待补充链接
5.点对点和发布订阅的总结
5.1 点对点总结
- 点对点模型是基于队列的,生产者发消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能,和我们给朋友发短信类似
- 1.如果在Session关闭时有部分消息已被收到但没有被签收(acknowledged),那当消费者下次连接到相同的队列时,这些消息还会再次接收
- 2.队列可以长久地保存消息直到消费者收到消息,消费者不需要因为担心消息丢失而时刻和队列保持激活的连接状态,充分体现了异步传输模式的优势
5.2 发布订阅总结
- JMS Pub/Sub模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic
- 主题可以被认为是消息的中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息
- 主题是的消息订阅者和消息发布者保持互相独立,不需要接触即可保证消息的传送
非持久订阅:
- 非持久订阅只有当客户端处于激活状态,也就是MQ保持连接状态才能收到发送到某个主题的消息
- 如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到
- 先订阅注册才能接收到发布的消息,生产者只给订阅者发布消息
持久订阅:
- 客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ时会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
- 非持久订阅状态下,不能恢复或重新派送一个未签收的消息
- 持久订阅才能恢复或重新派送一个未签收的消息
当所有的消息必须被接收,则用持久订阅。当丢失消息能够被容忍,则可以用非持久订阅。