上篇博文主要分析了三种不同的请求方式,其中提到了基于消息队列的请求,当然只是从理论的角度去进行了分析,本篇博文就再次结合具体实现来说说消息队列。
一、什么是消息队列?
作为中间件,消息队列是分布式应用间交换信息的重要组件。消息队列可驻留在内存或磁盘上, 队列可以存储消息直到它们被应用程序读走。通过消息队列,应用程序可以在不知道彼此位置的情况下独立处理消息,或者在处理消息前不需要等待接收此消息。所以消息队列可以解决应用解耦、异步消息、流量削锋等问题,是实现高性能、高可用、可伸缩和最终一致性架构中不可以或缺的一环。
简单的来说,消息队列就是独立于客户端与服务端,将消息(请求)以队列的形式存储起来,等待服务端进行读取。
二、消息队列的原理分析
如下图所示,用户1、2、3同时向服务端系统发送请求,三个请求会先被分配到队列中存储起来,服务端会监听队列中的消息,一旦系统空闲,并且监听到队列中有消息,系统就会从队列中取出消息,并进行处理。如此设计,系统可以按照自己的节奏去处理请求,从而减轻服务端的压力,保证业务处理的流畅;即使系统由于某些原因停止运行,由于未处理的请求仍保存在队列中,这些请求也不会丢失。
三、消息队列的架构
消息队列主要分为点对点(Queue)模式和订阅(Topic)模式两种,点对点模式的整体流程如下图所示:
主要角色有生产者、消息、队列、消费者
生产者将生产出来的消息塞入到队列中,消费者从队列中取出消息并消费,被消费完的消息不会存在在队列中,一条消息只会被一个消费者消费一次;
订阅模式与点对点模式整体流程相似,不同在于,由于是订阅关系,生产者生产出来的消息,会被所有的消费者接收到;(由于本文以主要分析点对点模式,这里仅大致介绍一下订阅模式,之后会对订阅模式进行补充)
常用的消息中间件有:ActivieMq、RabbitMq、RocketMq和kafka这几种,本文是以ActivieMq作为消息中间件的;
四、具体实现
1、安装ActiveMq
本文使用的是5.15.5版本的,下载地址是http://activemq.apache.org/activemq-5155-release.html
下载解压之后,进入到bin目录下,如果是windows版本,可以直接点击activemq.bat启动;如果是linux版本,则执行./activemq start;
2、在项目中导入相应jar包
在pom中添加maven:
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.5</version>
</dependency>
3、创建生产者:
Message(消息)有TextMessage(文本消息)、MapMessage(键值对消息)、StreamMessage(流消息)、BytesMessage(字节消息)和ObjectMessage(对象消息),由于MapMessage是一种结构比较灵活的消息类型,因此本文以此为例;
/**
*
* @author yuyan
* @create 2018-08-28 16:09
**/
@Service
public class Producer {
public void sendMessage(String msg){
try {
//创建连接工厂,三个参数分别是用户名、密码以及消息队列所在地址
ActiveMQConnectionFactory connFactory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,
"tcp://localhost:61616");
//连接到JMS提供者
Connection conn = connFactory.createConnection();
//开启连接
conn.start();
//事务性会话,自动确认消息
Session session = conn.createSession(true, Session.AUTO_ACKNOWLEDGE);
//消息的目的地,创建队列"queue"
Destination destination = session.createQueue("queue");
//消息生产者
MessageProducer producer = session.createProducer(destination);
// //文本消息
// TextMessage textMessage = session.createTextMessage("这是文本消息");
// producer.send(textMessage);
//键值对消息
MapMessage mapMessage = session.createMapMessage();
//将消息内容放入到消息里
mapMessage.setString("reqDesc", msg);
//生产者传送消息
producer.send(mapMessage);
//
// //流消息
// StreamMessage streamMessage = session.createStreamMessage();
// streamMessage.writeString("这是流消息");
// producer.send(streamMessage);
//
// //字节消息
// String s = "BytesMessage字节消息";
// BytesMessage bytesMessage = session.createBytesMessage();
// bytesMessage.writeBytes(s.getBytes());
// producer.send(bytesMessage);
//
// //对象消息
// User user = new User("obj_info", "对象消息"); //User对象必须实现Serializable接口
// ObjectMessage objectMessage = session.createObjectMessage();
// objectMessage.setObject(user);
// producer.send(objectMessage);
session.commit(); //提交会话,该条消息会进入"queue"队列,生产者也完成了历史使命
producer.close();
session.close();
conn.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
4、创建消费者:
消费者监听消息的方式有两种,一种是通过Api提供的监听器去实现监听;另一种是通过循环的方式去主动接收消息。这里将消费者设计成一个组件,在服务器启动时,消费者就会被创建,并监听队列。
/**
*
* @author yuyan
* @create 2018-08-28 18:39
**/
@Component
public class Comsumer implements ApplicationRunner{
@Override
public void run(ApplicationArguments args) throws Exception {
init();
}
public void init() throws JMSException {
//前期的初始化工作与生产者相同
ConnectionFactory factory = new ActiveMQConnectionFactory(
ActiveMQConnectionFactory.DEFAULT_USER,
ActiveMQConnectionFactory.DEFAULT_PASSWORD,
"tcp://localhost:61616"
);
Connection conn = factory.createConnection();
conn.start();
Session session = conn.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
//与生产者的消息目的地相同
Destination dest = session.createQueue("queue");
MessageConsumer messConsumer = session.createConsumer(dest);
//方式1设置消息监听
messConsumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
try {
MapMessage m = (MapMessage)message;
System.out.println("consumer接收到"+m.getString("reqDesc")+"的请求并开始处理,时间是"+new Date());
System.out.println("这里会停顿5s,模拟系统处理请求,时间是"+new Date());
Thread.sleep(5000);
System.out.println("consumer接收到"+m.getString("reqDesc")+"的请求并处理完毕,时间是"+new Date());
}catch (Exception e){
e.printStackTrace();
}
}
});
//方式2主动接收消息
// while(true){
// try {
// MapMessage m = (MapMessage) messConsumer.receive();
//
// Thread.sleep(1000);
// System.out.println(m.getString("reqDesc"));
// }catch (Exception e){
// e.printStackTrace();
// }
//
// }
// if(conn != null)conn.close();
}
}
5、写一个接口并进行测试
/**
*
* @author yuyan
* @create 2018-08-28 16:32
**/
@Controller
@RequestMapping(value = "MessageCenter")
public class MessageController {
@Autowired
//创建一个生产者,消费者在系统运行时已经创建
Producer producer;
@RequestMapping(value = "/SendMessageByQueue", method = RequestMethod.GET)
@ResponseBody
public void send(String msg) {
try {
System.out.println(msg+"开始发出一次请求,时间是"+new Date());
producer.sendMessage(msg);
System.out.println(msg+"请求发送完成,时间是"+new Date());
}catch (Exception e){
e.printStackTrace();
}
}
}
测试结果如下:
通过观察两个红框可以发现:1、2两个请求是几乎同时发出的,用户2先进入队列,随后1进入,通过观察两个绿框可以得知,由于2先进入队列,2先执行,5s执行完后,1才开始执行;
我们也可以通过http://localhost:8161/admin/queues.jsp,也就是ActiveMq的管理界面去观察变化:
Name:队列的名称
Number Of Pending Messages:队列中还未被处理的消息数量
Numer Of Consumers:消费者的数量
Messages Enququed:入队列的消息数量(包括已出队的)
Messages Deququed:出队列的消息数量
这是队列的初始状态:
当两个请求入队后:
此时队列中有两条入队(未被处理)的消息,
当一个请求并服务端(消费者)取走后:
此时队列中还未被处理的消息数量为1,出队列的消息数量为1;
当另一个请求也被服务端取走后:
队列中再无其他消息,两条消息均已出列;
那么,一个消息队列的流程就已经全部走完了。
五、总结
消息队列并不能提高系统的运行速度(如果想提高速度,还是需要用到多线程等方式),消息队列作为中间件的作用是降低应用间的耦合,在高并发、高流量的情况下保证服务端的稳定,保证业务流程的顺畅和数据的完整(请求不丢失)。