文章目录
01、MQ概述
MQ的产品种类和对比
目前市面上比较主流的消息队列中间件主要有:Kafka
、ActiveMQ
、RabbitMQ
、RocketMQ
等。
ActiveMQ
和RabbitMQ
这两由于吞吐量的原因,只有业务体量一般的公司在用,RabbitMQ
由于是erlang语言开发的,我们都不了解,因此扩展和维护成本都很高,查个问题都头疼。
Kafka
和RocketMQ
一直在各自擅长的领域发光发亮,两者的吞吐量、可靠性、时效性等都很可观。
kafka:
- 编程语言:scala
- 大数据领域的主流MQ。
RabbitMQ:
- 编程语言:erlang
- 基于erlang语言,不好修改底层,不要查找问题的原因,不建议选用。
RocketMQ
- 编程语言:java
- 适用于大型项目。适用于集群。
ActiveMQ
- 编程语言:java
- 适用于中小型项目。
我们通过图表看看这几个消息中间机的对比:
特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
---|---|---|---|---|
单机吞吐量 | 万级,比RocketMQ,Kafka低一个数量级 | 同ActiveMQ | 10万级,支持高吞吐 | 10万级,高吞吐,一般配合大数据类型的系统来进行实时数据计算,日志采集等场景 |
topic数量对吞吐量的影响 | topic可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是RocketMQ的一大优势·,在同等机器下可以支持大量的topic | topic从几十到几百的时候,吞吐量会大幅度下降,在同等机器下,Kafka尽量保证topic数量不要过多,如果要支持大规模的topic,需要增加更多的机器资源 | ||
时效性 | ms级别 | 微妙级别,这是RabbitMQ的一大特性,延时最低 | ms级别 | 延时在ms级别以内 |
可用性 | 高,基于主从架构实现高可用 | 同ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数的优化配置,可以做到0丢失 | 同RocketMQ |
功能支持 | MQ领域的功能极其完备 | 基于erlang开发,并发能力很强,性能很好,延时很低 | MQ功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用 |
社区活跃度 | 低 | 中 | 高 | 高 |
MQ的产生背景
系统之间直接调用存在的问题?
微服务架构后,链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(或子模块),比如模块A调用模块B,模块B调用模块C,模块C调用模块D。
但在大型分布式应用中,系统间的RPC
交互繁杂,一个功能背后要调用上百个接口并非不可能,从单机架构过渡到分布式微服务架构的通例。这些架构会有哪些问题?
-
(1) 系统之间接口耦合比较严重
每新增一个下游功能,都要对上游的相关接口进行改造;
-
(2) 面对大流量并发时,容易被冲垮。
每个接口模块的吞吐能力是有限的,这个上限能力如果是堤坝,当大流量(洪水)来临时,容易被冲垮。
举个例子秒杀业务:上游系统发起下单购买操作,就是下单一个操作,很快就完成。然而,下游系统要完成秒杀业务后面的所有逻辑(读取订单,库存检查,库存冻结,余额检查,余额冻结,订单生产,余额扣减,库存减少,生成流水,余额解冻,库存解冻)。 -
(3) 等待同步存在性能问题
RPC接口上基本都是同步调用,整体的服务性能遵循
木桶理论
,即整体系统的耗时取决于链路中最慢的那个接口。
根据上述的几个问题,在设计系统时可以明确要达到的目标:
- 1,要做到系统解耦,当新的模块接进来时,可以做到代码改动最小,
能够解耦
- 2,设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮,
能削峰
- 3,强弱依赖梳理能将非关键调用链路的操作异步化并提升整体系统的吞吐能力,
能够异步
MQ的主要作用
- (1) 异步。调用者无需等待。
- (2) 解耦。解决了系统之间耦合调用的问题。
- (3) 消峰。抵御洪峰流量,保护了主业务。
MQ的定义
面向消息的中间件(message-oriented middleware)MOM能够很好的解决以上问题。是指利用高效可靠的消息传递机制与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型在分布式环境下提供应用解耦,弹性伸缩,冗余存储、流量削峰,异步通信,数据同步等功能。
大致的过程是这样的:发送者把消息发送给消息服务器,消息服务器将消息存放在若干队列/主题topic中,在合适的时候,消息服务器回将消息转发给接受者。在这个过程中,发送和接收是异步的,也就是发送无需等待,而且发送者和接受者的生命周期也没有必然的关系;尤其在发布pub/订阅sub模式下,也可以完成一对多的通信,即让一个消息有多个接受者。
MQ的特点
(1) 采用异步处理模式
-
消息发送者可以发送一个消息而无须等待响应。消息发送者将消息发送到一条虚拟的通道(主题或者队列)上;
-
消息接收者则订阅或者监听该爱通道。一条消息可能最终转发给一个或者多个消息接收者,这些消息接收者都无需对消息发送者做出同步回应。整个过程都是异步的。
-
案例:
也就是说,一个系统跟另一个系统之间进行通信的时候,假如系统A希望发送一个消息给系统B,让他去处理。但是系统A不关注系统B到底怎么处理或者有没有处理好,所以系统A把消息发送给MQ,然后就不管这条消息的死活了
,接着系统B从MQ里面消费出来处理即可。至于怎么处理,是否处理完毕,什么时候处理,都是系统B的事儿,与系统A无关。
(2) 应用系统之间解耦合
- 发送者和接受者不必了解对方,只需要确认消息。
- 发送者和接受者不必同时在线。
(3) 整体架构
(4) MQ的缺点
- 两个系统之间不能同步调用,不能实时回复,不能响应某个调用的回复。
02、RPC架构
什么是RPC架构?
RPC (Remote Procedure Call):即远程过程调用
,是分布式系统常见的一种通信方法,它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。
除 RPC 之外,常见的多系统数据交互方案还有分布式消息队列、HTTP 请求调用、数据库和分布式缓存等。
其中 RPC 和 HTTP 调用是没有经过中间件的,它们是端到端系统的直接数据交互。
原理图如上,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
比如说,A服务器想调用B服务器上的一个方法:
Employee getEmployeeByName(String fullName)
RPC整个调用过程,主要经历如下几个步骤:
-
1、建立通信
首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。
主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输,连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
-
2、服务寻址
要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么。
通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。
可靠的寻址方式(主要是提供服务的发现)是RPC的实现基石,比如可以采用redis或者zookeeper来注册服务等等。
从服务提供者的角度看:当提供者服务启动时,需要自动向注册中心注册服务;
当提供者服务停止时,需要向注册中心注销服务;
提供者需要定时向注册中心发送心跳,一段时间未收到来自提供者的心跳后,认为提供者已经停止服务,从注册中心上摘取掉对应的服务。从调用者的角度看:调用者启动时订阅注册中心的消息并从注册中心获取提供者的地址;
当有提供者上线或者下线时,注册中心会告知到调用者;
调用者下线时,取消订阅。 -
3、网络传输
序列化
当A机器上的应用发起一个RPC调用时,调用方法和其入参等信息需要通过底层的网络协议如TCP传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化(Serialize)或者编组(marshal)成二进制的形式才能在网络中进行传输。然后通过寻址操作和网络传输将序列化或者编组之后的二进制数据发送给B机器。反序列化
当B机器接收到A机器的应用发来的请求之后,又需要对接收到的参数等信息进行反序列化操作(序列化的逆操作),即将二进制信息恢复为内存中的表达方式,然后再找到对应的方法(寻址的一部分)进行本地调用(一般是通过生成代理Proxy去调用,通常会有JDK动态代理、CGLIB动态代理、Javassist生成字节码技术等),之后得到调用的返回值。
-
4、服务调用
B机器进行本地调用(通过代理Proxy)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复为内存中的表达方式,最后再交给A机器上的应用进行相关处理(一般是业务逻辑处理操作)。
常见的RPC架构
-
Thrift
:thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。 -
Dubbo
:Dubbo是一个分布式服务框架,以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。 Dubbo是阿里巴巴内部的SOA服务化治理方案的核心框架,Dubbo自2011年开源后,已被许多非阿里系公司使用。 -
Spring Cloud
:Spring Cloud由众多子项目组成,如Spring Cloud Config、Spring Cloud Netflix、Spring Cloud Consul 等,提供了搭建分布式系统及微服务常用的工具,如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性token、全局锁、选主、分布式会话和集群状态等,满足了构建微服务所需的所有解决方案。Spring Cloud基于Spring Boot, 使得开发部署极其简单。
03、ActiveMQ安装
参考这篇博客:https://blog.csdn.net/weixin_45583303/article/details/119617825
04、入门案例
queue入门案例
pom.xml导入依赖
<dependencies>
<!-- activemq 所需要的jar包-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
<!-- activemq 和 spring 整合的基础包 -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-spring</artifactId>
<version>3.16</version>
</dependency>
</dependencies>
JMS开发的基本步骤
MQ推送目的地
在点对点的消息传递域中(point-to-point
),目的地被称为队列(queue
)
在发布订阅消息传递域中(publish-and-subscribe
),目的地被称为主题(topic
)
队列消息生产者的入门案例
public class JmsProduce {
// linux 上部署的activemq 的 IP 地址 + activemq 的端口号
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
// 目的地的名称
public static final String QUEUE_NAME = "first_queue";
public static void main(String[] args) throws Exception{
// 1 按照给定的url创建连接工厂,这个构造器采用默认的用户名密码。该类的其他构造方法可以指定用户名和密码。
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 创建目的地(两种 :队列/主题)。Destination是Queue和Topic的父类
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的生产者
MessageProducer messageProducer = session.createProducer(queue);
// 6 通过messageProducer 生产 3 条 消息发送到消息队列中
for (int i = 1; i < 4 ; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage("msg--" + i);
// 8 通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9 关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** 消息发送到MQ完成 ****");
}
}
ActiveMQ控制台之队列
运行上面代码,控制台显示如下:
Number Of Pending Messages:
- 等待消费的消息,这个是未出队列的数量,
公式=总接收数-总出队列数
。
Number Of Consumers:
- 消费者数量,消费者端的消费者数量。
Messages Enqueued:
- 进队消息数,进队列的总消息量,包括出队列的,这个数只增不减。
Messages Dequeued:
- 出队消息数,可以理解为是消费者消费掉的数量。
总结:
- 当有一个消息进入这个队列时,等待消费的消息是1,进入队列的消息是1。
- 当消息消费后,等待消费的消息是0,进入队列的消息是1,出队列的消息是1。
- 当再来一条消息时,等待消费的消息是1,进入队列的消息就是2。
队列消息消费者的入门案例
// 消息的消费者
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String QUEUE_NAME = "first_queue";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
// 5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(queue);
while(true){
// reveive() 一直等待接收消息,在能够接收到消息之前将一直阻塞,是同步阻塞方式 和socket的accept方法类似的。
// reveive(Long time) 等待n毫秒之后还没有收到消息,就是结束阻塞。
// 因为消息发送者是 TextMessage,所以消息接受者也要是TextMessage
TextMessage message = (TextMessage)messageConsumer.receive();
if (null != message){
System.out.println("****消费者的消息:"+message.getText());
}else {
break;
}
}
messageConsumer.close();
session.close();
connection.close();
}
}
控制台显示:
异步监听式消费者(MessageListener)
// 消息的消费者 也就是回答消息的系统
public class JmsConsumer02 {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String QUEUE_NAME = "first_queue";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer messageConsumer = session.createConsumer(queue);
/*
* 通过监听的方式来消费消息,是异步非阻塞的方式消费消息。
* 通过messageConsumer的setMessageListener注册一个监听器,
* 当有消息发送来时,系统自动调用MessageListener的onMessage方法处理消息
*/
messageConsumer.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
// instanceof 判断是否A对象是否是B类的子类
if (null != message && message instanceof TextMessage){
TextMessage textMessage = (TextMessage)message;
try {
System.out.println("****消费者的消息:"+textMessage.getText());
}catch (JMSException e) {
e.printStackTrace();
}
}
}
});
// 让主线程不要结束,因为一旦主线程结束了,其他的线程(如此处的监听消息的线程)也都会被迫结束。
// 实际开发中,我们的程序会一直运行,这句代码都会省略。
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
队列消息(Queue)总结
(1) 两种消费方式
同步阻塞方式(receive)
- 订阅者或接收者抵用MessageConsumer的receive()方法来接收消息,receive方法在能接收到消息之前(或超时之前)将一直阻塞。
异步非阻塞方式(监听器onMessage())
- 订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册
- 一个消息监听器,当消息到达之后,系统会自动调用监听器MessageListener的onMessage(Message message)方法。
(2)队列的特点(点对点消息传递域的特点如下):
- 1、每个消息只能有一个消费者来消费,类似于one to one的关系,好比个人快递自己领取自己的
- 2、消息的生产者和消息的消费者之间没有时间上的相关性,无论消费者在消息发送者发送消息的时候,是否处于运行状态。
消费者都可以提取消息,好比我们发短信,发送者发送后,接受者不一定会及时查看消息。
- 3、被消费掉的消息,在队列中不会在存储,所以消费者不会消费已经被消费掉的消息。
(3)消息消费情况
情况1:只启动消费者1。
结果:消费者1会消费所有的数据。
情况2:先启动消费者1,再启动消费者2。
结果:消费者1消费所有的数据。消费者2不会消费到消息。
情况3:生产者发布6条消息,在此之前已经启动了消费者1和消费者2。
结果:消费者1和消费者2平摊了消息。各自消费3条消息。
疑问:怎么去将消费者1和消费者2不平均分摊呢?而是按照各自的消费能力去消费。
topic入门案例
topic概述
在发布订阅消息传递域中,目的地被称为主题(topic)
发布/订阅消息传递域的特点如下:
(1)生产者将消息发布到topic中,每个消息可以有多个消费者,属于1:N的关系;
(2)生产者和消费者之间有时间上的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息。
(3)生产者生产时,topic不保存消息它是无状态的不落地,假如无人订阅就去生产,那就是一条废消息,所以,一般先启动消费者再启动生产者。
默认情况下如上所述,但是JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。一句话,好比我们的微信公众号订阅
生产者入门案例
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
// 目的地的名称
public static final String TOPIC_NAME = "first_queue";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
// 4 创建目的地(两种 :队列/主题),Destination是Queue和Topic的父类
//此处使用Topic
Topic topic = session.createTopic(TOPIC_NAME);
MessageProducer messageProducer = session.createProducer(topic);
// 6 通过messageProducer 生产 3 条 消息发送到消息队列中
for (int i = 1; i < 4 ; i++) {
// 7 创建消息
TextMessage textMessage = session.createTextMessage("msg--" + i);
// 8 通过messageProducer发送给mq
messageProducer.send(textMessage);
}
// 9 关闭资源
messageProducer.close();
session.close();
connection.close();
System.out.println(" **** 消息发送到MQ完成 ****");
}
}
ActiveMQ控制台之topic
消费者入门案例
// 消息的消费者
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String TOPIC_NAME = "first_queue";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
// 5 创建消息的消费者
MessageConsumer messageConsumer = session.createConsumer(topic);
while(true){
// reveive() 一直等待接收消息,在能够接收到消息之前将一直阻塞,是同步阻塞方式 和socket的accept方法类似的。
// reveive(Long time) 等待n毫秒之后还没有收到消息,就是结束阻塞。
// 因为消息发送者是 TextMessage,所以消息接受者也要是TextMessage
TextMessage message = (TextMessage)messageConsumer.receive();
if (null != message){
System.out.println("****消费者的消息:"+message.getText());
}else {
break;
}
}
messageConsumer.close();
session.close();
connection.close();
}
}
异步监听式消费者(MessageListener)
// 消息的消费者 也就是回答消息的系统
public class JmsConsumer02 {
public static final String ACTIVEMQ_URL = "tcp://127.0.0.1:61616";
public static final String TOPIC_NAME = "first_queue";
public static void main(String[] args) throws Exception{
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
javax.jms.Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic(TOPIC_NAME);
MessageConsumer messageConsumer = session.createConsumer(topic);
/*
* 通过监听的方式来消费消息,是异步非阻塞的方式消费消息。
* 通过messageConsumer的setMessageListener注册一个监听器,
* 当有消息发送来时,系统自动调用MessageListener的onMessage方法处理消息
*/
messageConsumer.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
// instanceof 判断是否A对象是否是B类的子类
if (null != message && message instanceof TextMessage){
TextMessage textMessage = (TextMessage)message;
try {
System.out.println("****消费者的消息:"+textMessage.getText());
}catch (JMSException e) {
e.printStackTrace();
}
}
}
});
// 让主线程不要结束,因为一旦主线程结束了,其他的线程(如此处的监听消息的线程)也都会被迫结束。
// 实际开发中,我们的程序会一直运行,这句代码都会省略。
System.in.read();
messageConsumer.close();
session.close();
connection.close();
}
}
topic和queue对比
Topic模式队列 | Queue模式队列 | |
---|---|---|
工作模式 | 订阅发布模式 ,如多当前没有订阅者,消息将会被丢弃,如果有多个订阅者那么这些订阅者都会收到消息 | 负载均衡模式 ,如果当前没有消费者,消息也不会被丢弃,如果有多个消费者,那么一条消息也只会发送给其中一个消费者 |
有无状态 | 无状态 | queue数据默认会在MQ上以文件形式保存,比如ActiveMQ一般会保存在:%HOME%data下面,也可以配置成DB存储 |
传递完整性 | 如果没有订阅者,消息将会被丢弃 | 消息不会被丢弃 |
处理效率 | 由于消息将会按照订阅者的数量来复制,所以处理性能会随着订阅者数量的增加而逐渐降低,并且还要结合不同消息协议自身的差异 | 由于一条消息只会发给一个消费者,所以就算消费者再多,性能也不会明显降低,当然不同消息协议的具体性能还是有差异的 |