Java自定义协议的发布订阅式消息队列(一)

一、项目背景介绍

曾经我们学习过阻塞队列 (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 来实现消息队列的基本功能。

  1. 创建队列(queueDeclare)
  2. 销毁队列(queueDelete)
  3. 创建交换机(exchangeDeclare)
  4. 销毁交换机(exchangeDelete)
  5. 创建绑定(queueBind)
  6. 解除绑定(queueUnbind)
  7. 发布消息(basicPublish)
  8. 订阅消息(basicConsume)
  9. 确认消息(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;
    }
}
相关推荐
一直都在5721 小时前
手写tomcat(2):Servlet原理和自定义tomcat
java·servlet·tomcat
方也_arkling1 小时前
【JS】日期对象及时间戳的使用(制作距离指定日期的倒计时)
开发语言·javascript·ecmascript
Zfox_1 小时前
【Go】反射
开发语言·后端·golang
Seven971 小时前
数据结构——树
java
郝学胜-神的一滴1 小时前
Linux kill命令与kill函数:从信号原理到实战解析
linux·服务器·开发语言·c++·程序人生
shyの同学1 小时前
Spring事务:为什么catch了异常,事务还是回滚了?
数据库·spring·事务·spring事务
未来之窗软件服务1 小时前
操作系统应用(三十七)C#华旭金卡身份证SDK-HX-FDX3S—东方仙盟筑基期
开发语言·c#·身份证阅读器·酒店管理系统·仙盟创梦ide
say_fall1 小时前
C语言编程实战:每日一题:有效的括号
c语言·开发语言·数据结构·
李贺梖梖1 小时前
day05 数组
java