仿RabbitMQ实现消息队列

前言:本项目是仿照RabbitMQ并基于SpringBoot + Mybatis + SQLite3实现的消息队列,该项目实现了MQ的核心功能:生产者、消费者、中间人、发布、订阅等。

源码链接:仿Rabbit MQ实现消息队列

目录

前言:本项目是仿照RabbitMQ并基于SpringBoot + Mybatis + SQLite3实现的消息队列,该项目实现了MQ的核心功能:生产者、消费者、中间人、发布、订阅等。

一、核心概念

二、模块划分 

三、创建核心实体类 

3.1 创建交换机(Exchange)

3.2 创建队列实体类(MSGQueue)

 3.3 创建绑定实体类(Binding)

 3.4 创建消息实体类(Message)

四、数据库操作

 五、封装对数据库的操作

 六、消息的存储设计

6.1 设计思路及设定

 6.2 设定存储消息的格式

6.3 实现消息序列化工具 

七、实现文件管理消息

7.1 读取消息统计文件

 7.2 写消息统计文件

7.3 创建队列对应的文件和目录

7.4 删除队列对应的文件和目录

 7.5 判断队列的目录和消息文件是否存在

7.6 新的消息写入到文件中 

 7.7 删除队列对应的消息数据文件中的消息

7.8 从文件中读取消息到内存中

 7.9 判断垃圾回收时机

7.10 获取新消息数据文件的路径

 7.11 消息的垃圾回收

八、统一硬盘处理

九、内存管理

 9.1 交换机相关操作的API

 9.2 队列相关操作的API

9.3 绑定相关操作的API

 9.4 消息相关操作的API

 十、虚拟机 VirtualHost

10.1 创建交换机

 10.2 删除交换机

 10.3 创建队列

10.4 删除队列

 10.5 创建绑定

10.6 删除绑定

 10.7 发送消息

10.8 订阅消息

10.9 消息确认

十一、网络通信协议设计

11.1 设计应用层协议 

 11.2 定义Request/Response

 11.3 定义参数父类

11.4 定义返回值父类

 11.5 定义其他参数类

 十二、实现 BrokerServer 类

 12.1 启动/停止服务器

 12.2 实现处理连接

 12.3 实现 readRequest / writeResponse

 12.4 实现处理请求

12.5 实现清理过期的会话

 十三、实现客户端

13.1 创建 ConnectionFactory

 13.2 Connection 和 Channel 定义

十四、样例演示


一、核心概念

关于消息队列,有几个重要的核心概念:

  • 生产者(Producer) :负责将应用程序产生的数据转换成消息,并将这些消息推送到消息队列服务器上,以便消费者(Consumer)可以接收并处理这些消息。
  • 消费者(Consumer):它的主要职责是监听特定的队列或主题,并对到达的消息执行必要的业务逻辑。
  • 中间人(Broker):作为生产者(Producer)和消费者(Consumer)之间的中介,负责管理和协调消息的传递过程。
  • 发布(Publish):生产者向中间人投递消息的过程。
  • 订阅(Subscribe):哪些消费者要从这个中间人获取数据,这个注册的过程称为订阅。

在中间人(Broker)模块,又有以下几个概念:

  • 虚拟机 (VirtualHost):类似于 MySQL 的 "database", 是⼀个逻辑上的集合,⼀个 BrokerServer 上可以存在多个 VirtualHost。
  • 交换机 (Exchange): ⽣产者把消息先发送到 Broker 的 Exchange 上,再根据不同的规则, 把消息转发给不同的 Queue。
  • 队列 (Queue): 真正⽤来存储消息的部分,每个消费者决定⾃⼰从哪个 Queue 上读取消息。
  • 绑定 (Binding): Exchange 和 Queue 之间的关联关系,Exchange 和 Queue 可以理解成 "多对多" 关系,使⽤⼀个关联表就可以把这两个概念联系起来。

二、模块划分 

明确需要做的工作:

  • 实现生产者、消费者、Broker Server这三个部分。

  • 针对生产者、消费者,主要实现的是客户端和服务器的网络通信部分。

  • 给客户端提供一组 API,让客户端的业务代码来调用,通过网络通信的方式远程调用Broker Server上的方法。

  • 实现Broker Server 内部的一些基本概念和API(虚拟主机、交换机、队列、绑定、消息)。

  • 持久化(考虑到 SQLite 相比 MySQL 来说比较轻量,因此存储交换机、队列等这些实体用 SQLite,消息的存储使用文件进行管理)。

针对于上述所需要实现的模块,进行划分:

 

三、创建核心实体类 

3.1 创建交换机(Exchange)

name type durable autoDelete arguments
交换机身份标识 交换机类型 是否持久化 是否自动删除

额外参数选项

@Data
public class Exchange {
    //交换机的身份标识(唯一)
    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<>();
}

此处省略 arguments 存储数据库时的Json转换,只需要使用 ObjectMapper即可实现。关于交换机的类型,此次主要实现了以下三种:DIRECT、FANOUT、TOPIC。并使用枚举类定义:

public enum ExchangeType {
    DIRECT(0),
    FANOUT(1),
    TOPIC(2);

    private final int type;

    private ExchangeType(int type){
        this.type = type;
    }

    public int getType() {
        return type;
    }
}

3.2 创建队列实体类(MSGQueue)

name durable exclusive autoDelete arguments consumerEnvList consumerSeq
队列标识 是否持久化 是否独占 是否自动删除 额外参数选项 当前订阅的消费者列表 记录当前取到第几个消费者
@Data
public class MSGQueue {
    //表示队列的身份标识
    private String name;
    //表示队列是否持久化 true:需要持久化 false:不需要持久化
    private boolean durable = false;
    //表示是否独占,true:独占 false:都可以使用
    private boolean exclusive = false;
    //表示无客户端使用是,是否自动删除
    private boolean autoDelete = false;
    //表示扩展参数
    private Map<String,Object> arguments = new HashMap<>();
    //当前队列都有哪些消费者订阅了.
    private List<ConsumerEnv> consumerEnvList = new ArrayList<>();
    //记录当前取到的第几个消费者,方便实现轮询策略
    private AtomicInteger consumerSeq = new AtomicInteger(0);
}

 3.3 创建绑定实体类(Binding)

exchangeName queueName bindingKey
交换机名字 队列名字 绑定(和 routingKey匹配)
@Data
public class Binding {
    // 交换机名字
    private String exchangeName; 
    //队列名字
    private String queueName;
    // 只在交换机类型为 TOPIC 时才有效. ⽤于和消息中的 routingKey 进⾏匹配
    private String bindingKey;
}

 3.4 创建消息实体类(Message)

消息存储为二进制形式,因此对消息的存储需要进行序列化处理,所以 Message 类要实现Serializable 接口。

basicProperties  body offsetBeg offsetEnd isValid
消息的属性 消息的正文 消息开头在文件中的偏移量 消息末尾在文件中的偏移量 是否有效
@Data
public class Message implements Serializable {
    private BasicProperties basicProperties = new BasicProperties();
    private byte[] body;

    //辅助属性,后续消息要存储在文件中
    //一个文件存储很多消息 [offsetBeg, offsetEnd)
    private transient long offsetBeg = 0;//消息数据的开头距离文件开头的位置偏移(字节)
    private transient long offsetEnd = 0;//消息数据的结尾距离文件开头的位置偏移(字节)
    private byte isValid = 0x1;//表示该消息在文件中是否是有效的消息,逻辑删除, 0x1:有效 0x0:无效

    //创建一个工厂方法,让工厂方法去封装一下创建 Message 对象的过程
    //这个方法创建的 Message 会自动生成一个唯一的 MessageId
    public static Message createMessageWithId(String routingKey,BasicProperties basicProperties,byte[] body){
        Message message = new Message();
        if(basicProperties != null){
            message.setBasicProperties(basicProperties);
        }
        //此处生成的 MessageId 以 "M-" 为前缀, 方便区分
        message.setMessageId("M-" + UUID.randomUUID());
        message.setRoutingKey(routingKey);
        message.body = body;
        return message;
    }
    public String getMessageId(){
        return basicProperties.getMessageId();
    }
    public void  setMessageId(String messageId){
        basicProperties.setMessageId(messageId);
    }
    public String getRoutingKey(){
        return basicProperties.getRoutingKey();
    }
    public void setRoutingKey(String routingKey){
        basicProperties.setRoutingKey(routingKey);
    }
    public int getDeliverMode(){
        return basicProperties.getDeliverMode();
    }
    public void setDeliverMode(int deliverMode){
        basicProperties.setDeliverMode(deliverMode);
    }
}

 对于消息的属性,使用一个实体类去表示:

messageId routingKey deliverMode
消息的唯一身份标识 和(bindingKey匹配) 是否持久化
@Data
public class BasicProperties implements Serializable {
    //消息的唯一身份标识,使用 UUID 作为 messageId
    private String messageId;
    /**
     * 如果当前的交换机类型是 DIRECT, 此时 routingKey 就表示要转发的队列名
     * 如果当前的交换机类型是 FANOUT, 此时 routingKey 无意义(不使用)
     * 如果当前的交换机类型是 TOPIC,  此时 routingKey 就表示和 bindingKey 进行匹配
     */
    private String routingKey;
    //表示消息是否持久化, 1: 不持久化; 2: 持久化
    private int deliverMode = 1;
}

四、数据库操作

对于 Exchange, MSGQueue, Binding, 需要使⽤数据库进⾏持久化保存,这里使用 SQLite 进行存储,直接去 Maven 中央仓库复制依赖到项目的POM文件,再配置数据库文件即可。

SQLite 只是把数据单纯的存储到⼀个⽂件中,因此在这里设定存储到 “./data/meta.db”文件。

 实现创建表以及数据库操作(这里不再展示具体的SQL语句)

@Mapper
public interface MetaMapper {
    //三个核心建表方法
    void createExchangeTable();
    void createQueueTable();
    void createBindingTable();

    //针对上述三个基本概念进行插入和删除
    void insertExchange(Exchange exchange);
    List<Exchange> selectAllExchanges();
    void deleteExchange(@Param("exchangeName") String exchangeName);

    void insertQueue(MSGQueue queue);
    List<MSGQueue> selectAllQueues();
    void deleteQueue(@Param("queueName") String queueName);


    void insertBinding(Binding binding);
    List<Binding> selectAllBindings();
    void deleteBinding(Binding binding);

}

 五、封装对数据库的操作

创建 DataBaseManager 类,通过这个类来封装针对数据库的操作。
public class DataBaseManager {
    //数据库初始化
    public void init(){...}
    //删除数据库
    public void deleteDB(){...}
    //判断数据库是否存在
    private boolean checkDBExists(){...}
    //建表操作
    private void createTable(){...}
    //创建默认数据,RabbitMQ 里默认也带有一个 匿名 的交换机,类型是 DIRECT
    private void createDefaultData(){...}

    //交换机的数据库操作:增删查
    public void insertExchange(Exchange exchange){...}
    public void deleteExchange(String exchangeName){...}
    public List<Exchange> selectAllExchanges(){...}

    //队列的数据库操作:增删查
    public void insertQueue(MSGQueue queue){...}
    public void deleteQueue(String queueName){...}
    public List<MSGQueue> selectAllQueues(){...}

    //Binding的数据库操作:增删查
    public void insertBinding(Binding binding){...}
    public void deleteBinding(Binding binding){...}
    public List<Binding> selectAllBindings(){...}
}

 六、消息的存储设计

6.1 设计思路及设定

设计思路:消息需要在硬盘上存储,考虑到对于消息的操作并不需要复杂的增删改查,而⽂件的操作效率比数据库会高很多,因此这里设定,用文件来管理消息。

同时,因为队列用来存储消息,因此这里约定:

  1. 给每个队列分配⼀个目录,目录的名字为 data + 队列名,形如 :./data/queueName
  2. 该目录中包含两个固定名字的⽂件:

 queue_data.txt 消息数据⽂件, 用来保存消息内容。

 queue_stat.txt 消息统计⽂件, 用来保存消息统计信息。(消息总个数/t有效消息数),这样设计主要时考虑到后续进行垃圾回收,方便判断进行GC的时机。

 6.2 设定存储消息的格式

消息数据文件以二进制的形式存储在 queue_data.txt 文件中 ,为了方便进行消息的读取,这里进行这样的设定:

          每个消息分成两个部分:前四个字节, 表示 Message 对象的长度(字节数),后面若干字节,表示 Message 内容,消息和消息之间首尾相连。同时每个 Message 基于 Java 标准库进行序列化。 Message 对象中的 offsetBeg 和 offsetEnd 正是⽤来描述每个消息体所在的位置。

6.3 实现消息序列化工具 

对于实现消息序列化,首先 Message 实体类要实现  Serializable 接口,接下来需要借助ByteArrayOutputStream 和 ObjectOutputStream 实现消息的序列化和反序列化:

public class BinaryTool {
    /**
     * 把对象序列化成一个字节数组
     * @param object
     * @return
     */
    public static byte[] toBytes(Object object) throws IOException {
        // 这个流对象相当于一个变长的字节数组,
        // 可以把 object 对象序列化的数据逐步写入导 byteArrayOutputStream 中,
        // 再统一转成 byte[]
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()){
            try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)){
                // 此处的 writeObject 就会把 object 进行序列化,生成的字节数据就会写入到 objectOutputStream
                // objectOutputStream 又关联到了 byteArrayOutputStream,最终结果写入到 byteArrayOutputStream 里
                objectOutputStream.writeObject(object);
            }
            return byteArrayOutputStream.toByteArray();
        }
    }

    /**
     * 把一个字节数组反序列化成一个对象
     * @param data
     * @return
     */
    public static Object fromBytes(byte[] data) throws IOException, ClassNotFoundException {
        Object object = null;
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data)){
            try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)){
                object = objectInputStream.readObject();
            }
        }
        return object;
    }
}

七、实现文件管理消息

 创建 MessageFileManager 类,这个类主要去实现消息统计文件的读写、消息数据文件的读写、创建存储消息的文件、消息的垃圾回收等等。

下面这段代码是 MessageFileManager 的基础代码,实现文件的读写和垃圾回收都需要调用下面的方法:

public class MessageFileManager {
    //定义一个内部类,来表示该队列的统计信息
    static public class Stat{
        public int totalCount; // 总消息数量
        public int validCount; // 有效消息数量
    }

    public void init(){

    }
    //约定消息文件所在的目录和文件名
    // 所在路径及文件名: ./data/队列名/queue_data.txt(消息数据文件)
    //                ./data/队列名/queue_stat.txt(消息统计文件)
    /**
     * 这个方法用来获取指定队列对应的消息文件所在路径
     * @param queueName
     * @return
     */
    private String getQueueDir(String queueName){
        return "./data/" + queueName;
    }

    /**
     * 这个方法用来获取该队列的消息数据文件路径
     * @param queueName
     * @return
     */
    private String getQueueDataPath(String queueName){
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    /**
     * 这个方法用来获取指定队列的消息统计文件的路径
     * @param queueName
     * @return
     */
    private String getQueueStatPath(String queueName){
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

7.1 读取消息统计文件

private Stat readStat(String queueName){
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))){
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

 7.2 写消息统计文件

    private void writeStat(String queueName, Stat stat){
        // 使用 PrintWriter 写文件
        try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))){
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

7.3 创建队列对应的文件和目录

public void createQueueFiles(String queueName) throws IOException {
        //1.创建队列对应的消息目录
        File baseDir = new File(getQueueDir(queueName));
        if(!baseDir.exists()){
           boolean success =  baseDir.mkdirs();
           if(!success){
               throw new IOException("创建目录失败! baseDir :" + baseDir.getAbsolutePath());
           }
        }
        //2.创建消息数据文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if(!queueDataFile.exists()){
            boolean success = queueDataFile.createNewFile();
            if(!success){
                throw new IOException("创建消息数据文件失败! queueDataFile: " + queueDataFile.getAbsolutePath());
            }
        }
        //3.创建消息统计文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        if(!queueStatFile.exists()){
            boolean success = queueStatFile.createNewFile();
            if(!success){
                throw new IOException("创建消息统计文件失败! queueStatFile: " + queueStatFile.getAbsolutePath());
            }
        }
        //4.给消息统计文件设置初始值
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName,stat);
    }

7.4 删除队列对应的文件和目录

public void destroyQueueFiles(String queueName) throws IOException {
        //先删除文件,再删除目录
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean success1 = queueDataFile.delete();

        File queueStatFile = new File(getQueueStatPath(queueName));
        boolean success2 = queueStatFile.delete();

        File baseDir = new File(getQueueDir(queueName));
        boolean success3 = baseDir.delete();
        if(!success1 || !success2 || !success3){
            //删除失败
            throw new IOException("删除队列目录和消息文件失败! baseDir: " + baseDir.getAbsolutePath());
        }
    }

 7.5 判断队列的目录和消息文件是否存在

    public boolean checkFileExists(String queueName){
        //判断队列的 消息数据文件 和 消息统计文件 是否都存在
        File queueDataFile = new File(getQueueDataPath(queueName));
        File queueStatFile = new File(getQueueStatPath(queueName));
        if(queueDataFile.exists() && queueStatFile.exists()){
            return true;
        }
        return false;
    }

7.6 新的消息写入到文件中 

步骤:

  • 检查要写入的队列对应的文件是否存在
  • 对 Message 对象进行序列化
  • 获取当前消息数据文件的长度,由此来设置当前要写入的消息的 offsetBeg 和 offsetEnd。

offsetBeg = 消息数据文件长度 + 4

offsetEnd = 消息数据文件长度 + 4 + 该消息序列化后的 byte 数组的长度

  • 写入消息数据文件,更新消息统计文件 
    public void sen
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

..清风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值