前言
-
消息
- 微信
- 短信
- 语音
-
中间件
-
MQ产品种类
- Kafka
- RabbitMQ
- RocketMQ
- ActiveMQ(技术维度)
- API发送和接收
- MQ的高可用性
- MQ的集群和容错配置
- MQ的持久化
- redis(RDB和AOF)
- 延时发送/定时投递
- 签收机制
- Spring整合
- 编程语言:java
- …
一.入门概述
1.从生活case到实际生产案例
-
微服务架构后,链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(子模块),比如模块A调用模块B,模块B调用模板C,模块C调用模块D。但在大型分布式应用中,系统间的RPC交互繁杂,一个功能背后可能要调用上百个接口并非不可能,从单机架构过渡到分布式微服务架构的通例,这种架构会有什么问题?
-
系统之间直接调用实际工程落地和存在的问题
- 系统之间接口耦合比较严重
- 面对大流量并发时,容易被冲垮
- 等待同步存在性能问题
- 你如何解决?
-
每新增一个下游功能,都要对上有的相关接口进行改造
- 举个例子:系统A要发送数据系统B和C,发送给每个系统的数据可能有差异,因此系统A对发送给每个系统的数据进行组装,然后逐一发送:当代码上线后又新增了一个需求:把数据也发送给D,新上了一个D系统也要接受A系统的数据。此时就需要修改A系统,让它感知到D的存在,同时把数据处理好再A给D,在这个过程中你会看到,每接入一个下游系统,都要对A系统进行代码改造,开发联调的效率很低。其整体架构如下图
- 举个例子:系统A要发送数据系统B和C,发送给每个系统的数据可能有差异,因此系统A对发送给每个系统的数据进行组装,然后逐一发送:当代码上线后又新增了一个需求:把数据也发送给D,新上了一个D系统也要接受A系统的数据。此时就需要修改A系统,让它感知到D的存在,同时把数据处理好再A给D,在这个过程中你会看到,每接入一个下游系统,都要对A系统进行代码改造,开发联调的效率很低。其整体架构如下图
-
每个接口模块吞吐能力有限,上线能力如果堤坝,当大流量(洪水)来临时,容易被冲垮
- 秒杀系统:上游系统发起下单购买操作,就是下单一个操作,下游系统完成秒杀业务逻辑(读取订单,库存检查,库存冻结,余额检查,余额冻结,订单生成,余额扣减,库存扣减,生成流水,余额解冻,库存解冻)
-
RPC接口基本上是同步调用,整体的服务性能遵循"木桶理论",即整体系统的耗时取决于链路中最慢的那个接口
- 比如A调用B/C/D都是50ms,但此时B又调用了B1,花费2000ms,那么直接就拖累了整个服务性能
- 比如A调用B/C/D都是50ms,但此时B又调用了B1,花费2000ms,那么直接就拖累了整个服务性能
-
消息中间件就可以摆平上面三种情况
- 要做系统解耦,当心的模块接进来时,可以做到代码改动最小:能够解耦
- 设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮;能够削峰
- 强弱依赖梳理能够将非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够一步
2.是什么
A.定义
- 是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的继承
- 通过提供消息传递和消息排队模型在分布式环境下提供应用解耦,弹性伸缩,冗余存储,流量削峰,异步通信,数据同步等功能
- 大致过程如下:
- 发送者把消息发送给消息服务器,消息服务器将消息存放在若干队列/主题topic中,在合适的时候,消息服务器会将消息转发给接受者,在这个过程中,发送和接受是异步的,也就是发送无需等待,而且发送者和接受者的生命周期也没有必然关系;
- 尤其在发布pub/订阅sub模式下,也可以完成一对多的通信,即让一个消息有多个接受者
B.特点
a.采用异步处理模式
- 消息发送者可以发送一个消息而无须等待相应。消息发送者将消息发送到一条虚拟的通道(主题或队列)上
- 消息接收者则订阅或监听该通道。一条信息可能最终转发给一个或多个消息接收者,这些接收者都无需对消息发送者做出同步回应。整个过程都是异步的
- 也就是说,一个系统跟另一个系统之间进行通信的时候,假如系统A希望发送一个消息给系统B,让他去处理。但是系统A不关注系统B到底怎么处理或者有没有处理好,所以系统A把消息发送给MQ,然后就不管这条消息的"死活"了,接着系统B从MQ里消费出来处理即可。至于怎么处理,是否处理完毕,什么时候处理,都是系统B的事儿,与系统A无关
- 这样的一种通信方式,就是所谓的"异步"通信方式对于系统A来说,只要把消息发送MQ,然后系统B就会异步的去进行处理,系统A不需要“同步”的等待系统B处理完。这样好处就是解耦
b.应用系统之间解耦合
- 发送者和接受者不必了解对方,只需要确认消息
- 发送者和接受者不必同时在线
3.能干嘛
- 解耦
- 削峰
- 异步
4.去哪下
- Kafka
- 编程语言:java/scala
- RabbitMQ
- erlang
- RocketMQ(大型项目)
- java
- ActiveMQ(中小型项目)
- java
- 阿里内部自己研发Notify
5.怎么玩
- 最主要的功能
- 实现高可用,高性能,可伸缩,易用和安全的企业级面向消息服务的系统
- 异步消息的消费和处理
- 控制消息的消费顺序
- 可以和spring/springboot整合简化编码
- 配置集群容错的MQ集群
- …
二.ActiveMQ安装和控制台
1.官网下载
- 下载好了丢到Linux服务器opt目录下(放第三方软件包)
2.Linux安装
tar -zxvf xxx
解压mkdir /myactiveMQ
根目录创建myactiveMQ文件夹cp -r xxxx /myactiveMQ/
拷贝过来- 启动命令
./activemq start
,需要jdk - activemq的默认进程端口是61616
ps -ef | grep activemq | grep -v grep
查看
./activemq stop
关闭- 带日志的启动
./activemq start > /myactiveMQ/run_activemq.log
3.Apache ActiveMQ控制台
- 后台已经启动,前台看不到?activemq是有图形化界面
- 访问地址http://ip:8161/admin,默认用户名和密码都是admin
- 8161是它的web前端访问地址,类似于tomcat的8080
- 不关防火墙的情况下,就让防火墙开启8161端口号
firewall-cmd --add-port=8161/tcp --permanent //开放8161端口
firewall-cmd --list-all //查看防火墙开放状态
firewall-cmd --reload //重启防火墙
systemctl stop firewalld //关闭防火墙
- 注意,开启了8161端口还存在着可能访问不到的情况,修改conf的jetty.xml这里,从官网下载下来的这里是127.0.0.1(原来是)
- 一般到这里,就能够访问到ActiveMQ了
4.备注
- 采用61616端口提供JMS服务
- 采用8161端口提供管理控制台服务
三.Java编码实现ActiveMQ通讯
1.建Maven工程
2.pom.xml
3.JMS编码总体架构
- 首先获得连接工厂,由连接工厂获得连接,由连接获得会话,由会话获得msg(消息)
- 会话产生消息生产者,产生消息消费者(像极了微服务的提供者和微服务的消费者)
- 目的地-分为队列和主题
- 对比JDBC操作数据库步骤
- 注册驱动(做一次) Class.forName(“com.mysql.jdbc.Driver”);
- 建立连接 Connection conn = DriverManager.getConnection(…);
- 创建运行的SQL语句 Statement st = conn.createStatement();
- 运行语句 ResultSet rs = st.executeQuery(sql);
- 处理运行结果
- 释放资源
4.粗说目的地Destination队列和主题
- 在点对点的消息传递域中,目的地被称为队列(queue)
- 在发布订阅消息传递域中,目的地被称为主题(topic)
- 点对点是一对一,下面是一对多,发布给订阅了的人
5.点对点的消息传递域中,目的地被称为队列
A.消息生产者编码
- 注意,你需要在LInux中防火墙打开61616端口,不然会报连接错误
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂
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.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Queue queue = session.createQueue(QUEUE_NAME);//Collection collection = new ArrayList();
//5.创建消息的生产者
MessageProducer producer = session.createProducer(queue);
//6.通过使用消息生产者生产3条消息发送到MQ的队列里面
for (int i = 0; i <=3 ; i++) {
//7.创建消息(好比学生按照要求写好的面试题消息)
TextMessage textMessage = session.createTextMessage("msg---" + i);//理解为一个字符串
//8.通过Producer发送给mq
producer.send(textMessage);
}
//9.关闭资源
producer.close();
session.close();
connection.close();
System.out.println("******消息发布到MQ完成");
}
}
- 运行之后访问http://ip:8161,默认admin用户名和密码
- 等待消费的消息:总接受数-总出队列数
- 消费者数量:消费者端的消费者数量
- 进队消息数:进入队列的总数量,包括出队列的。这个数量只增不减
- 出队消息数:可以理解为是消费者消费掉的数量
- 比如,当有一个消息进入这个队列时,等待消费的消息是1,进入队列的消息是1,当消息消费后,等待消费的消息是0,进入队列的消息是1,出队列的消息是1,再来一条消息时,等待消费的消息是1,进入队列的消息就是2
B.消息消费者编码
- 前4步和生产者步骤一样
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂
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.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Queue queue = session.createQueue(QUEUE_NAME);//Collection collection = new ArrayList();
//5.创建消费者
MessageConsumer consumer = session.createConsumer(queue);
while(true){
TextMessage textMessage = (TextMessage) consumer.receive();
if(textMessage!=null){
System.out.println("****消费者接受到的消息:"+textMessage.getText());
}
else{
break;
}
}
consumer.close();
session.close();
connection.close();
}
}
-
receive方法说明
- 你消费者执行完后,代码并没有结束,一直卡在receive方法上,因为它一直在等待着消息
- 它有个带参的,带参是等待时间,你设置带参,收不到消息就过时不候了
-
消息消费者MessageListener方法说明
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
//1.创建连接工厂
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.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Queue queue = session.createQueue(QUEUE_NAME);//Collection collection = new ArrayList();
//5.创建消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
同步阻塞方法receive
订阅者或接受者调用MessageConsumer的receive方法来接收消息,receive方法在能够接收到消息之前(或超时之前)将一直阻塞
while(true){
TextMessage textMessage = (TextMessage) consumer.receive(4000L);
if(textMessage!=null){
System.out.println("****消费者接受到的消息:"+textMessage.getText());
}
else{
break;
}
}
consumer.close();
session.close();
connection.close();*/
//通过监听的方式来接收消息
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if(message!=null&&message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("****消费者接受到的消息:"+textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
System.in.read();//保证控制台不灭,如果不加的话没消费到就关了,根本没有到MQ去消费...
consumer.close();
session.close();
connection.close();
}
}
C.消费者3大消费情况
- 先生产,只启动1号消费者,问题:1号消费者能消费消息嘛?
- 这个自然是能够消费的
- 先生产,启动1号消费者,再启动2号消费者,问题:2号消费者能够消费消息嘛?
- 1号可以消费
- 2号不能消费
- 先启动2个消费者,在生产8条消息,请问,消费情况如何?无非就三种可能
- 2个消费者都8条
- 先到先得,8条全给一个
- 一人一半(经过测试,一人一半,有点负载均衡轮询的样子)
D.总结
- JMS开发的基本步骤
- 两种消费方式
- 同步阻塞方式:订阅者或接收者调用MessageConsumer的receive方法来接收消息,reveive方法在能够接收到消息之前(或超时之前)将一直阻塞
- 异步非阻塞方式(监听器):订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,当消息到达之后,系统自动调用监听器MesageListener的onMessage(Message message)方法
点对点消息传递域的特点如下:
每个消息只能有一个消费者,类似1对1的关系。好比个人快递自己领取自己的
消息的生产者和消费者之间没有时间上的相关性。无论消费者在生产者发送消息的时候是否处于运行状态,消费者都可以提取消息。好比我们呢的发送短信,发送者发送后不见得接收者会即收即看
消息被消费后队列中不会再存储,所以消费者不会消费到已经被消费掉的消息
6.在发布订阅消息传递域中,目的地被称为主题(topic)
- 发布/订阅消息传递域的特点如下:
- 生产者将消息发布到topic中,每个消息可以有多个消费者,属于1:N的关系
- 生产者和消费者之间有时间上的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息
- 生产者生产时,topic不保存消息它是无状态的不落地,加入无人订阅就生产,那就是一条废消息,所以,一般先启动消费者再启动生产者
- JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。一句话,好比我们微信公众号订阅
A.发布主题生产者
public class JmsProduce_Topic {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String TOPIC_NAME = "topic-playMaker";
public static void main(String[] args) throws JMSException{
//1.创建连接工厂
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.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Topic topic = session.createTopic(TOPIC_NAME);
//5.创建消息的生产者
MessageProducer producer = session.createProducer(topic);
//6.通过使用消息生产者生产3条消息发送到MQ的队列里面
for (int i = 0; i < 3 ; i++) {
//7.创建消息(好比学生按照要求写好的面试题消息)
TextMessage textMessage = session.createTextMessage("TOPIC_NAME---" + i);//理解为一个字符串
//8.通过Producer发送给mq
producer.send(textMessage);
}
//9.关闭资源
producer.close();
session.close();
connection.close();
System.out.println("******TOPIC_NAME消息发布到MQ完成");
}
}
B.订阅主题消费者
public class JmsConsumer_Topic {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String TOPIC_NAME = "topic-playMaker";
public static void main(String[] args) throws JMSException, IOException {
//1.创建连接工厂
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.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Topic topic = session.createTopic(TOPIC_NAME);
//5.创建消费者
MessageConsumer consumer = session.createConsumer(topic);
/*consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if(message!=null&&message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("****消费者接受到的消息:"+textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});*/
//拉姆达表达式
consumer.setMessageListener((Message message)->{
if(message!=null&&message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("****消费者接受到的消息:"+textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
System.in.read();//保证控制台不灭
consumer.close();
session.close();
connection.close();
}
}
C.先启动订阅再启动生产,不然发送的消息是废消息
- 启动1号消费者,上面三个先不说,就看看最后一个
- 再启动2号消费者和3号消费者
- 启动生产者,发现每个消费者都有3条
- 我们试试反过来,没有订阅者,先启动发布者,再启动订阅者
- 订阅者收不到之前发送到的消息,当然你启动订阅者后再启动发布者自然能够收到
D.topic和queue的总结
- 两大模式特性和比较
四.JMS规范和落地产品
1.JMS是什么
- JavaEE是一套使用Java进行企业级应用开发的大家一致遵循的13个核心规范工业标准。Java EE平台提供了一个基于组件的方法来加快设计、开发、装配及部署企业应用程序
- JMS(Java Message Service,Java消息服务是JavaEE中的一个技术)
- Java消息服务指的是两个应用程序进行异步通信的API,它为标准消息协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持Java应用程序开发。在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间并不是直接相连的,而是通过一个共同的消息收发服务组件关联起来达到解耦/异步削峰的效果
2.MQ中间件的其它落地产品
- 常用的就Kafka,RocketMQ,ActiveMQ.RabbitMQ主要是erlang的…用的人不多
3.JMS的组成结构和特点
- 四大元素
- JMS provider-实现JMS接口和规范的消息中间件,也就是我们的MQ服务器
- JMS producer-消息生产者,创建和发送JMS消息的客户端应用
- JMS consumer-消息消费者,接收和处理JMS消息的客户端应用
- JMS message
- 消息头
- 消息属性
- 消息体
A.Message之消息头
- 常用
- JMSDestination-消息发送的目的地,主要是指Queue和Topic
- JMSDeliveryMode
持久模式和非持久模式
一条持久性的消息:应该被传送"一次仅仅一次",这就意味者如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之前再次传递
一条非持久的消息:最后会传送一次,这意味这服务器出现故障,该消息将永远丢失
- JMSExpiration
可以设置消息在一定时间后过期,默认是永不过期
消息过期时间,等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值
如果timeToLive值等于零,则JMSExpiration被设为零,表示该消息永不过期
如果发送后,在消息过期时间之后消息还没有被发送到目的地,则该消息被清除
- JMSPriority
消息优先级,从0-9十个级别,0到4是普通消息,5到9是加急消息
JMS不要求MQ严格按照这十个优先级发送消息,但必须保证加急消息要先于普通消息到达。默认是4级
- JMSMessageID
唯一识别每个消息的标识由MQ产生
B.Message之消息体
- 封装具体的消息数据
- 5种消息体格式
- TextMessage-普通字符串消息,包含一个string
- MapMessage-一个Map类型的消息,key为string类型,而值为Java的基本类型
- BytesMessage-二进制数组消息,包含一个byte[]
- StreamMessage-Java数据流消息,用标准流操作来顺序的填充和读取
- ObjectMessage-对象消息,包含一个可序列化的Java对象
- 发送和接受的消息体类型必须一致对应
- 最常用的是TextMessage和MapMessage,由session产生的
C.Message之消息属性
- 如果需要除消息头字段以外的值,那么可以使用消息属性
- 识别/去重/重点标注等操作非常有用的方法
- 是什么
他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器
消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。它们还用于暴露消息选择器在消息过滤时使用的数据
4.JMS的可靠性
- PERSISTENT:持久性
- 事务
- Acknowledge:签收
A.持久性
- 参数设置说明
- redis持久化有两种方式RDB和AOF
非持久:messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
非持久:当服务器宕机,消息就不存在了//队列形式,发送消息,停止activemq服务,再重新启动,消息不存在了,也就是消息丢掉了
持久:messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT);
持久化:当服务器宕机,消息依然存在//队列形式,发送消息,停止activemq服务,再重新启动,消息依然存在,只是消息入队清0了,待消费依然存在,消费者依然能访问到
-
持久的Queue
-
默认是持久还是非持久?
- 实验一样吧,实验表示默认是持久
- 这是队列的默认传送模式,此模式保证这些消息只被传送一次和成功使用一次。对于这些消息,可靠性是优先考虑的因素。可靠性的另一个重要方面是确保持久性消息传送至目标后,消息服务在向消费者传送它们之前不会丢失这些消息
-
持久的Topic
-
生产者的改造
public class JmsProduce_Topic {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String TOPIC_NAME = "topic-playMaker";
public static void main(String[] args) throws JMSException{
//1.创建连接工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2.通过连接工厂,获得连接connection 并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
//3.创建会话session 第一个参数叫事务 签收
Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Topic topic = session.createTopic(TOPIC_NAME);
//5.创建消息的生产者
MessageProducer producer = session.createProducer(topic);
producer.setDeliveryMode(DeliveryMode.PERSISTENT); //持久化
connection.start();//连接放在这里了,是因为上面设置了持久化的参数
//6.通过使用消息生产者生产3条消息发送到MQ的队列里面
for (int i = 0; i <3 ; i++) {
//7.创建消息(好比学生按照要求写好的面试题消息)
TextMessage textMessage = session.createTextMessage("TOPIC_NAME---" + i);//理解为一个字符串
//8.通过Producer发送给mq
producer.send(textMessage);
}
//9.关闭资源
producer.close();
session.close();
connection.close();
System.out.println("******TOPIC_NAME消息发布到MQ完成");
}
}
- 消费者改造
public class JmsConsumer_Topic {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String TOPIC_NAME = "topic-playMaker";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是3号消费者");
//1.创建连接工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2.通过连接工厂,获得连接connection 并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.setClientID("playMaker"); //表明是谁订阅
connection.start();
//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(message!=null){
TextMessage textMessage = (TextMessage) message;
System.out.println("******收到的持久化topic:"+textMessage.getText());
message = topicSubscriber.receive(1000L);
}
session.close();
connection.close();
}
}
- 启动订阅者,这里面有哪些处于激活状态的订阅者一目了然
- 启动发布者,因为你设置为1秒收不到消息就退出了,所以接完3条消息后,就处于离线状态了
- 一定要先运行一次消费者,等于向MQ注册,类似订阅了这个主题;然后再运行生产者发送消息,此时,无论消费者是否在线,都会接收到,不在线的话,下次连接的时候,会把没有收过的消息都接收下来
B.事务
- producer提交时的事务
- 设置成false,只要执行send,就进入到队列中;关闭事务,那第2个签收参数的设置需要有效
- 设置成true,先执行send再执行commit,消息才被真正的提交到队列中;消息需要批量发送,需要缓冲区处理
- 这里为什么需要用事务?因为项目中有可能没发没消息就报错异常之类的,可以用session.rollback来回滚
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
//1.创建连接工厂
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.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Queue queue = session.createQueue(QUEUE_NAME);//Collection collection = new ArrayList();
//5.创建消息的生产者
MessageProducer producer = session.createProducer(queue);
//6.通过使用消息生产者生产3条消息发送到MQ的队列里面
for (int i = 0; i <=3 ; i++) {
//7.创建消息(好比学生按照要求写好的面试题消息)
TextMessage textMessage = session.createTextMessage("message---" + i);//理解为一个字符串
//8.通过Producer发送给mq
producer.send(textMessage);
}
//9.关闭资源
producer.close();
session.commit();//设置成事务,session关闭要提交下;不开启事务,默认提交
session.close();
connection.close();
System.out.println("******消息发布到MQ完成");
}
}
- 此时生产者是用事务生产的,而消费者并没有用事务消费消息,消费者消费一次就不会接收到了,也就是只会消费一次
- 如果你生产者是用事务生产的,而消费者并事务消费消息,不提交,会一直重复消费,因为不提交MQ不知道你是否完成了对消息的消费
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://106.12.213.16:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是2号消费者");
//1.创建连接工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
//2.通过连接工厂,获得连接connection 并启动访问
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3.创建会话session 第一个参数叫事务 签收
Session session = connection.createSession(false/*true*/,Session.AUTO_ACKNOWLEDGE);
//4.创建目的地(主题和队列) 接口类 队列和主题是它的子类
Queue queue = session.createQueue(QUEUE_NAME);//Collection collection = new ArrayList();
//5.创建消费者
MessageConsumer consumer = session.createConsumer(queue);
//同步阻塞方法receive
//订阅者或接受者调用MessageConsumer的receive方法来接收消息,receive方法在能够接收到消息之前(或超时之前)将一直阻塞
while(true){
TextMessage textMessage = (TextMessage) consumer.receive(4000L);
if(textMessage!=null){
System.out.println("****消费者接受到的消息:"+textMessage.getText());
}
else{
break;
}
}
consumer.close();
//session.commit();
session.close();
connection.close();
}
}
- 事务偏生产者/签收偏消费者
C.签收
a.非事务
- 自动签收(默认)
Session.AUTO_ACKNOWLEDGE
- 手动签收
Session.CLIENT_ACKNOWLEDGE
//客户端调用acknowledge方法手动签收
//生产者用非事务 自动签收方式生产消息,消费者用非事务 手动签收方法接受消息,可以重复接收,就像拿快递一样,你需要给快递员一个反馈"我拿到了快递",所以需要用消息.acknowledge方法表示你已经"拿到了快递"
- 允许重复消息
Session.DUPS_OK_ACKNOWLEDGE //带副本的,重复签收,你签收了可能会显示没签收,会导致状态不对...
b.事务
- 运行产生三条
- 这个接收要一一匹配
- 这样会消费到消息嘛?
- 会消费到,并且不会重复消费,因为开启了事务,就认为你已经自动签收了
- 生产事务开启,只有commit后才能将全部消息变为已消费
- 那么,我们反过来,不commit,看看是否能消费到消息?
- 消费到了,但要重复消费,事务更大些,commit就认为已自动签收了,没有commit就算你写了ack也不认
- 消费到了,但要重复消费,事务更大些,commit就认为已自动签收了,没有commit就算你写了ack也不认
c.签收和事务关系
-
在事务性会话中,当一个事务被成功提交则消息被自动签收,如果事务回滚,则消息会被再次传送
-
非事务性会话中,消息何时被确认取决于创建会话时的应答模式(acknowledgement mode)
-
面试中,如果被问到消息中间件的可靠性,可以以ActiveMQ为例子,从持久性、事务、签收、多节点集群四大方面来回答,消息是否被重复消费
5.JMS的点对点总结
- 点对点模型是基于队列的,生产者发消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能。和我们平时给朋友发送短信类似
- 如果在Session关闭时由部分消息已被收到但还没有被签收(acknowledged),那当消费者下次连接到相同的队列时,这些消息还会被再次接受
- 队列可以长久地保存消息直到消费者收到消息。消费者不需要因为担心消息会丢失而时刻和队列保持激活的连接状态,充分你体现了异步传输模式的优势
6.JMS的发布订阅总结
- JMS Pub/Sub模型定义了如何向一个内容节点发布和订阅消息,这些节点被称作topic
- 主题可以被认为是消息的传输中介,发布者(publisher)发布消息到主题,订阅者(subscribe)从主题订阅消息
- 主题使得消息订阅者和消息发布者保持互相独立,不需要解除即可保证消息的传送
- 非持久订阅
- 只有当客户端处于激活状态,也就是和MQ保持连接状态才能收到发送到某个主题的消息
- 如果消费者处于离线状态,生产者发送的主题消息将会丢失作废,消费者永远不会收到
- 一句话:先要订阅注册才能接受到发布,只给订阅者发布消息
- 持久订阅
- 客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到主题的消息,当客户再次连接到MQ时会根据消费者的ID得到所有当自己处于离线时发送到主题的消息
- 非持久订阅状态下,不能恢复或重新派送一个未签收的消息
- 持久订阅才能恢复或重新派送一个未签收的消息
- 用哪个?
- 当所有消息必须被接收,则用持久订阅
- 容忍丢失,可以用非持久订阅