一、项目背景介绍
曾经我们学习过阻塞队列 (BlockingQueue), 我们说,阻塞队列最大的用途,就是用来实现 生产者消费者模型 。
生产者消费者模型,存在诸多好处,是后端开发的常用编程方式.
- 解耦合
- 削峰填谷
在实际的后端开发中,尤其是分布式系统里,跨主机之间使用生产者消费者模型,也是非常普遍的需求。
因此,我们通常会把阻塞队列,封装成一个独立的服务器程序,并且赋予其更丰富的功能。
这样的程序我们就称为 消息队列 (Message Queue, MQ)。
市面上成熟的消息队列非常多.
- RabbitMQ
- Kafka
- RocketMQ
- ActiveMQ
- ......
其中,RabbitMQ是一个非常知名,功能强大,广泛使用的消息队列。因此咱们就仿照 RabbitMQ,模拟实现一个简单的消息队列。
二、需求分析
核心概念
有以下几个概念:
- 生产者(Producer)
- 消费者(Consumer)
- 中间人(Broker)
- 发布(Publish)
- 订阅(Subscribe)
- 消费(Consume)
其中生产者、消费者、中间人等概念在学习了阻塞队列之后很好理解。
另外的:
发布:指的是生产者向中间人那里投递消息的过程。
订阅:指的是哪些消费者需要从中间人这里获取数据,这个注册的过程,被称为"订阅"。
消费:指的是消费者从中间人处获取数据的过程。
多个生产者和消费者
上述模型中,中间人Broker是最核心的部分,负责消息的存储和转发。
⭐️在 Broker中,又存在以下概念:
- 虚拟机(VirtualHost):类似于 MySQL 的 "database",是一个逻辑上的集合。一个 BrokerServer 上可以存在多个VirtualHost。
- 交换机(Exchange):生产者把消息先发送到 Broker 的 Exchange 上。再根据不同的规则,把消息转发给不同的 Queue。
- 队列(Queue):真正用来存储消息的部分。每个消费者决定自己从哪个 Queue 上读取消息。
- 绑定(Binding):Exchange 和 Queue 之间的关联关系。Exchange 和 Queue 可以理解 "多对多" 关系。使用一个关联表就可以把这两个概念联系起来。
- 消息(Message):传递的内容。
所谓的 Exchange 和 Queue 可以理解成 "多对多" 关系,和数据库中的 "多对多" 一样。意思是:
一个 Exchange 可以绑定多个 Queue(可以向多个 Queue 中转发消息)。
一个 Queue 也可以被多个 Exchange 绑定(一个 Queue 中的消息可以来自于多个 Exchange)。
核心API
对于 Broker 来说,要实现以下核心 API。通过这些 API 来实现消息队列的基本功能。
- 创建队列(queueDeclare)
- 销毁队列(queueDelete)
- 创建交换机(exchangeDeclare)
- 销毁交换机(exchangeDelete)
- 创建绑定(queueBind)
- 解除绑定(queueUnbind)
- 发布消息(basicPublish)
- 订阅消息(basicConsume)
- 确认消息(basicAck)
交换机类型
- Direct:生产者发送消息时,直接指定被该交换机绑定的队列名。
- Fanout:生产者发送的消息会被复制到该交换机的所有队列中。
- Topic:绑定队列到交换机上时,指定一个字符串为 bindingKey。发送消息指定一个字符串为 routingKey。当 routingKey 和 bindingKey 满足一定的匹配条件的时候,则把消息投递到指定队列。
这三种操作就像给 QQ 群发红包。
Direct 是发一个专属红包,只有指定的人能领。
Fanout 是使用了魔法,发一个 10 块钱红包,群里的每个人都能领 10 块钱。
Topic 是发一个画图红包,发 10 块钱红包,同时出个题,得画的像的人,才能领。也是每个领到的人都能领 10 块钱。
持久化
Exchange,Queue,Binding,Message都有持久化需求,将数据保存到硬盘中,当程序重启/主机重启时,保证上述内容不丢失。
网络通讯
其他的服务器,都是通过网络与Broker Server进行交互的。
在网络通讯的过程中,客户端部分要提供对应的api,来实现对服务端的操作。
- 创建 Connection
- 关闭 Connection
- 创建 Channel
- 关闭 Channel
- 创建队列(queueDeclare)
- 销毁队列(queueDelete)
- 创建交换机(exchangeDeclare)
- 销毁交换机(exchangeDelete)
- 创建绑定(queueBind)
- 解除绑定(queueUnbind)
- 发布消息(basicPublish)
- 订阅消息(basicConsume)
- 确认消息(basicAck)
可以看到,在 broker 的基础上,客户端还要增加 Connection 操作和 Channel 操作。
其中一个Connection 对象对应一个 TCP 连接。
Channel 则是 Connection 中的逻辑通道。
一个 Connection 中可以包含多个 Channel。
Channel 和 Channel 之间的数据是独立的,不会相互干扰。
这样的设定主要是为了能够更好地复用 TCP 连接,达到长连接的效果,避免频繁地创建关闭 TCP 连接。
Connection 可以理解成一根网线,Channel 则是网线里具体的线缆。

三、创建项目
创建 SpringBoot 项目,并引入 Spring Web 和 Mybatis 依赖。
创建核心类
创建包 mqserver.core:

Exchange 类
表示一个交换机。
java
package com.example.mq.mqserver.core;
import java.util.HashMap;
import java.util.Map;
/*
表示一个交换机
*/
public class Exchange {
//使用name来作为交换机唯一的的身份标识
private String name;
//交换机的三种类型,DIRECT、FANOUT、TOPIC使用枚举类表示
private ExchangeType type=ExchangeType.DIRECT;
//表示该交换机是否持久化,true为持久化。false为不持久化
private boolean durable=false;
//如果当前交换机,没人使用了,是否自动删除
private boolean autoDelete=false;
//表示创建交换机时指定的一些额外的参数选项
private Map<String,Object> arguments=new HashMap<String, Object>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ExchangeType getType() {
return type;
}
public void setType(ExchangeType type) {
this.type = type;
}
public boolean isDurable() {
return durable;
}
public void setDurable(boolean durable) {
this.durable = durable;
}
public boolean isAutoDelete() {
return autoDelete;
}
public void setAutoDelete(boolean autoDelete) {
this.autoDelete = autoDelete;
}
public Map<String, Object> getArguments() {
return arguments;
}
public void setArguments(Map<String, Object> arguments) {
this.arguments = arguments;
}
}
Exchange 枚举类
交换机使用到三种类型,这里使用枚举类来指定。
java
package com.example.mq.mqserver.core;
//枚举类
public enum ExchangeType {
DIRECT(0),
FANOUT(1),
TOPIC(2);
private final int type;
private ExchangeType(int type){
this.type=type;
}
}
MSGQueue 类
这个类表示一个存储消息的队列,MSG表示的是Message。
java
package com.example.mq.mqserver.core;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import java.lang.runtime.ObjectMethods;
import java.util.HashMap;
import java.util.Map;
/*
* 这个类表示一个存储消息的队列
* MSG表示的是Message*/
public class MSGQueue {
//作为队列的唯一身份标识
private String name;
//表示当前队列是否持久化
private boolean durable=false;
//这个属性表示是否独占,如果为true,则表示这个队列只能被一个消费者使用
private boolean exclusive=false;
//表示当前队列未使用时,是否自动删除
private boolean autoDelete=false;
//表示创建队列时,可能用到的扩展参数
//虽然此处使用的是Map,但我们要将它转换为字符串存进数据库
//不在此处进行修改,对它的get和set方法进行处理即可
private Map<String,Object> arguments=new HashMap<String, Object>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isDurable() {
return durable;
}
public void setDurable(boolean durable) {
this.durable = durable;
}
public boolean isExclusive() {
return exclusive;
}
public void setExclusive(boolean exclusive) {
this.exclusive = exclusive;
}
public boolean isAutoDelete() {
return autoDelete;
}
public void setAutoDelete(boolean autoDelete) {
this.autoDelete = autoDelete;
}
public Map<String, Object> getArguments() {
return arguments;
}
public void setArguments(Map<String, Object> arguments) {
this.arguments = arguments;
}
}
MSGQueue 对象中的 arguments 属性类型是 Map 型,后续在存入数据库时会出现异常,所以在之后要使用 Json 将 Map 格式的 arguments 转换成 String 格式。
Binding 类
表示队列与交换机之间的关联关系。
java
package com.example.mq.mqserver.core;
/*
* 表示队列与交换机之间的关联关系*/
public class Binding {
private String exchangeName;
private String queueName;
//用来表示这个连接
private String bindingKey;
public String getExchangeName() {
return exchangeName;
}
public void setExchangeName(String exchangeName) {
this.exchangeName = exchangeName;
}
public String getQueueName() {
return queueName;
}
public void setQueueName(String queueName) {
this.queueName = queueName;
}
public String getBindingKey() {
return bindingKey;
}
public void setBindingKey(String bindingKey) {
this.bindingKey = bindingKey;
}
}
Message 类
java
package com.example.mq.mqserver.core;
import java.io.Serializable;
import java.util.UUID;
/*
* 表示一个要传递的消息
*/
public class Message implements Serializable {
//表示消息的一些属性
private BasicProperties basicProperties;
private byte[] body;
//下面的属性是用来辅助的
// Message 后续会存储到文件中(如果持久化的话).
// 一个文件中会存储很多的消息. 如何找到某个消息, 在文件中的具体位置呢?
// 使用下列的两个偏移量来进行表示. [offsetBeg, offsetEnd)
// 这俩属性并不需要被序列化保存到文件中~~ 此时消息一旦被写入文件之后,所在的位置就固定了,并不需要单独存储.
// 这俩属性存在的目的,主要就是为了让内存中的 Message 对象,能够快速找到对应的硬盘上的 Message 的位置.
//加上transient表示不被序列化
private transient long offsetBeg = 0; // 消息数据的开头距离文件开头的位置偏移(字节)
private transient long offsetEnd = 0; // 消息数据的结尾距离文件开头的位置偏移(字节)
// 使用这个属性表示该消息在文件中是否是有效消息. (针对文件中的消息, 如果删除, 使用逻辑删除的方式)
// 0x1 表示有效. 0x0 表示无效.
private byte isValid = 0x1;
//创建一个工厂方法,让工厂方法来帮我们封装一下创建Message的过程,其方法名能更好的解释其作用
//这个方法中创建的Message对象,会自动生成唯一的messageId
//万一routingKey与BasicProperties里面的routingKey冲突,以外面的为准
public static Message createMessageById(String routingKey,BasicProperties basicProperties,byte[] body){
Message message=new Message();
if(basicProperties!=null){
message.setBasicProperties(basicProperties);
}
//使用UUID来生成messageId,并加上前缀更好分辨
message.setMessageId("M"+UUID.randomUUID());
message.setRoutingKey(routingKey);
message.setBody(body);
return message;
}
public String getMessageId(){
return basicProperties.getMessageId();
}
public void setMessageId(String id){
basicProperties.setMessageId(id);
}
public String getRoutingId(){
return basicProperties.getRoutingKey();
}
public void setRoutingKey(String key){
basicProperties.setRoutingKey(key);
}
public DeliverModeType getDeliverMode(){
return basicProperties.getDeliverMode();
}
public void setDeliverMode(DeliverModeType type){
basicProperties.setDeliverMode(type);
}
public BasicProperties getBasicProperties() {
return basicProperties;
}
public void setBasicProperties(BasicProperties basicProperties) {
this.basicProperties = basicProperties;
}
public byte[] getBody() {
return body;
}
public void setBody(byte[] body) {
this.body = body;
}
public long getOffsetBeg() {
return offsetBeg;
}
public void setOffsetBeg(long offsetBeg) {
this.offsetBeg = offsetBeg;
}
public long getOffsetEnd() {
return offsetEnd;
}
public void setOffsetEnd(long offsetEnd) {
this.offsetEnd = offsetEnd;
}
public byte getIsValid() {
return isValid;
}
public void setIsValid(byte isValid) {
this.isValid = isValid;
}
}
Message 需要实现 Serializable 接口,后续需要把 Message 写入文件以及进行网络传输。
BasicProperties 类
这个类是用来存放 Message 的一些属性的。
java
package com.example.mq.mqserver.core;
public class BasicProperties {
//消息的唯一身份标识,在构造方法时使用UUID来创建唯一的id
private String messageId;
// 是一个消息上带有的内容, 和 bindingKey 做匹配.
// 如果当前的交换机类型是 DIRECT, 此时 routingKey 就表示要转发的队列名.
// 如果当前的交换机类型是 FANOUT, 此时 routingKey 无意义(不使用).
// 如果当前的交换机类型是 TOPIC, 此时 routingKey 就要和 bindingKey 做匹配. 符合要求的才能转发给对应队列.
private String routingKey;
//表示是否要持久化,1表示不需要持久化,2表示要持久化
private DeliverModeType deliverMode=DeliverModeType.TRUE;
public String getMessageId() {
return messageId;
}
public void setMessageId(String messageId) {
this.messageId = messageId;
}
public String getRoutingKey() {
return routingKey;
}
public void setRoutingKey(String routingKey) {
this.routingKey = routingKey;
}
public DeliverModeType getDeliverMode() {
return deliverMode;
}
public void setDeliverMode(DeliverModeType deliverMode) {
this.deliverMode = deliverMode;
}
}
DeliverModeType 枚举类
用来指定 BasicProperties 中 deliverMode 属性,来判断 Message 对象是否持久化。
java
package com.example.mq.mqserver.core;
public enum DeliverModeType {
TRUE(1),
FALSE(2);
private final int deliverMode;
private DeliverModeType(int type){
this.deliverMode=type;
}
}
多个生产者和消费者