消息队列 - ActiveMQ
一、入门概述
1.在什么场景下使用消息中间件,为什么使用
首先举个课后找老师提问的例子。
下课了,同学们争先恐后 (并发) 的去找老师问问题,每当这个时候,老师都是苦不堪言(服务器压力过大),因为同学们太多了,为了防止同学因为提问问题打架(并发写内容出错),所以只能让同学们排队等待(加锁)。
现在是小张正在问问题,后面是小王、小美。。。。一直到第10个人小红,假设每个同学问问题的时间都是10分钟,那么小红就需要在100分钟以后才能轮到提问,这个时候她什么都做不了。而难过的是,这时候老师已经回家了。。。。
这时,聪明的小杨同学想到了一个办法,在讲桌旁边,建立一个提问箱(消息队列),排队的同学们可以把自己的问题按照某种规范(姓名,手机号,寄回地址)去书写自己想要提问的问题写在A4纸上(消息),然后去忙自己的事情**(异步**),等老师处理完其他问题之后在处理她的问题,处理之后在回寄给他(异步回调)。
很明显这样的好处就是
- 消峰:
同一时刻没有大量的同学去询问老师,不会给老师增加压力,也不会耽误下节课。 - 解耦
同学并不是直接去找老师提问的,这就代表老师和同学之间的接触变少,疫情当下,可以减少疫情传播(2021年)。 - 异步
同学将提问卡放到提问箱之后,就可以去做别的事情了,比如说打王者农药,反正等老师处理完结果也会寄回(异步回调)的。
从生活Case到实际生产案例
系统之间直接调用实际工程落地和存在的问题
微服务架构,链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(或子模块),比如模块A调用模块B,模块B调用模块C,模块C调用模块D。
但在大型分布式应用中,系统间的RPC交互繁杂,一个功能背后要调用上百个接口并非不可能,从单机架构过渡到分布式微服务架构后这种架构会有哪些问题 ???
1.系统之间接口耦合比较严重.
每新增一个下游功能,都要对上游的相关接口进行改造;
举个例子:假如系统A要发送数据给系统B和C,发送给每个系统的数据可能有差异(如B系统用xml,C系统用JSON),因此系统A就要对发送给每个系统的数据进行了组装,然后逐一发送;
当代码上线后又新增了一个需求:
把数据也发送给D,新上了一个D系统也要接受A系统的数据。此时就需要修改A系统,让他感知到D的存在,同时把数据处理好再A给D。在这个过程中你会看到,每接入一个下游系统,都要对A系统进行代码改造,开发联调的效率很低。
其整体架构如下图
2.面对大流量并发时,容易被冲垮
每个接口模块的吞吐能力是有限的,这个上限能力如堤坝,当大流量(洪水)来临时,容易被冲垮。
举个栗子秒杀业务:
上游系统发起下单购买操作,就是下单一个操作所需要调用的接口就很多。
下游系统完成秒杀业务逻辑
(读取订单,库存检查,库存冻结,余额检查,余额冻结,订单生成,余额扣减,库存扣减,生成流水,余额解冻,库存解冻)
3.等待同步存在性能问题
RPC接口基本上是同步调用,整体的服务性能遵循“木桶理论”,即整体系统的耗时取决于链路中最慢的那个接口。
比如A调用B/C/D都是50ms,但此时B又调用了B1,花费2000ms,那么直接就拖累了整个服务性能。
你如何解决? ? ?
需要有一种东东能够摆平上述情况
根据上述的几个问题,在设计系统时可以明确要达到的目标:
-
要做到系统解耦,当新的模块接进来时,可以做到代码改动最小;能够解耦
-
设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮;能够削峰
-
强弱依赖梳理能将非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步
是不是和上面举得上课提问的例子对上了?
在例子中,我们总提到一个介质 - 提问箱,这个提问箱就是消息队列,我们下面学习 activeMQ的知识。
2. 消息队列是什么
面向消息的中间件(message-oriented middleware)MOM能够很好的解决以上问题。
MOM 是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。
通过提供消息传递和消息排队模型在分布式环境下提供应用解耦、弹性伸缩、冗余存储、流量削峰、异步通信、数据同步等功能。
一大堆概念之后,大致的过程是这样的:
发送者把消息发送给消息服务器,消息服务器将消息存放在若干队列/主题topic中,在合适的时候,消息服务器会将消息转发给接受者。
在这个过程中,发送和接受是异步的,也就是发送无需等待,而且发送者和接受者的生命周期也没有必然关系;
尤其在发布pub/订阅sub模式下,也可以完成一对多的通信,即让一个消息有多个接受者。
上面提到发送和接收都是异步的
消息发送者可以发送一个消息而无须等待响应。消息发送者将消息发送到一条虚拟的通道(主题或队列)上;
消息接收者则订阅或监听该通道。一条信息可能最终转发给一个或多个消息接收者,这些接收者都无需对消息发送者做出同步回应。整个过程都是异步的。
案例:
也就是说,一个系统跟另外一个系统之间进行通信的时候,假如系统A希望发送一个消息给系统B,让他去处理。
但是系统A不关注系统B到底怎么处理或者有没有处理好,所以系统A把消息发送给MQ,然后就不管这条消息的“死活”了,接着系统B从MQ里消费出来处理即可。至于怎么处理,是否处理完毕,什么时候处理,都是系统B的事儿,与系统A无关。
这样的一种通信方式,就是所谓的“异步”,这种通信方式对于系统A来说,只要把消息发给MQ,然后系统B就会异步的去进行处理了,系统A不需要“同步”的等待系统B处理完。这样的好处是什么呢?两个字:解耦
总结下
应用系统之间解耦合
- 发送者和接受者不必了解对方,只需要确认消息;
- 发送者和接受者不必同时在线。
举一个电商下单的例子
用户下单后,不会立刻看到自己的物流,快递员等,需要等一段时间才会收到,也不会立刻收到积分,返现等,一般需要等订单结算后才会收到消息。
下图用户下单后,发送一条用户下单信息到MQ中,无需等待结果,继续支付网关,创建订单。而信息到MQ的同时,MQ会通知仓储系统进行发货,配送,这些步骤订单系统是不关心的。等仓库信息变化的时候,在通知订单系统,如,商品已签收,订单系统会签收订单。
消息队列最主要的功能
- 实现高可用、 高性能、可伸缩、 易用和安全的企业级面向消息服务的系统
- 异步消息的消费和处理
- 控制消息的消费顺序
- 可以和spring/springboot整合简化编码
- 配置集群容错的MQ集群
3. 去哪下
ActiveMQ官网: http://activemq.apache.org/
二、ActiveMQ的安装和控制台
1.Linux安装
1.解压安装
2.普通启动
启动信息打在控制台
lsof = list of file
3.普通关闭
4.带运行日志的启动方式
启动信息打印在自定义文件内
2.Apache ActiveMQ 控制台
在浏览器输入服务器ip地址:8161
默认用户名和密码是admin,admin
采用61616端口是提供JMS服务的
采用8161端口是提供管理控制台的
三、JAVA编码实现ActiveMQ通讯(未完成)
一、理论基础
ActiveMQ的架构很简单,创建连接, 创建session,然后通过session创建消息、生产者、消费者,再由生产者将消息发送出去,消费者接收。
消息队列消息的存储位置也有两个,一个是Queue,一个是topic。
1. 在点对点的消息传递域中,目的地被称为队列(queue)
简单的说就是一对一,消息发送给Queue,queue在通知对方。
点对点消息传递域的特点如下:
- 每个消息只能有一个消费者,类似1对1的关系。好比个人快递自己领取自己的。
- 消息的生产者和消费者之间没有时间上的相关性。无论消费者在生产者发送消息的时候是否处于运行状态,消费者都可以提取消息。好比我们的发送短信,发送者发送后不见得接收者会即收即看。
- 消息被消费后队列中不会再存储,所以消费者不会消费到已经被消费掉的消息。
2. 在发布订阅消息传递域中,目的地被称为主题(topic)
简单的说就是一对多,消息发送给topic,topic在通知订阅的服务。
发布/订阅消息传递域的特点如下:
- 生产者将消息发布到topic中,每个消息可以有多个消费者,属于1:N的关系
- 生产者和消费者之间有时间上的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息。
- 生产者生产时,topic不保存消息它是无状态的不落地,假如无人订阅就去生产,那就是一条废消息,所以,一般先启动消费者再启动生产者。
JMS 规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。一句话,好比我们的微信公众号订阅
3.两大模式的比较
1.Queue上手案例
前期准备
创建一个Maven项目
pom文件中添加如下依赖
<dependencies>
<!-- activemq所需要的jar包配置 -->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
1.1 模拟Queue发送消息
public class JMSProduce {
public static final String MQ_BROKER_URL = "tcp://10.10.10.120:61616";
public static final String QUEUE_NAME = "queueyu";
public static void main(String[] args) throws JMSException {
//1先通过ActiveMQConnectionFactory 获得mq工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(MQ_BROKER_URL);
//2 获得连接connection
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3通过connection 获得Session
//3.1第一个参数叫事务,默认用false
//3.2第二个参数叫签收,默认自动签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4通过session创建目的地
Queue queue = session.createQueue(QUEUE_NAME);
//5通过session 创建消息生产者
MessageProducer producer = session.createProducer(queue);
for(int i = 0; i<3; i++) {
//6编写发送的消息(提问卡msg)
TextMessage textMessage = session.createTextMessage("提问"+i);
//7 messageProducer开始发送消息到MQ
producer.send(textMessage);
}
//8释放资源
connection.close();
session.close();
producer.close();
System.out.println("发送完毕");
}
}
当运行程序,发送完毕之后,控制台应该显示出上面显示待消费的消息有6个,已进入队列6个。
有些看不懂?没关系注释已准备好
中文版
1.2 模拟消费消息(同步阻塞方式版本)
public class JMSConsumer {
public static final String MQ_BROKER_URL = "tcp://10.10.10.120:61616";
public static final String QUEUE_NAME = "queueyu";
public static void main(String[] args) throws JMSException {
//1先通过ActiveMQConnectionFactory 获得mq工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(MQ_BROKER_URL);
//2 获得连接connection
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3通过connection 获得Session
//3.1第一个参数叫事务,默认用false
//3.2第二个参数叫签收,默认自动签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4通过session创建目的地
Queue queue = session.createQueue(QUEUE_NAME);
//5通过session 创建消息消费者
MessageConsumer consumer = session.createConsumer(queue);
/*
同步阻塞方式receive() ,订阅者或接收者调用MessageConsumer的receive()方法来接收消息,
receive()将一直阻塞
receive(long timeout) 按照给定的时间阻塞,到时间自动退出
*/
while(true)
{
//发什么格式的消息,接收什么格式的消息
// TextMessage textMessage = (TextMessage)consumer.receive(); //不见不散
TextMessage textMessage = (TextMessage)consumer.receive(8 * 1000); //过时不侯
if(textMessage == null) {
break;
}
System.out.println(textMessage.getText());
}
//8释放资源
connection.close();
session.close();
consumer.close();
System.out.println("接收完毕");
}
}
如果使用不见不散的方式时,消费消息之后程序不会结束,还是会继续等待,如果生产者在次发送消息的话,消费者会直接消费。
如果使用过时不候的方式时,消费消息之后程序会等待到约定的时间结束运行,在约定的时间内生产者在次发送消息的话,消费者会直接消费。
以下截图是过时不候的方式,约定时间为8秒。
8秒之后,可以看到连接数为0;
1.3 模拟消费消息( 异步非阻塞方式)
public class JMSConsumerAsynchronous {
public static final String MQ_BROKER_URL = "tcp://10.10.10.120:61616";
public static final String QUEUE_NAME = "queueyu";
public static void main(String[] args) throws JMSException, InterruptedException {
//1先通过ActiveMQConnectionFactory 获得mq工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(MQ_BROKER_URL);
//2 获得连接connection
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3通过connection 获得Session
//3.1第一个参数叫事务,默认用false
//3.2第二个参数叫签收,默认自动签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4通过session创建目的地
Queue queue = session.createQueue(QUEUE_NAME);
//5通过session 创建消息消费者
MessageConsumer consumer = session.createConsumer(queue);
CountDownLatch countDownLatch = new CountDownLatch(3);
// consumer.setMessageListener(new MessageListener() {
//
// @Override
// public void onMessage(Message message) {
// // TODO Auto-generated method stub
//
// }
// });
/*
异步非阻塞方式(监听器onMessage())
订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,
当消息到达之后,系统自动调用监听器MessageListener的onMessage(Message message)方法。
*/
consumer.setMessageListener(message -> {
if(message != null && message instanceof TextMessage) {
TextMessage textMessage = (TextMessage)message;
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
//不释放了,因为监听器需要监听,所以要阻塞主线程
System.out.println("接收完毕");
}
}
2.Topic上手案例
生产者
public class JMSProduceTopic {
public static final String MQ_BROKER_URL = "tcp://10.10.10.120:61616";
public static final String TOPIC_NAME = "topicyu";
public static void main(String[] args) throws JMSException {
//1先通过ActiveMQConnectionFactory 获得mq工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(MQ_BROKER_URL);
//2 获得连接connection
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3通过connection 获得Session
//3.1第一个参数叫事务,默认用false
//3.2第二个参数叫签收,默认自动签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4通过session创建目的地,改动处
Topic topic = session.createTopic(TOPIC_NAME);
//5通过session 创建消息生产者
MessageProducer producer = session.createProducer(topic);
for(int i = 0; i<3; i++) {
//6编写发送的消息(提问卡msg)
TextMessage textMessage = session.createTextMessage("topic 提问"+i);
//7 messageProducer开始发送消息到MQ
producer.send(textMessage);
}
//8释放资源
connection.close();
session.close();
producer.close();
System.out.println("topic发送完毕");
}
}
package com.yangyu.activemq.consumer;
public class JMSConsumerAsynchronousTopic {
public static final String MQ_BROKER_URL = "tcp://10.10.10.120:61616";
public static final String TOPIC_NAME = "topicyu";
public static void main(String[] args) throws JMSException, InterruptedException {
//1先通过ActiveMQConnectionFactory 获得mq工厂
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(MQ_BROKER_URL);
//2 获得连接connection
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
//3通过connection 获得Session
//3.1第一个参数叫事务,默认用false
//3.2第二个参数叫签收,默认自动签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//4通过session创建目的地
Topic topic = session.createTopic(TOPIC_NAME);
//5通过session 创建消息消费者
MessageConsumer consumer = session.createConsumer(topic);
/*
异步非阻塞方式(监听器onMessage())
订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,
当消息到达之后,系统自动调用监听器MessageListener的onMessage(Message message)方法。
*/
consumer.setMessageListener(message -> {
if(message != null && message instanceof TextMessage) {
TextMessage textMessage = (TextMessage)message;
try {
System.out.println(textMessage.getText());
} catch (JMSException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
//不释放了,因为监听器需要监听,所以要阻塞主线程
System.out.println("接收完毕");
}
}
前面提到过,topic只给订阅过的消费者发送消息,就像微信公众号一样,如果已经订阅,则每次发送消息都能收到推文,如果在发送推文之后在订阅,则历史推文不会收到。
不同于quque方式,queue负载均衡是将消息平均分配给每一个消费者,而topic是将消息全部发送给每一个订阅者。
3. 总结
1.JMS开发基本步骤
2.两种消费方式
同步阻塞方式(receive())
订阅者或接收者调用MessageConsumer的receive()方法来接收消息,receive方法在能够接收到消息之前(或超时之前)将一直阻塞。
异步非阻塞方式(监听器onMessage())
订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,
当消息到达之后,系统自动调用监听器MessageListener的onMessage(Message message)方法。
3.两大模式比较
4.梳理
当有一个消息进入这个队列时,等待消费的消息是1,进入队列的消息是1。
当消息消费后,等待消费的消息是0,进入队列的消息是1,出队列的消息是1.
再来一条消息时,等待消费的消息是1,进入队列的消息就是2.
四、JMS规范和落地产品
不知道前面有没有注意到,我多次提到了JMS,甚至是类名,都有JMS,那JMS是什么。
1. 什么是JMS
学习JMS之前,我们先复习以下javaEE
JavaEE是一套使用Java进行企业级应用开发的大家一致遵循的13个核心规范工业标准。 JavaEE平台提供了一个基于组件的方法来加快设计、开发、装配及部署企业应用程序
- JDBC(Java Database)数据库连接
- JNDI(Java Naming and Directory Interfaces)Java 的命名和目录接口
- EJB(Enterprise JavaBean)
- RMI(Remote Method Invoke)远程方法调用
- Java IDL(Interface Description Language)/CORBA(Common Object Broker Architecture) 接口定义语言/公用对象请求代理程序体系结构
- JSP(Java Server Pages)
- Servlet
- XML(Extensible Markup Language)可扩展白标记语言
9. JMS(Java Message Service)Java 消息服务 - JTA(Java Transaction API)Java 事务 API
- JTS(Java Transaction Service)Java 事务服务
- JavaMail
- JAF(JavaBean Activation Framework
在javaEE的工业标准第九条就有JMS(Java Message Service)Java 消息服务。
那什么是什么是Java消息服务呢?
Java消息服务指的是两个应用程序之间进行异步通信的API,它为标准消息协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持JAVA应用程序开发。在JavaEE中,当两个应用程序使用JMS进行通信时,它们之间并不是直接相连的,而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。
而ActiveMQ就是JMS的一种产品,那除了ActiveMQ之外,还有没有别的产品呢?
哇有这么多,那他们各自有什么特性呢?
2. JMS的组成结构及其特点
1.1 JMS Server: 实现JMS接口和规范的消息中间件,也就是我们的MQ服务器
1.2 JMS producer: 消息生产者,创建和发送JMS消息的客户端应用
1.3 JMSconsumer:消息消费者,按收和处理JMS消息的客户端应用
1.4 JMS message
消息头
1.4.1 JMSDestination:消息发送的目的地,主要是指Queue和Topic
1.4.2 JMSDeliveryMode
持久模式和非持久模式。
一条持久性的消息:应该被传送“一次仅仅一次”,这就意味者如果JMS提供者出现故障,该消息并不会丢失,它会在服务器恢复之后再次传递。
一条非持久的消息:最多会传送一次,这意味这服务器出现故障,该消息将永远丢失。
1.4.3 JMSExpiration
可以设置消息在一定时间后过期,默认是永不过期
消息过期时间,等于Destination的send 方法中的timeToLive值加上发送时刻的GMT 时间值。
如果timeToLive 值等于零,则JMSExpiration 被设为零,表示该消息永不过期。
如果发送后,在消息过期时间之后消息还没有被发送到目的地,则该消息被清除。
1.4.4 JMSPriority
息优先级,从 0-9 十个级别,0到4是普通消息,5到9是加急消息。
JMS不要求MQ严格按照这十个优先级发送消息,但必须保证加急消息要先于普通消息到达。默认是4级。
1.4.5 JMSMessagelD:唯一识别每一个消息的标识,由
消息体
封装具体的消息数据,发送和接收的消息类型必须一致
消息体有5种消息体格式
1. TextMessage:日一普通字符串消息,包含一个string(重点)
2. MapMessage:一个Map类型的消息。key为String类型,而值为java的基本类型(重点)
3. BytesMessage:二进制数组消息, 包含一个byte[]
4. streamMessage:Java数据流消息, 用标准流操作来顺序的填充和读取。
5. ObectMessage: 对象消息, 包含一个可序列化的java对象。
消息属性
他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展,属性指定一些消息头没有包括的附加信息,比如可以在属性里指定消息选择器。
消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。
它们还用于暴露消息选择器在消息过滤时使用的数据。
用白话来说,消息头就是邮件的标题,消息体就是邮件的正文,消息属性就是邮件的附件,消息属性可以通过message.setStringProperty()
在消息种添加附件,消息属性虽然也可以携带数据,但是并不会影响控制台的消息数量。
如:
TextMessage message = session.createTextMessage();
message.setText(text);
message.setStringProperty("username","z3"); //自定义属性
3. PERSISTENT(持久) 持久性
我们知道redis有持久化的功能,这无疑会大大增加系统稳定性,防止可怕的事情发生。
从图中我们可以看到,DeliveryMode有两种模式,一种是持久化,一种是非持久化。
那么默认是什么呢?
我们用事实证明一下
-
我们把持久化操作注释掉
-
现在MQ里面存储了4条消息
-
我们把MQ干掉
-
重启MQ
-
我们刷新控制台,发现入队消息是0,难道默认是非持久化吗?
-
为了验证,使用消费者消费一次,如果消费成功就代表持久化开启了
果然,收到了消息
- 控制台状态
发现出队为4,消费者为1,经过测试,发现默认是持久化。
重启之后,入队清零和持久化没有关系。
持久化测试了,我们再来测试一下非持久化吧!
-
我们把开启非持久化
-
现在MQ里面存储了4条消息
-
我们把MQ干掉
-
重启MQ
-
我们刷新控制台,发现空空如也,
总结
Active消息分为持久化和非持久化
非持久
messageProducer.setDeliveryMode (DeliveryMode.NON_ PERSISTENT);
- 非持久化: 当服务器宕机,消息不存在。
持久
messageProducer.setDeliveryMode (DeliveryMode.PERSISTENT);
- 持久化:当服务器宕机,消息依然存在。
默认情况,它是持久还是非持久?
- 默认是持久
至于为什么选用持久化作为默认模式,我想消息只能被传送一次和成功使用一次。对于这些消息,可靠性是优先考虑的因素。
可靠性的另一个重要方面是确保持久性消息传送至目标后,消息服务在向消费者传送它们之前不会丢失这些消息。
4. TRANSACTION 事务
开局一张图
这个图里面的两个参数相比小伙伴们在前面应该看到了吧,没有解释的原因并不是我忘记了,也不是它不重要,而是想把它拿到后面说,先来第一个参数transacted,,,事务。
事务,事务,又是事务,事务大家见的太多了,我mysql进阶那一章详细介绍过,简单的说就是一套流程,要么一起成功,要么一起失败。
我们直接举例子吧。
-
开启事务,第一个参数传true
-
在开启事务之后发布4条消息
-
查看控制台
怎么回事,没有发布成功吗?在发布一次,还是没有成功,仔细想想,对了,在mysql中事务是需要提交的。赶快试试 -
提交事务
-
发布成功
-
为什么要提交呢,不由得让我们想起回滚
嗯。。。这是生产者开启了事务,那消费者开启事务会怎么样呢?
在学mysql和JUC的时候都学过读写分离,而mysql更是强调了写才要开启事务。
那么在activeMQ中,生产者向MQ中写入数据,消费者可以从MQ中读取数据,我们在读取数据中开启事务会什么样呢,脑洞大开,但是这也是常常容易忽略的一个问题,毕竟生产者消费者都要设置事务的参数,如果一不小心在消费者中开启了事务,会发生什么事呢?
继续做实验
-
生产者发布4个消息
-
消费者开启事务之后,在运行读取消息(截图少截了一个消息)
挺正常的? -
查看控制台,发现已经有消费者连接但是消息还没有出队
怎么回事? -
消费者在消费一次试试?
同样的结果,是不是落掉了什么?对!commit! -
手动commmit
果然恢复了正常
思考一下?消费者在session关闭之前手动提交事务有什么缺点?
我们学JUC的时候学过,主线程的任务,可能在其他线程任务执行完毕之前执行,也就是说,还没消费呢,就提交了,然后导致一条消息都读不到。
结论
producer提交时的事务
1.关闭事务
- 只要执行send, 就进入到队列中。
- 关闭事务,那第2个签收参数的设置需要
2.开启事务
- 先执行send再执行commit,消息才被真正的提交到队列中。
- 消息需要批量发送,需要缓冲区处理
Consumer消费时事务
1.关闭事务
- 只要执行程序, 就会读取消息,并且无法重读
- 控制台显示消息已出队
2.开启事务
- 只要执行程序, 就会读取消息,但是可以重读
- 控制台显示消息未出队
5. Acknowledge 签收
就像收快递一样,生产者发送了消息,那么消费者应该告诉他,我签收了,也就是ACK,默认是自动签收。
到这里聪明的小伙伴可能都猜到了,一定是自动签收与手动签收的关系,更聪明的小伙伴甚至都猜到了我下面要作什么实验了吧。
开始实验
- 开启手动签收
- 生产者发送消息
- 已经开启手动签收的消费者开始消费,正常读取到了数据
- 再次开始消费,发现又读到了数据,重读?果然应该告诉MQ我签收了。手动ACK
- 全部初始化,再次生产,再次消费,发现已经不能重读了,恢复正常。
结论:如果手动签收必须要在代码中手动ACK。
认真看了事务那一节的小伙伴都知道,如果把事务开启,会发生重读的现象,解决方案是手动提交。那么如果我们把事务和手动签收都开启呢?一山难容二虎啊!
实验方案:我们开启手动签收与事务,事务手动提交,但是手动签收开启之后并不在代码中手动ACK。有点乱?缕一缕
- 不在代码中手动ACK:会重读
- 在代码中手动提交事务:不会重读
看看他们谁会赢
实验步骤
-
开启事务,开启手动签收
-
把手动提交注释
会不会出现重复消费? -
执行消费者,发现没有出现重复消费
山中无老虎,猴子称大王,事务优先级要高于ACK
总结:
事务偏生产者 / 签收偏消费者
1.非事务状态下
- 默认模式为自动签收
Session.AUTO_ACKNOWL EDGE
- 如果改为手动签收模式
Ses sion. CLIENT_ ACKNOWL EDGE
,需要客戶端调用acknowledge方法手动签收message.acknowledge();
2.事务状态下
- 消费者事务开启, 只有commit后才能将全部消息变为已消费
- 补充解释:因为事务优先级高,所以自动签收也没用,只有提交事务才能消费成功,否则就会重复消费。
3.签收和事务关系
- 在事务性会话中,当一个事务被成功提交则消息被自动签收,如果事务回滚,则消息会被再次传送。
- 非事务性会话中,消息何时被确认取决于创建会话时的应答模式(acknowledgementmode),山中无老虎,猴子。。。
- 第二条补充解释:就是没开启事务,才会看应答模式是自动还是手动,要是自动就会确认,要是手动需要手动ACK。
学无止境,再会