RabbitMQ
前言
消息中间件负责数据的传递,存储,分发,三部分,数据存储和分发的过程中肯定要遵循某种约定成的规范,而这些规范就是协议
一、消息队列协议
1. AMQP协议
AMQP:(全称: Advanced Message Queuing Protocol)是高级消息队列协议。由摩根大通集团联合其他公司共同设计(与 Spring是一家公司)。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
特性:
- 分布式事务支持。
- 消息的持久化支持。
- 高性能和高可靠的消息处理优势。
2.MQTT协议
MQT协议: (Message Queueing Telemetry Transport)消息队列是IBM开放的一个即时通讯协议,物联网系统架构中的重要组成部分。
特点:
- 轻量
- 结构简单
- 传输快,不支持事考
- 没有持久化设计
应用场景
- 适用于计算能力有限
- 低带宽网络不稳定的场景
3.OpenMessage协议
是近几年由阿里、雅虎和滴滴出行、Sltrermalio等公司共同参与创立的分布式消息中间件、流处理等领域的应用开发标准。
特点:
- 结构简单
- 解析速度快
- 支持事务和持久化设计
4.kafka协议
Kafka协议是基于TCP/IP的二进制协议。消息内部足通过长度来分割,由一些基本数据类型组成。
特点:
- 结构简单
- 解析速度快
- 无事务支持
- 有持久化设计
二、消息队列持久化
1.常见的持久化方式
持久化方式 | ActiveMQ | RabbitMQ | kafka | RocketMQ |
---|---|---|---|---|
文件存储 | 支持 | 支持 | 支持 | 支持 |
数据库 | 支持 | / | / | / |
2.消息分发测略
MQ消息队列有如下几个角色
- 生产者
- 存储消息
- 消费者
那么生产者生成消息以后,MQ进行存,消费者是如何获取消息的呢?
一般获取数据的方式无外乎推(push)或者拉(pull)两种方式,典型的git就有推拉机制,我们发送的Ihttp请求就是一种典型的拉取数据库数据返回的过程。而消息队列MQ是一种推送的过程,而这些推机制会适用到很多的业务场景也有很多对应推机制策略。
三、入门案例
1.simple 简单模式
package com.rabbitmq.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
/**
* @author chuyao
* @version 1.0
* @date 2021/7/26 10:34
* @description: TODO 生产者简单模式 simple
*/
public class Producer {
public static void main(String[] args) {
//1.创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("47.108.167.33");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//2.创建 connection
connection = connectionFactory.newConnection();
//3.通过连接获取通道
channel = connection.createChannel();
//4.创建交换机,声明队列
String name = "queue";
/**
* @params1 队列名称
* @params2 是持久化,不管true/false持久化都会存盘,false的时候持久化存盘会随着服务器的重启而丢失。
* @params3 排他性,是否是独占独立
* @params4 是否自动删除,随着最后一个消费者消费完毕后是否把队列删除
* @params5 附带数据
*/
channel.queueDeclare(name,false,false,false,null);
//5.发送消息给队列
channel.basicPublish("",name,null,"hello rabbitMQ".getBytes(StandardCharsets.UTF_8));
System.out.println("发送成功");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
try {
//6.关闭连接
if (connection != null && connection.isOpen())
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
//7.关闭通道
if (channel != null && channel.isOpen())
channel.close();
} catch (IOException e) {
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
package com.rabbitmq.simple;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
1. @author chuyao
2. @version 1.0
3. @date 2021/7/26 10:35
4. @description: TODO 消费者简单模式 simple
*/
public class Consumer {
public static void main(String[] args) {
//1.创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("47.108.167.33");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//2.创建 connection
connection = connectionFactory.newConnection();
//3.通过连接获取通道
channel = connection.createChannel();
//4.创建交换机,声明队列
String name = "queue";
channel.basicConsume(name, true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("接受消息" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("接受消息失败");
}
});
System.in.read();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
try {
//6.关闭连接
if (connection != null && connection.isOpen())
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
//7.关闭通道
if (channel != null && channel.isOpen())
channel.close();
} catch (IOException e) {
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
常见面试题
-
消息队列不开启持久化数据会存盘吗?
答案:是会的,首先开启持久化和不开启持久化数据都会存盘!只是当你不开始持久化时 数据会根据你服务器下一次的重启而丢失。
-
RabbitMQ 为什么是基于 channel (信道)去处理而不是 connection (连接)?
答案:因为 connection 是一个短连接,短连接就会经过三次握手四次挥手,是会很慢的,耗时很长,而且连接需要开启和关闭,这会造成很昂贵的性能开销。所以rabbitmq把连接处理变成一个长连接,而长连接里面会有很多信道 channel ,当我们在高并发的场景下信道的性能非常高。它可以创建多个信道,一个连接可以创建多个信道来处理消息。所以说它的性能是非常高的!
-
可以存在没有交换机的队列吗?
答案:肯定是不可能的,就算你没有指定交换机,而它一定会存在一个默认的交换机(默认交换机是一种分发策略)!因为 rabbitmq底层语言采用的是Erlang语言,本身就是专门开发交换机的语言。交换机就负责消息的分发投递
2.work工作模式
工作模式有两种模式:
- 轮询模式的分发:—个消费者一条,按均分配;
- 公平分发:根据消费者的消费力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配
RabbitMQ核心组成部分
RabbitMQ的操作流程
- 获取Conection
- 获取Channel
- 定义Exchange,Queue
- 使用一个RoutingKey将Queue Binding到一个Exchange上
- 通过指定一个Exchange和一个RoutingKey来将消息发送到对应的Queue上
- Consumer在接收时也是获取connection,接着获取channel,然后指定一个Queue,到Queue上取消息,它对Exchange,RoutingKey及如何Binding都不关心,到对应的Queue上去取消息就行了。
**注意:**一个PublisherClient发送消息,哪些ConsumerClient可以收到消息,在于Exchange,RoutingKey,Queue的关系上
RabbitMQ的运行流程图
四、RabbitMQ使用场景
1.解耦,消峰,异步
同步异步问题(串行)
串行方式:将订单信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端
并行方式,异步线程池
并行方式:将订单信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间
异步线程池存在问题:
- 耦合度高
- 需要自己写线程池自己维护成本太高
- 出现了消息可能会丢失,需要你自己做消息补偿
- 如何保证消息的可靠性你自己写
- 如果服务器承载正了,你需要自己去写高可用
异步消息队列的方式
好处
- 完全解耦,用MQ建立桥接
- 有独立的线程池和运行模型
- 出现了消息可能会丢失,MQ有持久化功能
- 如何保证消息的可靠性,死信队列和消息转移的等
- 如果服务器承载不了,你需要自己去写高可用,HA镜像模型高可用。
按照以上约定,用户的响应时间相当于是订单信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20
QPS。比串行提高了3倍,比并行提高了两倍
2.高内聚,低耦合
其他场景:
- 流量的削峰
- 分布式事务的可靠消费和可靠生产
- 索、缓存、静态化处理的数据同步
- 流量监控
- 日志监控(ELK)08、下单、订单分发、抢票