文章目录
前言
在学习消息中间件的过程中,对rabbitMQ的学习进行记录。
一、消息中间件概述
1. 什么是消息中间件?
MQ全称为Message Queue,消息队列是应用程序和应用程序之间的通信方法。
2. 为什么使用消息中间件?
在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
3. 开发中消息队列的应用场景
- 任务异步处理
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。 - 应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。 - 削峰填谷
如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。
消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。
但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷”
4. 消息队列产品
市场上常见的消息队列有如下:
- ActiveMQ:基于JMS
- ZeroMQ:基于C语言开发
- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品
- Kafka:类似MQ的产品;分布式消息系统,高吞吐量
二、RabbitMQ
1.RabbitMQ简介
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
RabbitMQ官方地址:http://www.rabbitmq.com/
2.RabbitMQ安装配置
关于安装配置这里不多赘述,有需要的可以查找网上的步骤
安装配置完成后启动服务,输入地址http://127.0.0.1:15672/
填写默认用户名密码guest登录
3.搭建示例工程
- 创建Maven项目
- 引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
- 编写生产者和消费者,可以参考下面的几种模式
4.RabbitMQ的几种工作模式
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
4.1 simple简单模式
生产者Send.java
。
/**
* simple简单模式:一个生产者,一个消费者
* 消息产生着将消息放入队列
* 消息的消费者(consumer) 监听(while) 消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除
* (隐患 消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失)应用场景:聊天(中间有一个过度的服务器;p端,c端)
*/
public class Send {
//Set up the class and name the queue:
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try {
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消费者Recv.java
。
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
4.2 work工作模式
生产者NewTask.java
。
/**
* work工作模式(资源的竞争):一个生产者,多个消费者,每个消费者获取到的消息唯一。
* 消息产生者将消息放入队列.消费者可以有多个,消费者1,消费者2,同时监听同一个队列,消息被消费?
* C1 C2共同争抢当前的消息队列内容,谁先拿到谁负责消费消息
* (隐患,高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个开关(syncronize,与同步锁的性能不一样) 保证一条消息只能被一个消费者使用)
*/
/*
特点:
1、一条消息只会被一个消费端接收;
2、队列采用轮询的方式将消息是平均发送给消费者的;
3、消费者在处理完某条消息后,才会收到下一条消息
*/
public class NewTask {
//Set up the class and name the queue:
private final static String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try {
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
String message = String.join(" ", args);
channel.basicPublish("",TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消费者Worker.java
。
public class Worker {
private final static String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
try {
doWork(message);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(" [x] Done");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
private static void doWork(String task) throws InterruptedException {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(1000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
4.3 publish/subscribe发布订阅模式
生产者EmitLog.java
。
/**
* publish/subscribe发布订阅(共享资源):一个生产者发送的消息会被多个消费者获取。
* X代表交换机rabbitMQ内部组件,erlang 消息产生者是代码完成,代码的执行效率不高
* 消息产生者将消息放入交换机,交换机发布订阅把消息发送到所有消息队列中,对应消息队列的消费者拿到消息进行消费
*/
/*
1、每个消费者监听自己的队列;
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息
应用场景:用户通知,当用户充值成功或转账完成系统通知用户,通知方式有短信、邮件多种方法;
*/
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info: Hello World!" :
String.join(" ", argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
消费者ReceiveLogs.java
。
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
4.4 routing路由模式
生产者EmitLogDirect.java
。
/**
* routing路由模式:发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key
* 消息生产者将消息发送给交换机按照路由判断,路由是字符串(info)
* 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息;
* 根据业务功能定义路由字符串
* 从系统的代码逻辑中获取对应的功能字符串,将消息任务扔到对应的队列中
* 业务场景:error 通知;EXCEPTION;错误通知的功能;传统意义的错误通知;客户通知;
* 利用key路由,可以将程序中的错误封装成消息传入到消息队列中,开发者可以自定义消费者,实时接收错误;
*/
/*
1、每个消费者监听自己的队列,并且设置routingkey;
2、生产者将消息发给交换机,由交换机根据routingkey来转发消息到指定的队列;
应用场景:用户通知,当用户充值成功或转账完成系统通知用户,通知方式有短信、邮件多种方法;
*/
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//发送error两个队列都会收到消息
String routingKey = "error";
//发送info 只有消费者2会收到消息
String routingKey2 = "info";
String message = "hello exchange direct";
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes());
System.out.println("Send message" + message);
}
}
}
消费者ReceiveLogsDirect1.java
。
public class ReceiveLogsDirect1 {
private static final String EXCHANGE_NAME = "direct_logs";
private static final String QUEUE_NAME="queue_direct_one";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String routingKey = "error";
//绑定队列到交换机 转发器
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,routingKey);
//保证一次只发一个
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "utf-8");
System.out.println("[1] Recv message:" + message);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
System.out.println("[Consumer 1 start]");
}
}
消费者ReceiveLogsDirect2.java
。
public class ReceiveLogsDirect2 {
private static final String EXCHANGE_NAME = "direct_logs";
private static final String QUEUE_NAME="queue_direct_two";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String routingKey = "error";
String routingKey2 = "info";
String routingKey3 = "warning";
//绑定队列到交换机 转发器
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,routingKey);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,routingKey2);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,routingKey3);
//保证一次只发一个
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "utf-8");
System.out.println("[2] Recv message:" + message);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
System.out.println("[Consumer 2 start]");
}
}
4.5 topic主题模式
生产者EmitLogTopic.java
。
/**
* topic 主题模式(路由模式的一种):将路由键和某模式进行匹配,此时队列需要绑定在一个模式上,“#”匹配一个词或多个词,“*”只匹配一个词。
* 路由功能添加模糊匹配
* 消息产生者产生消息,把消息交给交换机
* 交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费
*/
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String message = "goods......";
//发送goods.add 消费者1和消费者2都可以收到消息
//发送goods.delete 消费者2收到消息
String routingKey = "goods.add";
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes());
System.out.println("Send message"+message);
}
}
}
消费者ReceiveLogsTopic1.java
。
public class ReceiveLogsTopic1 {
private static final String EXCHANGE_NAME = "topic_logs";
private static final String QUEUE_NAME="queue_topic_one";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String routingKey = "goods.add";
//绑定队列到交换机 转发器
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,routingKey);
//保证一次只发一个
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] Recv msg:" + msg);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
System.out.println("[Consumer 1 start]");
}
}
消费者ReceiveLogsTopic2.java
。
public class ReceiveLogsTopic2 {
private static final String EXCHANGE_NAME = "topic_logs";
private static final String QUEUE_NAME="queue_topic_two";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机 转发器
String routingKey = "goods.#";
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,routingKey);
//保证一次只发一个
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] Recv msg:" + msg);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
System.out.println("[Consumer 2 start]");
}
}
三、AMQP
1.相关概念介绍
AMQP 一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。
AMQP是一个二进制协议,拥有一些现代化特点:多信道、协商式,异步,安全,扩平台,中立,高效。
RabbitMQ是AMQP协议的Erlang的实现。
概念 | 说明 |
---|---|
连接Connection | 一个网络连接,比如TCP/IP套接字连接。 |
会话Session | 端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。 |
信道Channel | 多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。 |
客户端Client | AMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。 |
服务节点Broker | 消息中间件的服务节点;一般情况下可以将一个RabbitMQ Broker看作一台RabbitMQ 服务器。 |
端点 | AMQP对话的任意一方。一个AMQP连接包括两个端点(一个是客户端,一个是服务器)。 |
消费者Consumer | 一个从消息队列里请求消息的客户端程序。 |
生产者Producer | 一个向交换机发布消息的客户端应用程序。 |
2.RabbitMQ运转流程:
- 生产者发送消息
1.生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker;
2.声明队列并设置属性;如是否排它,是否持久化,是否自动删除;
3.将路由键(空字符串)与队列绑定起来;
4.发送消息至RabbitMQ Broker;
5.关闭信道;
6.关闭连接; - 消费者接收消息
1.消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker
2.向Broker 请求消费相应队列中的消息,设置相应的回调函数;
3.等待Broker回应闭关投递响应队列中的消息,消费者接收消息;
4.确认(ack,自动确认)接收到的消息;
5.RabbitMQ从队列中删除相应已经被确认的消息;
6.关闭信道;
7.关闭连接;