1、入门概念
1.1、MQ的产品种类和对比
(1) kafka
编程语言:scala。
大数据领域的主流MQ。
(2) rabbitmq
编程语言:erlang
基于erlang语言,不好修改底层,不要查找问题的原因,不建议选用。
(3) rocketmq
编程语言:java
适用于大型项目。适用于集群。
(4) activemq
编程语言:java
适用于中小型项目。
1.2、MQ的产生背景
微服务架构后,链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(或子模块),比如模块A调用模块B,模块B调用模块C,模块C调用模块D。但在大型分布式应用中,系统间的RPC交互繁杂,一个功能背后要调用上百个接口并非不可能,从单机架构过渡到分布式微服务架构的通例。
系统之间直接调用存在的问题?
1、系统之间接口耦合比较严重
每新增一个下游功能,都要对上游的相关接口进行改造;
- 举个例子:如果系统A要发送数据给系统B和系统C,发送给每个系统的数据可能有差异,因此系统A对要发送给每个系统的数据进行了组装,然后逐一发送;当代码上线后又新增了一个需求:把数据也发送给D,新上了一个D系统也要接受A系统的数据,此时就需要修改A系统,让他感知到D系统的存在,同时把数据处理好再给D。在这个过程你会看到,每接入一个下游系统,都要对系统A进行代码改造,开发联调的效率很低。其整体架构如下图:
2、面对大流量并发时,容易被冲垮
每个接口模块的吞吐能力是有限的,这个上限能力如果是堤坝,当大流量(洪水)来临时,容易被冲垮。
- 举个例子秒杀业务:上游系统发起下单购买操作,就是下单一个操作,很快就完成。然而,下游系统要完成秒杀业务后面的所有逻辑(读取订单,库存检查,库存冻结,余额检查,余额冻结,订单生产,余额扣减,库存减少,生成流水,余额解冻,库存解冻)。
3、等待同步存在性能问题
RPC接口上基本都是同步调用,整体的服务性能遵循“木桶理论”,即整体系统的耗时取决于链路中最慢的那个接口。比如A调用B/C/D都是50ms,但此时B又调用了B1,花费2000ms,那么直接就拖累了整个服务性能。
根据上述的几个问题,在设计系统时可以明确要达到的目标:
- 要做到系统解耦,当新的模块接进来时,可以做到代码改动最小;能够解耦
- 设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮;能削峰
- 强弱依赖梳理能将非关键调用链路的操作异步化并提升整体系统的吞吐能力;能够异步
1.3、MQ的主要作用
- 异步。调用者无需等待。
- 解耦。解决了系统之间耦合调用的问题。
- 消峰。抵御洪峰流量,保护了主业务。
1.4、MQ的定义
-
面向消息的中间件(message-oriented middleware)MOM能够很好的解决以上问题。是指利用高效可靠的消息传递机制与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型在分布式环境下提供应用解耦,弹性伸缩,冗余存储、流量削峰,异步通信,数据同步等功能。
-
大致的过程是这样的:发送者把消息发送给消息服务器,消息服务器将消息存放在若干队列/主题topic中,在合适的时候,消息服务器回将消息转发给接受者。在这个过程中,发送和接收是异步的,也就是发送无需等待,而且发送者和接受者的生命周期也没有必然的关系;尤其在发布pub/订阅sub模式下,也可以完成一对多的通信,即让一个消息有多个接受者。
1.5、MQ的特点
1、采用异步处理模式
消息发送者可以发送一个消息而无须等待响应。消息发送者将消息发送到一条虚拟的通道(主题或者队列)上;消息接收者则订阅或者监听该爱通道。一条消息可能最终转发给一个或者多个消息接收者,这些消息接收者都无需对消息发送者做出同步回应。整个过程都是异步的。
- 案例:也就是说,一个系统跟另一个系统之间进行通信的时候,假如系统A希望发送一个消息给系统B,让他去处理。但是系统A不关注系统B到底怎么处理或者有没有处理好,所以系统A把消息发送给MQ,然后就不管这条消息的“死活了”,接着系统B从MQ里面消费出来处理即可。至于怎么处理,是否处理完毕,什么时候处理,都是系统B的事儿,与系统A无关。
2、应用系统之间解耦合
-
发送者和接受者不必了解对方,只需要确认消息。
-
发送者和接受者不必同时在线。
3、整体架构
4、缺点
两个系统之间不能同步调用,不能实时回复,不能响应某个调用的回复。
2、ActiveMQ 安装和控制台
2.1、ActiveMQ 安装
[root@localhost /]# docker pull webcenter/activemq #拉取镜像
Using default tag: latest
latest: Pulling from webcenter/activemq
7dcf5a444392: Pull complete
9eebba75a87f: Pull complete
1f0440d87cc7: Pull complete
dacd0555c1b4: Pull complete
b0f19aa05a94: Pull complete
[root@localhost /]# docker run -d -p 8161:8161 -p 61616:61616 webcenter/activemq #启动并映射端口
1ff9ee0303bb0b686b74c708f4db2af3a5cfdcb0baf327792e5ba9e553bb7761
2.2、ActiveMQ 控制台
-
访问activemq管理页面地址:http://IP地址:8161/
账户admin 密码admin
-
进入
3、API使用
3.1、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>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
JMS开发的基本步骤
3.2、消息队列(Queue)生产者的入门案例
public class JmsProduce {
public static final String ACTIVEMQ_URL = "tcp://192.168.5.130:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
// 1、创建连接工厂,按照给定的URL地址,采用默认的用户名和密码admin
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2、获得连接对象,并启动
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3、创建会话session,两个参数,第一个是事务,第二个是签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
// 5、创建消息的生产者
MessageProducer producer = session.createProducer(queue);
// 6、使用messageProducer生产3条消息发送到MQ的队列里面
for (int i = 0; i < 3; i++) {
TextMessage textMessage = session.createTextMessage("我是第" + i + "条message!");
producer.send(textMessage);
}
// 7、关闭资源
producer.close();
session.close();
connection.close();
System.out.println("消息发布到MQ完成!");
}
}
控制台信息
- Number Of Pending Messages:
等待消费的消息,这个是未出队列的数量,公式=总接收数-总出队列数。
- Number Of Consumers:
消费者数量,消费者端的消费者数量。
- Messages Enqueued:
进队消息数,进队列的总消息量,包括出队列的。这个数只增不减。
- Messages Dequeued:
出队消息数,可以理解为是消费者消费掉的数量。
3.3、消息队列消费者的入门案例
public class JmsConsumer {
public static final String ACTIVEMQ_URL = "tcp://192.168.5.130:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException {
// 1、创建连接工厂,按照给定的URL地址,采用默认的用户名和密码admin
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2、获得连接对象,并启动
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3、创建会话session,两个参数,第一个是事务,第二个是签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
// 5、创建消息的消费者
MessageConsumer consumer = session.createConsumer(queue);
while (true) {
// reveive() 一直等待接收消息,在能够接收到消息之前将一直阻塞。是同步阻塞方式 。
TextMessage textMessage = (TextMessage) consumer.receive();
if (textMessage != null) {
System.out.println("消费者收到的消息:" + textMessage.getText());
} else {
break;
}
}
consumer.close();
session.close();
connection.close();
}
}
3.4、异步监听式消费者(MessageListener)
public class MessageListener {
public static final String ACTIVEMQ_URL = "tcp://192.168.5.130:61616";
public static final String QUEUE_NAME = "queue01";
public static void main(String[] args) throws JMSException, IOException {
// 1、创建连接工厂,按照给定的URL地址,采用默认的用户名和密码admin
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2、获得连接对象,并启动
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3、创建会话session,两个参数,第一个是事务,第二个是签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地(具体是队列还是主题topic)
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer consumer = session.createConsumer(queue);
/* 通过监听的方式来消费消息,是异步非阻塞的方式消费消息。
通过messageConsumer 的setMessageListener 注册一个监听器,当有消息发送来时,系统自动调用MessageListener 的 onMessage 方法处理消息
*/
consumer.setMessageListener(new javax.jms.MessageListener() {
public void onMessage(Message message) {
// instanceof 判断是否A对象是否是B类的子类
if (message != null && message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者的消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
System.out.println("主线程运行到这了...");
// 让主线程不要结束。因为一旦主线程结束了,其他的线程(如此处的监听消息的线程)也都会被迫结束。
// 实际开发中,我们的程序会一直运行,这句代码都会省略。
System.in.read();
consumer.close();
session.close();
connection.close();
}
}
3.5、队列消息(Queue)总结
1、两种消费方式
- 同步阻塞方式(receive):订阅者或接收者抵用MessageConsumer的receive()方法来接收消息,receive方法在能接收到消息之前(或超时之前)将一直阻塞。
- 异步非阻塞方式(监听器MessageListener):订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器,当消息到达之后,系统会自动调用监听器MessageListener的onMessage(Message message)方法。
2、队列的特点
3、消息消费情况
情况1:只启动消费者1。
- 结果:消费者1会消费所有的数据。
情况2:先启动消费者1,再启动消费者2。
- 结果:消费者1消费所有的数据。消费者2不会消费到消息。
情况3:生产者发布6条消息,在此之前已经启动了消费者1和消费者2。
- 结果:消费者1和消费者2平摊了消息。各自轮询的消费3条消息。
3.6、Topic 主题入门案例
1、topic 介绍
在发布订阅消息传递域中,目的地被称为主题(topic)
发布/订阅消息传递域的特点如下:
(1)生产者将消息发布到topic中,每个消息可以有多个消费者,属于1:N的关系;
(2)生产者和消费者之间有时间上的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息。
(3)生产者生产时,topic不保存消息它是无状态的不落地,假如无人订阅就去生产,那就是一条废消息,所以,一般先启动消费者再启动生产者。
public class MessageListener_topic {
public static final String ACTIVEMQ_URL = "tcp://192.168.5.130:61616";
public static final String TOPIC_NAME = "topic01";
public static void main(String[] args) throws JMSException, IOException {
System.out.println("我是2号消费者");
// 1、创建连接工厂,按照给定的URL地址,采用默认的用户名和密码admin
ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
// 2、获得连接对象,并启动
Connection connection = activeMQConnectionFactory.createConnection();
connection.start();
// 3、创建会话session,两个参数,第一个是事务,第二个是签收
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
// 4、创建目的地(具体是队列还是主题topic)
Topic topic = session.createTopic(TOPIC_NAME);
MessageConsumer consumer = session.createConsumer(topic);
/* 通过监听的方式来消费消息,是异步非阻塞的方式消费消息。
通过messageConsumer 的setMessageListener 注册一个监听器,当有消息发送来时,系统自动调用MessageListener 的 onMessage 方法处理消息
*/
consumer.setMessageListener(new MessageListener() {
public void onMessage(Message message) {
// instanceof 判断是否A对象是否是B类的子类
if (message != null && message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("消费者的消息:" + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
System.out.println("主线程运行到这了...");
// 让主线程不要结束。因为一旦主线程结束了,其他的线程(如此处的监听消息的线程)也都会被迫结束。
// 实际开发中,我们的程序会一直运行,这句代码都会省略。
System.in.read();
consumer.close();
session.close();
connection.close();
}
}