MessageFileManage 类详解

这个类的作用是对硬盘中的文件数据进行管理

使用静态的内部类,来定义文件中总的数据(totalCount)和有效数据(validCount)定义在 Stat这个类中

java 复制代码
//定义一个内部类,来表示该队列的统计信息
    //创建内部类,有限考虑使用 static(静态的) 这样会使的该内部类与外部类解耦合
    static public class Stat {
        //此处直接定义成 public
        public int totalCount;  //总的消息个数
        public int validCount;  //有效的消息个数
    }

1.方法 getQueueDir

队列名作为唯一索引,通过队列名来作为文件路径的标识

java 复制代码
//予定消息文件所在目录和文件名
    //这个方法,用来获取到指定队列对应的消息文件所在目录
    private String getQueueDir(String queueName) {
        return "./data/" + queueName;
    }

2.方法:getQueueDataPath

"./data/queueTest1/queue_data.txt" => 把队列名当做二级目录,用于区分不同队列中的数据文件,这个文件中存放的数队列数据

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

3.方法:getQueueStatPath

"./data/queueTest1/queue_stat.txt" => 这个里面存放的是该队列中的总数据条数和有效数据条数,就是说在每个队列下面都会有这样的两个目录

data

queueTest1

queue_data.txt

queue_stat.txt

queueTest2

queue_data.txt

queue_stat.txt

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

4.方法:readStat

该方法的目的是"持久化数据的还原",在消息队列中为了防止数据在重启后丢失,统计信息通常会放在磁盘文件中,这个方法的作用就是找到对应队列的统计文件,读取其中的两个整数数值,并分装进一个 Stat对象返回

通过队列名定位到统计文件,读取关键的两个数据

java 复制代码
//这个方法用来读取该队列的消息统计信息
    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;
    }

5.方法:writeStat

核心任务是将内存中的 stat 对象 持久化到硬盘文件中去

FileOutputStream 是字节流,负责将二进制数据写入文件

PrintWriter:是字符流,它的内部会自动的处理编码

flush():由于PrintWriter内部维护了一个缓冲区,数据可能还在内存中,调用 flush() 能够确保数据能够落到硬盘中去,防止数据丢失

java 复制代码
//这个方法是用来写入统计信息的
    private void writeStat(String queueName, Stat stat) {
        //使用 PrintWrite 来写文件
        //OutputStream 打开文件,默认的情况下是,会直接把文件清空,此时相当于新的数据覆盖率旧的数据
        //这里写入的操作,我的理解应该是这样的
        //1.先根据 getQueueStatPath 获取文件路径
        //2.把获取到的文件路径作为参数,来创建 FileOutputStream.并且打开
        //3.把打开的文件 outputStream 作为参数,来创建 PrintWriter
        //4.通过 printWriter.writer 来进行写入操作
        //5.写入完成之后通过 flush() 将硬盘中的数据刷新到硬盘中去
        try(OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

6.方法:createQueueFile

根据队列名来创建一个二级目录(这里假定/data/为一级目录)

通过上述创建的方法来分别创建二级目录(queueTest1 ...)和三级目录(queue_data.txt .../ queue_stat.txt ...),在对消息文件进行初始化,初始化完成之后再重新写回文件

java 复制代码
//创建队列对应的文件和目录
    public void createQueueFile(String queueName) throws IOException {
        //1.先创建队列对应的消息目录
        File baseDir = new File(getQueueDir(queueName));
        if(!baseDir.exists()) {
            boolean ok = baseDir.mkdirs();
            if(!ok) {
                throw new RuntimeException("创建队列目录失败: " + baseDir.getAbsolutePath());
            }
        }

        //2.创建队列数据文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if(!queueDataFile.exists()) {
            boolean ok = queueDataFile.createNewFile();
            if(!ok) {
                throw new RuntimeException("创建队列数据文件失败: " + queueDataFile.getAbsolutePath());
            }
        }

        //3.创建消息统计文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        if(!queueStatFile.exists()) {
            boolean ok = queueStatFile.createNewFile();
            if(!ok) {
                throw new RuntimeException("创建消息统计文件失败: " + queueStatFile.getAbsolutePath());
            }
        }

        //4.给消息统计文件设置初始值. 0\t0
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName, stat);
    }

7.方法:destroyQueueFile

删除队列中的文件,要先把文件中内容先给删除干净,之后再把空的文件目录删除干净,在对文件目录是否成功删除做一下判断

java 复制代码
//删除队列的目录和文件
    //队列也是可以被删除的,当队列删除之后,对应的消息文件啥的,自然也就消失了
    public void destroyQueueFile(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.getAbsolutePath());
        }
    }

8.方法:checkFileExists

检查目标文件是否存在

java 复制代码
//检查队列的目录和文件是否存在
    //比如后续就有生产者给 BrokerServer 生产消息了,这个消息就可能需要记录到文件中(取决于消息是否需要持久化
    public boolean checkFileExists(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;
    }

9.方法:sendMessage

把新消息写入队列的文件中

写入是以二进制的方式写入,这里要先对Message对象进行序列化操作

先获取一下当前文件中的数据长度,根据原有文件的长度和待写入文件的长度来计算 offsetBeg和offsetEnd的数值

写入方式采用追加写的方式,按照前面规定的文件写入格式的要求,前四个字节写入的数待写入文件的主体内容字节数,写入四个字节之后再写入消息的本体

java 复制代码
//这个方法用来把一个新消息,放入到队列对应的文件中
    //queue 表示要把消息写入队列,message 则是要写的消息
    public void sendMessage(MSGQueue queue, Message message) throws MqException, IOException {
        //1.检查一下当前要写入的队列文件是否存在
        if(!checkFileExists(queue.getName())) {
            throw new MqException("[MessageFileManager] 队列对应的文件不存在! queueName: " + queue.getName());
        }
        //2.把Message对象,进行序列化,转成二进制的字节数组
        byte[] messageBinary = BinaryTool.toBytes(message);
        synchronized (queue) {
            //3.先获取到当前队列文件中数据的长度,用这个来计算 Message 对象的 offsetBeg 和 offsetEnd
            //把新的 Message 数据,写入到队列数据文件的末尾,此时 Message 对象的 offsetBeg,就是当前文件的长度 + 4
            //而 offsetEnd 的长度就是文件的长度 + 4 + Message 的长度
            File queueDataFile = new File(getQueueDataPath(queue.getName()));
            //通过这个方法 queueDataFile.length() 就能获取到文件的长度,单位字节
            message.setOffsetBeg(queueDataFile.length() + 4);
            message.setOffsetEnd(queueDataFile.length() + 4 + messageBinary.length);
            //4.写入消息到数据文件,注意的是,此时的写入是追加写入
            try(OutputStream outputStream = new FileOutputStream(queueDataFile, true)) {
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    //接下来要先写当前消息的长度,占据 4 个字节
                    dataOutputStream.writeInt(messageBinary.length);
                    //写入消息本体
                    dataOutputStream.write(messageBinary);
                }
            }
            //5.更新消息统计文件,增加一个消息长度,有效消息个数加一
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
        }
    }

10:方法:deleeteMessage

这里的删除采用的是逻辑删除通过设置关键字的方式来处理消息的有效与无效,并没有采用物理删除

这里使用 RondomAccessFile的原因在于 允许我们通过 seek(offset) 像访问数据下标一样,直接通过移动光标的方式,快速的定位到待删除文件的开头位置,然后根据长度进行读取,当我们把相关的关键字修改完毕之后,由于光标移动的原因,我们需要再次从新定位光标位置,对 Message 对象进行反序列化之后再次写入文件中

java 复制代码
//这个是删除消息的方法
    //这里的删除是逻辑删除,也就是把硬盘上存储的这个数据里面的 isValid 字段设置为 0
    //1.先把文件中的这段数据读取出来,还原会 Message 对象
    //2.把isValid字段设置为0
    //3.把修改后的的数据从新写入的文件中
    //此处的message 对象,必须包含 offsetBeg 和 offsetEnd
    public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException {
        synchronized (queue) {
            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.把修改后的数据重新写入文件
                byte[] bufferDest = BinaryTool.toBytes(diskMessage);
                //虽然上面已经 seek 过了,但是上的 seek 完了之后,进行读操作,这一读,就导致文件的光标会发生移动
                //移动到下一个文件的位置了,因此要想让接下来,能够刚好的写回道之前的位置,就需要从新调整文件光标的位置
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bufferDest);
                //通过上述这样的操作,对于文件来说,只是有一个字节发生了改变
            }
            //5.更新消息统计文件,有效消息个数减一
            Stat stat = readStat(queue.getName());
            if(stat.validCount > 0) {
                stat.totalCount -= 1;
            }
            writeStat(queue.getName(), stat);
        }
    }

11.方法:loadAllMessageFromQueue

把硬盘中的数据加载到内存中去

这个代码设计的巧妙之处在于看似这个 while 循环是一个死循环,而且在其内部也没有结束循环的标语,巧妙之处体现在,当 readInt( )读取到文件的末尾,它不会返回 -1 而是直接抛出 EOFException 此时的循环自然也就结束了,也到达到了我们的目的

java 复制代码
//这个方法从文件中,读取出所有的消息内容,加载道内存中(具体的说是一个链表中)
    //这个方法,在程序准备启动的时候进行调用
    //这里使用 LinkedList 主要是为了后续进行的头删操作
    //这个方法的参数使用 queueName 而不是使用 MSGQueue对象,因为这个方法不需要加锁,只使用 queueName 就行了
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        //通过队列名获取到该队列的数据文件路径,然后进行文件读取
        try(InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))) {
            //创建DataInputStream 来读取数据
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                //创建变量来记录文件光标的位置,初始值为0
                long currentOffset = 0;
                //一个文件中有很多的消息数据,要进行循环数据读取
                while(true) {
                    //1.读取当前消息长度,先读取四个字节获取内容长度
                    int messageSize = dataInputStream.readInt();
                    //2.按照这个长度去读取消息内容
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    //messageSize : 预期长度, actualSize : 实际长度
                    if (actualSize != messageSize) {
                       //如果不匹配说明文件有问题,格式错乱
                        throw new MqException("[MessageFileManager] 文件格式错乱 queueName" + queueName);
                    }
                    //3.把读到的这个二进制数据给反序列化成Message对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    //4.判定一下这个消息对象是否是无效的消息对象
                    if(message.getIsValid() != 0x1) {
                        //无效数据直接跳过
                        currentOffset += (4 + messageSize);
                        continue;
                    }
                    //5.有效数据,则需要把这个 Message 对象加入到链表中去,加入之前还要填写 offsetBeg 和 offsetEnd
                    //进行计算 offset 的时候,需要知道当前光标文件的位置的,由于当下使用的是 DataInputStream 并不方便
                    //这里采用手动记录光标位置
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    currentOffset += (4 + messageSize);
                    messages.add(message);
                }
            } catch (EOFException e) {
                //这个 catch 并非是处理"异常"的,而是处理"正常"的业务逻辑,文件读到末尾,会被 readInt 抛出该异常
                System.out.println("[MessageFileManager] 文件读取完毕 queueName" + queueName);
            }
        }
        return messages;
    }

12.方法:gc

垃圾处理机制 gc这里设置的触发条件是 消息总数达到 2000 条及以上,有效消息条数小于50%的时候会触发 垃圾回收操作

java 复制代码
//检查是否需要对当前文件的内容进行 GC 操作
    public boolean checkGC(String queueName) {
        Stat stat = readStat(queueName);
        if (stat.totalCount > 2000 && (double)stat.validCount / (double)stat.totalCount < 0.5) {
            return true;
        }
        return false;
    }

13.方法getQueueDataNewPath

目的在于创建新的用于替换旧的文件的文件名

java 复制代码
//用这个方法来创建新的文件名
    private String getQueueDataNewPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }

14.方法 gc

垃圾回收操作

线程安全采用加锁的方式来解决(synchronized),加锁对象就是队列

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

            //1.创建一个新的文件, 仅仅是在内存里准备一个"路径标签",此时硬盘没动静
            File queueDataNewFile = new File(getQueueDataNewPath(queue.getName()));
            if(queueDataNewFile.exists()) {
                //正常的情况下,这个文件是不应该出现的,如果存在就说明上次的 gc 操作出现了意外
                throw new MqException("[MessageFileManager] gc 文件已存在 queueName" + queue.getName());
            }
            // 真正命令操作系统在硬盘上创建这个新文件
            boolean ok = queueDataNewFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManager] 创建新文件失败 queueDataNewFile" + queueDataNewFile.getAbsolutePath());
            }

            //2.从旧的文件中,读取所有的有效消息对象(可以使用上述的方法)
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());

            //3.把有效的消息,写入到新的文件
            try(OutputStream outputStream = new FileOutputStream(queueDataNewFile)) {
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        byte[] buffer = BinaryTool.toBytes(message);
                        //先写四个字节的长度,表示后续要写入的数据长度
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }

            //4.删除旧的数据文件,并且把新的数据文件进行重命名
            File queueDataOldFile = new File(getQueueDataPath(queue.getName()));
            ok = queueDataOldFile.delete();
            if (!ok) {
                throw new MqException("[MessageFileManager] 删除旧文件失败 queueDataOldFile" + queueDataOldFile.getAbsolutePath());
            }

            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 耗时 " + (gcEnd - gcBeg) + " ms");

        }
    }

MessageFileManage 类中的方法对应的测试用例

java 复制代码
private MessageFileManager messageFileManager = new MessageFileManager();

    private static final String queueName1 = "testQueue1";
    private static final String queueName2 = "testQueue2";

    //这个方法是每个用例执行之前的准备工作
    @BeforeEach
    public void setUp() throws IOException {
        //准备阶段创建两个队列
        messageFileManager.createQueueFile(queueName1);
        messageFileManager.createQueueFile(queueName2);
    }

    //这个方法是每个用例执行之后的清理工作
    @AfterEach
    public void tearDown() throws IOException {
        //清理阶段删除两个队列
        messageFileManager.destroyQueueFile(queueName1);
        messageFileManager.destroyQueueFile(queueName2);
    }

    @Test
    public void testCreateFiles() {
        //在上述的 setUp 中已经调用了这个方法,我们在此处主要是检验一下
        //该队列中下属的两个文件是否存在
        File queueDataFile1 = new File("./data/" + queueName1 + "/queue_data.txt");
        Assertions.assertEquals(true, queueDataFile1.isFile());
        File queueStatFile1 = new File("./data/" + queueName1 + "/queue_stat.txt");
        Assertions.assertEquals(true, queueStatFile1.isFile());

        File queueDataFile2 = new File("./data/" + queueName2 + "/queue_data.txt");
        Assertions.assertEquals(true, queueDataFile2.isFile());
        File queueStatFile2 = new File("./data/" + queueName2 + "/queue_stat.txt");
        Assertions.assertEquals(true, queueStatFile2.isFile());
    }

    @Test
    public void testReadWriteStat() {
        MessageFileManager.Stat stat = new MessageFileManager.Stat();
        stat.totalCount = 100;
        stat.validCount = 52;
        //此处采用反射的方式来调用 writeStat 和 readStat 方法
        //这里采用 Spring 提供的反射方法来调用私有方法
        //第一个参数是对象实例,第二个参数是方法名,之后的参数是方法的参数
        ReflectionTestUtils.invokeMethod(messageFileManager, "writeStat", queueName1, stat);

        //写入完毕之后检查读取的文件内容是否一致
        MessageFileManager.Stat newStat = ReflectionTestUtils.invokeMethod(messageFileManager, "readStat", queueName1);
        Assertions.assertEquals(100, newStat.totalCount);
        Assertions.assertEquals(52, newStat.validCount);
    }


    private MSGQueue createTestQueue(String queueName) {
        MSGQueue queue = new MSGQueue();
        queue.setName(queueName);
        queue.setDurable(true);
        queue.setAutoDelete(false);
        queue.setExclusive(false);
        return queue;
    }

    private Message createTestMessage(String content) {
        Message message = Message.createMessageWithId("testRoutingKey",null, content.getBytes());
        return message;
    }

    @Test
    public void testSendMessage() throws IOException, MqException, ClassNotFoundException {
        //构造出消息,并构造队列
        Message message = createTestMessage("testMessage");
        //构造队列,此处的队列名不能随便写,只能使用前面创建好的 queueName1 和queueName2
        MSGQueue queue = createTestQueue(queueName1);

        //调用发送消息方法
        messageFileManager.sendMessage(queue, message);

        //检查 stat 文件
        MessageFileManager.Stat stat = ReflectionTestUtils.invokeMethod(messageFileManager, "readStat", queueName1);
        Assertions.assertEquals(1, stat.totalCount);
        Assertions.assertEquals(1, stat.validCount);

        //检查 data 文件
        LinkedList<Message> messages = messageFileManager.loadAllMessageFromQueue(queueName1);
        Assertions.assertEquals(1, messages.size());
        Message curMessage = messages.get(0);
        Assertions.assertEquals(message.getMessageId(), curMessage.getMessageId());
        Assertions.assertEquals(message.getRoutingKey(), curMessage.getRoutingKey());
        Assertions.assertEquals(message.getDeliveryMode(), curMessage.getDeliveryMode());
        //比较两个字节数组的内容是否相同,不能直接使用 assertEquals 了
        Assertions.assertArrayEquals(message.getBody(), curMessage.getBody());

        System.out.println("message:" + curMessage.toString());
    }

    @Test
    public void testLoadAllMessageFromQueue() throws IOException, MqException, ClassNotFoundException {
        //往队列中插入 100 条消息,然后验证这 100 条消息从文件中读取之后,是否和最初的是一致的
        MSGQueue queue = createTestQueue(queueName1);
        List<Message> expectedMessages = new LinkedList<>();
        for (int i = 0; i < 100; i ++) {
            Message message = createTestMessage("testMessage" + i);
            messageFileManager.sendMessage(queue, message);
            expectedMessages.add(message);
        }

        //读取所有消息
        LinkedList<Message> actualMessages = messageFileManager.loadAllMessageFromQueue(queueName1);
        Assertions.assertEquals(expectedMessages.size(), actualMessages.size());
        for(int i = 0; i < expectedMessages.size(); i++) {
            Message expectedMessage = expectedMessages.get(i);
            Message actualMessage = actualMessages.get(i);
            System.out.println("[" + i + "] actualMessage:" + actualMessage.toString());

            Assertions.assertEquals(expectedMessage.getMessageId(), actualMessage.getMessageId());
            Assertions.assertEquals(expectedMessage.getRoutingKey(), actualMessage.getRoutingKey());
            Assertions.assertEquals(expectedMessage.getDeliveryMode(), actualMessage.getDeliveryMode());
            Assertions.assertArrayEquals(expectedMessage.getBody(), actualMessage.getBody());
            Assertions .assertEquals(0x1, actualMessage.getIsValid());
        }
    }

    @Test
    public void testDeleteMessage() throws IOException, MqException, ClassNotFoundException {
        //创建队列写入 10 个消息,删除其中的几个消息,再把所有的消息读取出来,判断是否符合预期
        MSGQueue queue = createTestQueue(queueName1);
        List<Message> expectedMessages = new LinkedList<>();
        for (int i = 0; i < 10; i++) {
            Message message = createTestMessage("testMessage" + i);
            messageFileManager.sendMessage(queue, message);
            expectedMessages.add(message);
        }

        //删除其中的三个消息
        messageFileManager.deleteMessage(queue, expectedMessages.get(7));
        messageFileManager.deleteMessage(queue, expectedMessages.get(8));
        messageFileManager.deleteMessage(queue, expectedMessages.get(9));

        //对比内容是否正确
        LinkedList<Message> actualMessages =  messageFileManager.loadAllMessageFromQueue(queueName1);
        Assertions.assertEquals(expectedMessages.size() - 3, actualMessages.size());
        for (int i = 0; i < actualMessages.size(); i++) {
            Message expectedMessage = expectedMessages.get(i);
            Message actualMessage = actualMessages.get(i);
            System.out.println("[" + i + "] actualMessage:" + actualMessage.toString());

            Assertions.assertEquals(expectedMessage.getMessageId(), actualMessage.getMessageId());
            Assertions.assertEquals(expectedMessage.getRoutingKey(), actualMessage.getRoutingKey());
            Assertions.assertEquals(expectedMessage.getDeliveryMode(), actualMessage.getDeliveryMode());
            Assertions.assertArrayEquals(expectedMessage.getBody(), actualMessage.getBody());
            Assertions .assertEquals(0x1, actualMessage.getIsValid());
        }
    }

    @Test
    public void testGC() throws IOException, MqException, ClassNotFoundException {
        //先往队列中写100个消息
        //再把 100 个消息的一半,都给删掉
        //再手动调用 gc 方法,检查到当前文件的体积是否比之前缩小了
        MSGQueue queue = createTestQueue(queueName1);
        List<Message> expectedMessages = new LinkedList<>();
        for(int i = 0; i < 100; i++) {
            Message message = createTestMessage("testMessage" + i);
            messageFileManager.sendMessage(queue, message);
            expectedMessages.add(message);
        }
        //获取 gc 前的文件大小
        File beforeGCFile = new File("./data/" + queueName1 + "/queue_data.txt");
        long beforeGCLength = beforeGCFile.length();

        //删除偶数下标的消息
        for (int i = 0; i < expectedMessages.size(); i+=2) {
            messageFileManager.deleteMessage(queue, expectedMessages.get(i));
        }

        //手动调用 gc
        messageFileManager.gc(queue);

        //重新读取文件,验证新的文件内容是否和之前另外一部分内容相同
        LinkedList<Message> actualMessages = messageFileManager.loadAllMessageFromQueue(queueName1);
        Assertions.assertEquals(50, actualMessages.size());
        for(int i = 0; i < actualMessages.size(); i++) {
            //之前把偶数下标的元素
            Message expectedMessage = expectedMessages.get(i * 2 + 1);
            Message actualMessage = actualMessages.get(i);

            Assertions.assertEquals(expectedMessage.getMessageId(), actualMessage.getMessageId());
            Assertions.assertEquals(expectedMessage.getRoutingKey(), actualMessage.getRoutingKey());
            Assertions.assertEquals(expectedMessage.getDeliveryMode(), actualMessage.getDeliveryMode());
            Assertions.assertArrayEquals(expectedMessage.getBody(), actualMessage.getBody());
            Assertions .assertEquals(0x1, actualMessage.getIsValid());
        }
        //获取新的文件大小
        File afterGCFile = new File("./data/" + queueName1 + "/queue_data.txt");
        long afterGCLength = afterGCFile.length();
        System.out.println("beforeGCLength:" + beforeGCLength);
        System.out.println("afterGCLength:" + afterGCLength);
        Assertions.assertTrue(beforeGCLength > afterGCLength);
    }
相关推荐
2501_944525546 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
android·开发语言·前端·javascript·flutter·ecmascript
heartbeat..6 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
7 小时前
java关于内部类
java·开发语言
好好沉淀7 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin7 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder7 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
lsx2024067 小时前
FastAPI 交互式 API 文档
开发语言
吨~吨~吨~7 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟7 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日7 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式