从 0 到 1 ,手把手教你编写《消息队列》项目(Java实现) —— 核心类持久化存储


一、持久化存储的方式与路径

交换机,队列,绑定关系,这些我们使用数据库来管理,

Message 消息 并不会涉及到复杂的增删改查操作.且 消息 的数量可能会非常多,数据库的访问效率并不高

因此在Message持久化的存储,我们不存储在数据库中,我们直接存储到文件中.

  • 我们来规定一下数据存储的文件及路径

在这里插入图片描述
在这里插入图片描述


二、公共模块

序列化 / 反序列化

上面提到了,消息存储到文件中,那么存储到文件中,就要将 Message 对象 序列化成二进制数据,再存储到文件中,而读取消息时,就要将二进制数据反序列化成 Message 对象.
因此咱们先去写公共模块的 序列化方法与反序列化方法 .
创建一个 common 包,再创建一个 BinaryTool类 来写序列化 / 反序列化方法
在这里插入图片描述

public class BinaryTool {
    // 要用 Java 标准库提供的流对象完成 序列化/反序列化 操作,一定要继承 Serializable 接口,不然运行时,会抛出异常

    // 把一个对象序列化成一个字节数组
    public static byte[] toBytes(Object object) throws IOException {
        // ByteArrayOutputStream 这个流对象是相当于一个中间过渡,解决无法确定数组长度的问题
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            // ObjectOutputStream 这个流对象是 Java 标准库提供的序列化的流对象,将过渡流对象绑定到序列化流对象中,最后直接转换成数组返回
            try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
                // 将对象进行序列化后,存储到绑定的流对象中 即 过渡流对象
                objectOutputStream.writeObject(object);
                // 序列化完成后,存储到了绑定的流对象中,然后再通过绑定的流对象转换成数组返回
            }
            return byteArrayOutputStream.toByteArray();
        }
    }



    // 把一个字节数组反序列化成一个对象
    public static Object fromBytes(byte[] data) throws IOException, ClassNotFoundException {
        Object object = null;
        // ByteArrayInputStream 这个流对象直接绑定 data 数组,相当于直接从 data 数组中取数据
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data)) {
            // ObjectInputStream 这个流对象是 Java 标准库提供的反序列化的流对象,绑定中间流对象,从中间流对象取数据
            try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
                // readObject方法,从中间流对象取数据,中间流对象从 data 数组取数据,=》 将 data 数组 反序列化成对象
                object = objectInputStream.readObject();
            }
        }
        return object;
    }
}


异常规定

对文件进行读写操作,可能会出现意料之外的情况,因此我们自定义一个异常,来针对咱们这个程序中出现意料之外的情况时.
在这里插入图片描述

public class MqException extends Exception{
    public MqException(String message) {
        super(message);
    }
}

三、持久化存储

数据库数据管理

MySQL数据库本身就比较重量,
此处为了使用方便,我们使用更轻量的数据库 SQLite,
在Java中使用SQLite,不需要额外安装,只需要引入依赖即可.(记得去maven找到与你jdk相匹配的版本)

在这里插入图片描述


SQLite,只是一个本地的数据库,这个数据库相当于直接操作本地硬盘文件,
因此需要在配置文件中配置好数据库文件的路径
使用 yaml格式的配置文件
在这里插入图片描述


创建一个 mapper包,通过里面的接口来操作数据库
在这里插入图片描述

/**
 * 使用这个类来操作数据库
 */
@Mapper // 注入到容器中
public interface MetaMapper {
    // 提供三个核心建表方法,create 可以直接使用 update代替
    @Update("create table if not exists exchange (name varchar(50) primary key,type int,durable boolean,autoDelete boolean,arguments varchar(1024))")
    void createExchangeTable();

    @Update("create table if not exists queue(name varchar(50) primary key,durable boolean,exclusive boolean,autoDelete boolean,arguments varchar(1024))")
    void createQueueTable();

    @Update("create table if not exists binding(exchangeName varchar(50),queueName varchar(50),bindingKey varchar(256))")
    void createBindingTable();



    // 针对上述三个表,进行 增加删除查找 操作
    @Insert("insert into exchange values (#{name},#{type},#{durable},#{autoDelete},#{arguments})")
    void insertExchange(Exchange exchange);

    @Delete("delete from exchange where name = #{exchangeName}")
    void deleteExchange(String exchangeName);

    @Select("select * from exchange")
    List<Exchange> selectAllExchanges();



    @Insert("insert into queue values (#{name},#{durable},#{exclusive},#{autoDelete},#{arguments})")
    void insertQueue(MSGQueue queue);

    @Delete("delete from queue where name = #{queueName}")
    void deleteQueue(String queueName);

    @Select("select * from queue")
    List<MSGQueue> selectAllQueues();




    @Insert("insert into binding values (#{exchangeName},#{queueName},#{bindingKey})")
    void insertBinding(Binding binding);

    @Delete("delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName}")
    void deleteBinding(Binding binding);

    @Select("select * from binding")
    List<Binding> selectAllBindings();
}

我们再创建一个 datacenter包,通过里面的类来 整合对硬盘与内存上数据的管理 ,
首先来整合数据库操作,
在这里插入图片描述

注意了!此时我并不想将 DataBaseManager 这个类的控制权交给容器,
因此我就不给这个类加 类注解,所以在这里也没办法用 @Autowired依赖注入得到这个类,
因此我们需要用 依赖查找 得到这个类.故而需要给项目启动类 增添 一个 静态属性:容器上下文 ,
在这里插入图片描述
我直接贴出代码,代码中有详细注释!

/**
 * 通过这个类,来整合数据库操作
 */
public class DataBaseManager {

    private MetaMapper metaMapper;


    // 数据库的初始化方法
    public void init() {
        // 通过启动类中的 上下文对象,进行依赖查找给 metaMapper 赋值
        metaMapper = MessageQueueApplication.context.getBean(MetaMapper.class);

        if (!checkDBExists()) {
            // 如果数据库不存在,则建库建表,构造默认数据

            // 创建 data 目录
            File dataDir = new File("./data");
            dataDir.mkdirs();
            createTable();
            createDefaultData();
            System.out.println("[DataBaseManger]数据库初始化完成");
        } else {
            // 如果数据库存在(有表有数据),不做任何操作
            System.out.println("[DataBaseManger]数据库已存在");
        }
    }

    // 删除数据库文件
    public void deleteDB() {
        File file = new File("./data/meta.db");
        if (file.delete()) {
            System.out.println("[DataBaseManager] 删除数据库文件成功");
        } else {
            System.out.println("[DataBaseManager] 删除数据库文件失败");

        }

        // 删除数据库目录(一定要先删除文件,目录是空的才能删除成功)
        File dataDir = new File("./data");
        if (dataDir.delete()) {
            System.out.println("[DataBaseManager] 删除数据库目录成功");
        } else {
            System.out.println("[DataBaseManager] 删除数据库目录失败");
        }

    }


    // 判断数据库是否存在
    private boolean checkDBExists() {
        File file = new File("./data/meta/.db");
        return file.exists();
    }


    // 建表方法
    // 建库操作并不需要手动执行,(不需要手动创建 meta.db 文件)
    // 首次执行这里的数据库操作时,就会自动创建 meta.db 文件(MyBatis 完成的)
    private void createTable() {
        metaMapper.createBindingTable();
        metaMapper.createExchangeTable();
        metaMapper.createQueueTable();
        System.out.println("[DataBaseManger]创建表完成");
    }

    // 给数据库表中,添加默认数据
    // 此处主要是添加一个默认的交换机
    // RabbiMQ 里有一个这样的设定,带有一个 匿名 的交换机,类型是 DIRECT
    private void createDefaultData() {
        Exchange exchange = new Exchange();
        exchange.setName("");
        exchange.setType(ExchangeType.DIRECT);
        exchange.setDurable(true);
        exchange.setAutoDelete(false);
        metaMapper.insertExchange(exchange);
        System.out.println("[DataBaseManger]插入初始数据完成");
    }

    // 将其他数据库操作,也在这个类中封装成方法
    public void insertExchange(Exchange exchange) {
        metaMapper.insertExchange(exchange);
    }

    public void deleteExchange(String exchangeName) {
        metaMapper.deleteExchange(exchangeName);
    }

    public List<Exchange> selectAllExchanges() {
        return metaMapper.selectAllExchanges();
    }

    public void insertQueue(MSGQueue queue) {
        metaMapper.insertQueue(queue);
    }

    public void deleteQueue(String queueName) {
        metaMapper.deleteQueue(queueName);
    }
    public List<MSGQueue> selectAllQueues() {
        return metaMapper.selectAllQueues();
    }

    public void insertBinding(Binding binding) {
        metaMapper.insertBinding(binding);
    }

    public void deleteBinding(Binding binding) {
        metaMapper.deleteBinding(binding);
    }

    public List<Binding> selectAllBindings() {
        return metaMapper.selectAllBindings();
    }
}

文件数据管理

读写规定

在上面已经规定好了文件的存储路径,每个队列的消息都放在 ./data/队列名 下.

大家想,文件里所有的消息都是二进制数据,因此文件中都是二进制数据,没办法找到消息之间有效的界限.

故而我们规定,写入文件的 Message对象 格式如下
在这里插入图片描述

在每个 Message 对象 的二进制数据前,加一个 大小为 四个字节 的整数 来描述 Message二进制数据的长度.
先读取 四个字节 获得 Message 的 长度,再读取该长度个 字节.
这样就可以使每个 Message 的二进制数据有一个可读界限.

在写入的时候只需写入下面的这两个 属性,只有这三个属性,才是 Message 对象的 核心属性,其他的属性都是为管理内存中的 Message而设置的,不需要写入到文件中.
在这里插入图片描述

新增 /删除规定

对于 消息来说,是需要频繁的新增和删除的,
当生产者 生产新的消息,就需要新增,
当消费者 消费了一个消息,就需要删除这个消息.

对于新增来说,比较简单,可以直接写到文件末尾,

但是对于删除来说,就比较复杂了,因为要删除的消息 不一定就是开头或末尾的消息,
文件类似于一个顺序表的结构,如果删除中间的消息,就需要将后面的消息向前搬运,

因此我们使用 逻辑删除 ,即使用 isValid 这个属性来表示该消息 是否有效.
在这里插入图片描述


内存中 Message 的规定

上面说到了使用 逻辑删除 来让一个消息失效,那么在要如何在文件中找到这个要删除的消息呢?

所以我们设置了 下面这两个属性
在这里插入图片描述
根据这两个属性,去读取对应的字节.
在这里插入图片描述


存储规定

针对逻辑删除,
我们可以队列文件中,再创建两个文件,
queue_data.txt(消息信息文件) 用来存储所有消息的本体,
queue_stat.txt(消息统计文件) 用来存储一行数据,这一行数据只有两个数字,用 "/t"分隔
所有消息的数量,
有效消息的数量.
在这里插入图片描述


代码编写

咱们再创建一个类,来集中管理消息.
在这里插入图片描述

/**
 * 通过这个类,来针对硬盘上的消息管理
 */
public class MessageFileManager {

    public void init() {
        // 暂时不需要做啥额外的初始化工作, 以备后续扩展
    }

    // 定义一个内部类,来表示该队列的统计信息
    // 优先考虑使用 static,静态内部类
    static public class Stat {
        // 此处直接定义成 public,就不再搞 get  set 方法了
        // 对于这样简单的类,就直接使用成员,类似于 C 的结构体了
        public int totalCount; // 总消息数量
        public int validCount; // 有效消息数量
    }



    // 约定消息文件所在的目录和文件名
    // 这个方法用来获取指定队列对应的消息文件所在路径
    private String getQueueDir(String queueName) {
        return "./data/" + queueName;
    }

    // 这个方法用来获取该队列的消息数据文件路径
    // 注意,二进制文件,使用 txt 作为后缀,不太合适,txt 一般表示文本(.bin或.dat 较为合适)
    private String getQueueDataPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data.txt";
    }

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

    // 读取该队列的消息统计文件
    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;
    }

    // 向该队列的消息统计文件中写入数据
    private void writeStat(String queueName,Stat stat) {
        // 使用 PrintWrite 来写文件
        // OutputStream 打开文件,默认情况下,会直接把原文件清空,此时相当于新的数据覆盖了旧的
        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();
        }
    }


    // 创建队列对应的文件和目录
    public void createQueueFiles(String queueName) throws IOException {
        // 1.先创建队列对应的消息目录
        File baseDir = new File(getQueueDir(queueName));
        if (!baseDir.exists()) {
            // 不存在则创建
            boolean ok = baseDir.mkdirs();
            if (!ok) {
                throw new IOException("创建队列对应的消息目录失败!baseDir=" + baseDir.getAbsolutePath());
            }
        }

        // 2.创建队列对应的 data 文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()) {
            // 不存在则创建
            boolean ok = queueDataFile.createNewFile();
            if (!ok) {
                throw new IOException("创建该队列的消息数据文件失败!queueDataFile=" + queueDataFile.getAbsolutePath());
            }
        }

        // 3.创建队列对应的 stat 文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()) {
            boolean ok = queueStatFile.createNewFile();
            if (!ok) {
                throw new IOException("创建该队列的消息统计文件失败!queueStatFile="+queueStatFile.getAbsolutePath());
            }
        }
        // 4.给消息统计文件设置初始值
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName,stat);
    }


    // 删除队列的目录和文件
    // 队列可以被删除,当队列删除后,对应的消息文件等,自然也要随之删除
    public void destroyQueueFiles(String queueName) throws IOException {
        // 先删除文件再删除目录
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = queueDataFile.delete();

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

        File baseDir = new File(getQueueDir(queueName));
        boolean ok3 = baseDir.delete();

        if (!ok1 || !ok2 || !ok3) {
            // 有任意一个删除失败,都算整体失败,
            throw new IOException("删除队列目录和文件失败!baseDir="+getQueueDir(queueName));
        }
    }


    // 检查文件的目录和文件是否存在
    // 后续有生产者给 broker server 生产消息了,这个消息就可能需要记录到文件上(取决是否要持久化)
    // 在记录之前就要先检查文件是否存在
    public boolean checkFilesExits(String queueName) {
        // 判断队列和消息文件是否都存在!
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()) {
            return false;
        }
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()) {
            return false;
        }
        return true;
    }

    // 将新消息写入到对应的队列的数据文件并更新统计文件的方法
    // queue 表示要对应的队列,message 表示要写入的消息
    public void sendMessage(MSGQueue queue, Message message) throws MqException, IOException {
        // 1.检查一下当前要写入的队列对应的文件是否存在
        if (!checkFilesExits(queue.getName())) {
            throw new MqException("[MessageFileManager] 队列对应的文件不存在!queueName=" + queue.getName());
        }
        // 2.把要写入的 message 序列化
        byte[] messageBinary = BinaryTool.toBytes(message);

        // 写入消息时,可能会与其他线程对该队列的操作产生线程安全问题,因此针对这个 队列加锁
        // (如其他线程此时也向该队列写入消息,那么此时offsetBeg与offsetEnd就可能是不准确的,那么后续操作就也可能会出现BUG)
        synchronized (queue) {
            // 3.先获取当前队列的文件长度,计算 message 的 offsetBeg 和 offsetEnd
            // 把新的Message数据写入到队列数据文件的末尾,
            // 此时 offsetBeg = 文件长度 + 4,offsetEnd = 文件长度 + 4 + message长度
            File queueDataFile = new File(getQueueDataPath(queue.getName()));
            message.setOffsetBeg(queueDataFile.length() + 4);
            message.setOffsetEnd(queueDataFile.length() + 4 + messageBinary.length);
            // 4.写入消息到数据文件,注意,是写入到文件末尾                       第二个属性,一定要写true,不然会直接覆盖掉之前的数据
            try (OutputStream outputStream = new FileOutputStream(queueDataFile,true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    // 先写入当前消息的长度,占据 4 个字节,writeInt()方法,写入的整形占四个字节
                    dataOutputStream.writeInt(messageBinary.length);
                    // 将数据本体写入到队列数据文件中
                    outputStream.write(messageBinary);
                }

            }
            // 5.更新消息统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(),stat);
        }
    }

    // 删除消息的方法
    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException {
        // 此处的删除指的是逻辑上的删除,即将 isValid 由 0x1 改为0x0
        // 1.先把文件中对应的 [offsetBeg,offsetEnd) 的 消息读出来
        // 2.然后修改 isValid 为 0x0
        // 3.再将消息写回对应的 [offsetBeg,offsetEnd) 位置

        // 删除消息时,可能会与其他线程对该队列的操作产生线程安全问题,因此针对这个 队列加锁
        // (如在删除消息时,进行了gc操作,那么就很有可能导致,这个被逻辑删除的消息,覆盖其他消息,且也有可能会覆盖正确的统计文件)
        synchronized (queue) {
            // RandomAccessFile 这个流,可以自由移动光标(读取位置),                                 第二个参数 "rw"代表读写
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")) {
                // 1.先从文件中读取 message
                byte[] bufferSrc = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                // 移动光标
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(bufferSrc);
                // 2.把读取出来的数据 反序列化 成 Message
                Message diskMessage = (Message) BinaryTool.fromBytes(bufferSrc);
                // 3.把 isValid 设置成无效
                diskMessage.setIsValid((byte) 0x0);
                // 4.将修改完成后的 message 序列化成二进制数据
                byte[] bufferDest = BinaryTool.toBytes(diskMessage);
                // 5.重新移动光标
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bufferDest);
            }

            // 更新 统计文件 stat,逻辑删除了一个消息,有效消息数量 validCount 需要 -1
            Stat stat = readStat(queue.getName());
            if (stat.validCount > 0) {
                stat.validCount -= 1;
            }
            writeStat(queue.getName(), stat);
        }
    }

    // 使用这个方法, 从文件中, 读取出所有的消息内容, 加载到内存中(具体来说是放到一个链表里)
    // 这个方法, 准备在程序启动/重启时, 进行调用.
    // 这里使用一个 LinkedList, 主要目的是为了后续进行头删操作.
    // 这个方法的参数, 只是一个 queueName 而不是 MSGQueue 对象. 因为这个方法不需要加锁, 只使用 queueName 就够了.
    // 由于该方法是在程序启动时调用, 此时服务器还不能处理请求呢~~ 不涉及多线程操作文件.
    public LinkedList<Message> loadAllMessageFromQueue (String queueName) throws IOException, ClassNotFoundException, MqException {
        LinkedList<Message> messages = new LinkedList<>();
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))){
            // DataInputStream 提供了 readInt()方法,会读取四个字节的然后转化为整数,普通的 read()方法只会读取一个字节就转化为整数了
            try (DataInputStream dataInputStream = new DataInputStream(inputStream)){
                // 记录光标位置
                // 由于当下使用的 DataInputStream 并不方便直接获取到文件光标位置因此就需要手动记录下文件光标.
                long currentOffset = 0;
                while (true) {
                    // 1.读取每个消息前用来记录消息长度的 4 个字节大小的 int
                    // readInt()方法,会读取 4 个字节的大小 的int
                    int messageSize = dataInputStream.readInt();
                    currentOffset += 4;
                    // 2.按照 messageSize 读取对应的字节数
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    if (messageSize != actualSize) {
                        // 如果不匹配, 说明文件有问题, 格式错乱了!!
                        throw new MqException("[MessageFileManager] 文件格式错误! queueName=" + queueName);
                    }
                    // 3.将读取的二进制数据 反序列化成 对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    // 4.判断该数据是否有效
                    if (message.getIsValid() == 0x1) {
                        // 设置 message 的 offsetBeg 与 offsetEnd
                        message.setOffsetBeg(currentOffset);
                        currentOffset += messageSize;
                        message.setOffsetEnd(currentOffset);
                        // 5.填加到链表中
                        messages.add(message);
                    } else {
                        // 数据无效也要更新光标位置
                        currentOffset += messageSize;
                    }

                }
            } catch (EOFException e) {
                // 这个 catch 并非真是处理 "异常", 而是处理 "正常" 的业务逻辑. 文件读到末尾, 会被 readInt 抛出该异常.
                // 这个 catch 语句中也不需要做啥特殊的事情
                System.out.println("[MessageFileManager] 恢复 Message 数据完成!");
            }
        }
        return messages;
    }


    // 检查当前是否需要针对该队列的消息数据文件进行 GC(无效消息回收)
    public boolean checkGC(String queueName) {
        Stat stat = readStat(queueName);
        if (stat.totalCount > 2000 && (double)(stat.validCount / stat.totalCount) < 0.5) {
            return true;
        }
        return false;
    }

    // 获取 GC 复制算法的 新文件路径
    private String getQueueDataNewPath (String queueName) {
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }


    // 通过这个方法, 真正执行消息数据文件的垃圾回收操作.
    // 使用复制算法来完成.
    // 创建一个新的文件, 名字就是 queue_data_new.txt
    // 把之前消息数据文件中的有效消息都读出来, 写到新的文件中.
    // 删除旧的文件, 再把新的文件改名回 queue_data.txt
    // 同时要记得更新消息统计文件.
    public void gc(MSGQueue queue) throws IOException, MqException, ClassNotFoundException {
        // 进行 gc 的时候,是针对消息数据文件进行大洗牌,在这个过程中,其他线程不能针对该队列的消息文件做任何修改
        synchronized (queue) {
            // 由于 gc 操作可能比较耗时, 此处统计一下执行消耗的时间.
            long gcBeg = System.currentTimeMillis();

            // 1.创建新数据文件
            File queueDataNewFile = new File(getQueueDataNewPath(queue.getName()));
            boolean ok = queueDataNewFile.exists();
            if (ok) {
                throw new MqException("[MessageFileManager] gc 时发现该队列的 queue_data_new 已经存在! queueName="+ queue.getName());
            }
            ok = queueDataNewFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManager] gc 时创建新文件失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath());
            }
            // 2.获取原数据文件中的有效消息,并写入到新数据文件中
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());
            try (OutputStream outputStream = new FileOutputStream(queueDataNewFile)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        byte[] buffer = BinaryTool.toBytes(message);
                        // 先写入记录 消息长度 的占 4 个字节的 int
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }
            // 3.删除原数据文件
            File queueDataOldFile = new File(getQueueDataPath(queue.getName()));
            ok = queueDataOldFile.delete();
            if (!ok) {
                throw new MqException("[MessageFileManager] 删除旧的数据文件失败! queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
            }
            // 4.将新数据文件重命名
            ok = queueDataNewFile.renameTo(queueDataOldFile);
            if (!ok) {
                throw new MqException("[MessageFileManager] 文件重命名失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath()
                        + ", queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
            }
            // 5.更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queue.getName(),stat);
            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc 执行完毕! queueName=" + queue.getName() + ", time="
                    + (gcEnd - gcBeg) + "ms");
        }
    }
}

硬盘数据管理

咱们再创建一个类, 集中管理 数据库数据与文件数据 .
在这里插入图片描述

/**
 * 使用这个类来管理所有硬盘上的数据
 * 1. 数据库: 交换机,绑定,队列
 * 2. 数据文件: 消息
 * 上层逻辑如果需要操作硬盘,统一都通过这个类来使用,(上层代码不关心当前数据是存储在数据库还是文件中的)
 */
public class DiskDataCenter {
    // 这个实例用来管理数据库
    private DataBaseManager dataBaseManager = new DataBaseManager();
    // 这个实践用来管理数据文件中的数据
    private MessageFileManager messageFileManager = new MessageFileManager();

    public void init() {
        // 针对上述两个实例进行初始化
        dataBaseManager.init();
         // 当前 messageFileManager.init() 方法,是空的,只是先放在这里,方便后续扩展
        messageFileManager.init();
    }

    // 封装交换机操作
    public void insertExchange(Exchange exchange) {
        dataBaseManager.insertExchange(exchange);
    }

    public void deleteExchange(String exchangeName) {
        dataBaseManager.deleteExchange(exchangeName);
    }

    public List<Exchange> selectAllExchanges() {
        return dataBaseManager.selectAllExchanges();
    }

    // 封装队列操作
    public void insertQueue(MSGQueue queue) throws IOException {
        dataBaseManager.insertQueue(queue);
        // 创建队列的同时, 不仅仅是把队列对象写到数据库中, 还需要创建出对应的目录和文件
        messageFileManager.createQueueFiles(queue.getName());
    }

    public void deleteQueue(String queueName) throws IOException {
        dataBaseManager.deleteQueue(queueName);
        // 删除队列的同时, 不仅仅是把队列从数据库中删除, 还需要删除对应的目录和文件
        messageFileManager.destroyQueueFiles(queueName);
    }

    public List<MSGQueue> selectAllQueues() {
        return dataBaseManager.selectAllQueues();
    }

    // 封装绑定操作
    public void insertBinding(Binding binding) {
        dataBaseManager.insertBinding(binding);
    }

    public void deleteBinding(Binding binding) {
        dataBaseManager.deleteBinding(binding);
    }

    public List<Binding> selectAllBindings() {
        return dataBaseManager.selectAllBindings();
    }

    // 封装消息方法
    public void sendMessage(MSGQueue queue, Message message) throws IOException, MqException {
        messageFileManager.sendMessage(queue,message);
    }

    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException, MqException {
        messageFileManager.deleteMessage(queue,message);
        if (messageFileManager.checkGC(queue.getName())) {
            messageFileManager.gc(queue);
        }
    }

    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        return messageFileManager.loadAllMessageFromQueue(queueName);
    }
}


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值