仿 RabbitMQ 消息队列5(实战项目)

九. 内存数据结构设计

硬盘上存储数据, 只是为了实现 "持久化" 这样的效果. 但是实际的消息存储/转发, 还是主要靠内存的结构.

对于 MQ 来说, 内存部分是更关键的, 内存速度更快, 可以达成更⾼的并发.

创建 MemoryDataCenter

用来管理内存里的数据

  • 使⽤四个哈希表, 分别管理 Exchange, Queue, Binding, Message.
  • 再使⽤⼀个哈希表 + 链表管理 队列 -> 消息 之间的关系.
  • 再使⽤⼀个哈希表 + 哈希表管理所有的未被确认的消息.
java 复制代码
/**
 * 关于内存中的数据结构:刚接触这个数据结构的设计可能有点不太理解,忍一忍,多往下看看
 *
 * • 使⽤四个哈希表, 管理 Exchange, Queue, Binding(嵌套的hash表), Message.
 * • 使⽤⼀个哈希表 + 链表管理 队列 -> 消息 之间的关系.
 *
 * • 使⽤⼀个哈希表 + 哈希表管理所有的未被确认的消息.
 *  此处的最后一个数据结构创建的原因:
 *  咱们的MQ,支持两种应答模式,
 *  1,自动应答:消费者取了这个元素,这个消息就算是被应答了,此时这个消息就能被删掉了
 *  2,手动应答:消费者取了这个元素,这个消息还不算被应答,需要消费者主动再调用一个basicAck方法,此时才认为是真正应答了这个消息,才能删除这个消息
 *  而此处我们设计的这个数据结构就是为了第二种应答方式,保存未被应答的消息。
 */
public class MemoryDataCenter {
    //由于会涉及到线程安全问题,所以我们用的是ConcurrentHashMap来new hash表

    //储存Exchange  key:exchangeName value:Exchange对象
    private ConcurrentHashMap<String, Exchange> exchangeMap = new ConcurrentHashMap<>();
    //储存Queue     key:queueName   value:queue对象
    private ConcurrentHashMap<String, MESGQueue> queueMap = new ConcurrentHashMap<>();
    //储存Message  key: messageId    value:Message对象:
    private ConcurrentHashMap<String, Message> messageMap = new ConcurrentHashMap<>();
    //储存绑定 使用嵌套的hash表    key:exchangeName  value:hash表  key2:queueName  value2:Binding对象
    private ConcurrentHashMap<String, ConcurrentHashMap<String, Binding>> bindingMap = new ConcurrentHashMap<>();
    //储存队列->消息之间的关系,使用hash表嵌套链表   key:queueName value:LinkedList  链表:保存Message对象
    private ConcurrentHashMap<String, LinkedList<Message>> queueMessageMap = new ConcurrentHashMap<>();
    //储存未被确认的消息,使用hash表嵌套hash表:key :queueName value:hash表 key2:messageId  value2:Message对象
    private ConcurrentHashMap<String, ConcurrentHashMap<String, Message>> queueMessageWaitAck = new ConcurrentHashMap<>();
 }

封装 Exchange ⽅法

java 复制代码
//封装Echange方法:
    //删除交换机:
    public void insertExchange(Exchange exchange) {
        exchangeMap.put(exchange.getName(), exchange);
    }

    //获取交换机:
    public Exchange getExchange(String exchangeName) {
        return exchangeMap.get(exchangeName);
    }

    //删除交换机:
    public void deleteExchange(String exchangeName) {
        exchangeMap.remove(exchangeName);
    }

封装 Queue ⽅法

java 复制代码
/封装queue方法:
    public void insertQueue(MESGQueue queue) {
        queueMap.put(queue.getName(), queue);
    }

    public MESGQueue getQueue(String queueName) {
        return queueMap.get(queueName);
    }

    public void deleteQueue(String queueName) {
        queueMap.remove(queueName);
    }

封装binding方法

java 复制代码
     //封装binding方法:
    public void insertBinding(Binding binding) throws MqException {
//        ConcurrentHashMap<String,Binding> hasBindingMap = bindingMap.get(binding.getExchangeName());
        //这里代码其实有一个错误,就是锁对象不能为空,判空要在加锁外面
//        synchronized (hasBindingMap){
//            //如果不存在才能插入:
//            if(hasBindingMap == null){
//                hasBindingMap = new ConcurrentHashMap<>();
//                hasBindingMap.put(binding.getQueueName(),binding);
//                bindingMap.put(binding.getExchangeName(),hasBindingMap);
//            }else{
//                //存在则抛出异常:
//                throw new MqException("[MemoryDataCenter] 绑定已经存在!!不可再插入绑定!!! bindingExchangeName:"+binding.getExchangeName()
//                        +"bindingQueueName:"+binding.getQueueName());
//            }
//        }

        //这一行是线程安全的,能代替上述判断空然后将hashBindingMap new出来的代码。
        //bindingMap.computeIfAbsent 作用:如果第一个参数为空,则将第二个参数插入,并返回第二个参数
        ConcurrentHashMap<String, Binding> bindingMapSon = bindingMap.computeIfAbsent(binding.getExchangeName(), k -> new ConcurrentHashMap<>());

        synchronized (bindingMapSon) {
            if (bindingMapSon.get(binding.getQueueName()) != null) {
                throw new MqException("[MemoryDataCenter] 绑定已经存在!!不可再插入绑定!!! bindingExchangeName:" + binding.getExchangeName()
                        + "bindingQueueName:" + binding.getQueueName());
            }
            bindingMapSon.put(binding.getQueueName(), binding);
        }
    }

    //写两个版本 的绑定:
    //1,根据exchangeName 和 queueName 确定唯一一个Binding
    //2,根据exchangeName 获取到所有的 Binding哈希表,后面会用到
    public Binding getBinding(String exchangeName, String queueName) {
        ConcurrentHashMap<String, Binding> bindingMapIn = bindingMap.get(exchangeName);
        if (bindingMapIn == null) return null;
        synchronized (bindingMapIn) {
            return bindingMapIn.get(queueName);
        }
    }

    public ConcurrentHashMap<String, Binding> getBindings(String exchangeName) {
        return bindingMap.get(exchangeName);
    }

    //删除绑定;
    public void deleteBinding(Binding binding) throws MqException {
        ConcurrentHashMap<String, Binding> bindingMapIn = bindingMap.get(binding.getExchangeName());
        if (bindingMapIn == null) {
            throw new MqException("[MemoryDataCenter] 绑定不存在!!!exchangeName:" + binding.getExchangeName()
                    + "queueName:" + binding.getQueueName());
        }
        synchronized (bindingMapIn) {
            Binding toDelete = bindingMapIn.get(binding.getQueueName());
            if (toDelete == null) {
                throw new MqException("[MemoryDataCenter] 绑定不存在!!!exchangeName:" + binding.getExchangeName()
                        + "queueName:" + binding.getQueueName());
            }
            bindingMapIn.remove(binding.getQueueName());
        }
    }

封装 Message ⽅法

java 复制代码
//对消息的操作:
    //查询消息:
    public Message getMessage(String messageId) {
        return messageMap.get(messageId);
    }

    //添加消息:
    public void addMessage(Message message) {
        messageMap.put(message.getMessageId(), message);
        System.out.println("[MemoryDataCenter] message插入成功!!! messageId:" + message.getMessageId());
    }

    //删除消息:
    public void deleteMessage(String messageId) {
        messageMap.remove(messageId);
        System.out.println("[MemoryDataCenter] message删除成功!!! messageId:" + messageId);
    }

    // 发送消息到指定队列中
    public void sendMessage(MESGQueue queue, Message message) {
        //这些代码可以用一行代码代替:这两行应该都行:经过验证:第一行是错的。
//        LinkedList<Message> messages = queueMessageMap.putIfAbsent(queue.getName(), new LinkedList<>());
        LinkedList<Message> messages = queueMessageMap.computeIfAbsent(queue.getName(),k->new LinkedList<>());
//        //先根据队列的名字,找到对应的消息链表:
//        LinkedList<Message> messages = queueMessageMap.get(queue.getName());
//        //如果指定的消息链表不存在,则new出来:
//        if(messages == null){
//            messages = new LinkedList<>();
//        }
        synchronized (messages) {
            //将消息添加到链表里:
            messages.add(message);
        }
        //别忘了将消息添加到消息中心,假设message已经在消息中心存在,重复插入也没有关系,毕竟是hash表嘛
        //另外,这一句也不用考虑线程安全问题,因为,就算多线程多插了几次,可他是hash表啊,插入的对象都一样,不就相当于覆盖了吗
        messageMap.put(message.getMessageId(), message);
        System.out.println("[MemoryDataCenter]  消息send到队列中成功!!!messageId:" + message.getMessageId());
    }

    //从队列中取消息(需要删除的那一种):
    public Message pollMessage(String queueName) throws MqException {
        //先根据队列的名字获取对应的消息链表:
        LinkedList<Message> messages = queueMessageMap.get(queueName);
        //必须将messages == null写在外边,因为加锁操作的锁对象不能为空。
        if (messages == null) {
            throw new MqException("[MemoryDataCenter] 消息不存在! 取消息失败!!! queueName=" +
                    queueName);
        }
        synchronized (messages) {
            //如果非空,则将表头的元素返回:
            if (messages.size() == 0) return null;
            Message currentMessage = messages.remove(0);
            System.out.println("[MemoryDataCenter] 消息从队列中取出成功!!!messageId:" + currentMessage.getMessageId());
            return currentMessage;
        }
    }

    //获取消息的数量:
    public int countMessages(String queueName) {
        LinkedList<Message> messages = queueMessageMap.get(queueName);
        if (messages == null) {
            // 如果队列不存在, 则直接返回⻓度 0, 说明该 queueName 下还没有消息
            return 0;
        }
        synchronized (messages) {
            return messages.size();
        }
    }

针对未确认的消息处理

java 复制代码
 //对于未被确认的消息:

    //添加未被确认的消息:
    public void addMessageWaitAck(String queueName, Message message) {
        //先依据queueName 获取对应的消息hash表;由于不存在还是需要new出来一个,这里就直接使用cmputeIfAbsent了
        ConcurrentHashMap<String, Message> messageWaitMap = queueMessageWaitAck.computeIfAbsent(queueName, k -> new ConcurrentHashMap<>());
        //由于ConcurrentHashMap是线程安全的,所以这里就不加锁了。
        messageWaitMap.put(message.getMessageId(), message);
        System.out.println("[MemoryDataCenter] 添加 未被确认的消息成功!!!messageId:" + message.getMessageId());
    }

    //删除未被确认的消息:
    public void deleteMessageWaitAck(String queueName, String messageId) {
        ConcurrentHashMap<String, Message> messageWaitMap = queueMessageWaitAck.get(queueName);
        if (messageWaitMap == null) return;
        messageWaitMap.remove(messageId);
        System.out.println("[MemoryDataCenter] 删除 未被确认的消息成功!!!messageId:" + messageId);
    }

    //查询未被确认的消息:
    public Message getMessageWaitAck(String queueName, String messageId) {
        ConcurrentHashMap<String, Message> messageWaitMap = queueMessageWaitAck.get(queueName);
        if (messageWaitMap == null) return null;
        return messageWaitMap.get(messageId);
    }

实现重启后恢复内存

java 复制代码
// 实现重启后恢复内存,从硬盘上恢复数据
    public void recovery(DiskDataCenter diskDataCenter) throws IOException, ClassNotFoundException, MqException {
        //在恢复数据之前,先将内存中的数据结构清空:
        exchangeMap.clear();
        queueMap.clear();
        bindingMap.clear();
        messageMap.clear();
        queueMessageMap.clear();
        //恢复交换机:
        List<Exchange> exchanges= diskDataCenter.selectExchanges();
        for(Exchange x :exchanges){
            exchangeMap.put(x.getName(),x);
        }
        //恢复消息队列:
        List<MESGQueue> queues = diskDataCenter.selectMESGQueues();
        for (MESGQueue x:queues) {
            queueMap.put(x.getName(),x);
        }
        //恢复绑定(注意绑定是嵌套的数据结构)
        List<Binding> bindings = diskDataCenter.selectBindings();
        for(Binding x :bindings){
//            ConcurrentHashMap<String,Binding> bindingMap2 = bindingMap.computeIfAbsent(x.getExchangeName(),k->new ConcurrentHashMap<>());
//            bindingMap2.put(x.getQueueName(),x);
            String exchangeName = x.getExchangeName();
            String queueName = x.getQueueName();
            ConcurrentHashMap<String,Binding> hash = new ConcurrentHashMap<>();
            bindingMap.put(exchangeName,hash);
            hash.put(queueName,x);
        }
        //恢复消息:
        //这一步就要将所有的队列都枚举出来,挨个恢复消息(说白了也就是loadAllMessagesFromQueue缺少queueName参数,所以才一个个枚举队列,拿到queueName)
        //上面已经将所有的队列拿出来过了,可以使用现成的方法:
        for(MESGQueue queue:queues){
            LinkedList<Message> messages = diskDataCenter.loadAllMessagesFromQueue(queue.getName());
            //这个恢复消息表面上只有一个恢复消息的操作,可是他却需要将两个数据结构都填上:
            //messageMap和queueMessageMap
            queueMessageMap.put(queue.getName(),messages);
            //然后再将消息一一填入消息中心:
            for(Message message:messages){
                messageMap.put(message.getMessageId(),message);
            }
        }
        //    queueMessageWaitAck 则不必恢复. 未被确认的消息只是在内存存储. 如果这个时候
       //       broker 宕机了, 则消息视为没有被消费过.
        //另一个原因也就是我们根本没有准备任何关于queueMessageWaitAck的代码。

    }

测试 MemoryDataCenter

java 复制代码
@SpringBootTest
public class MemoryDataCenterTests {
    private MemoryDataCenter memoryDataCenter = null;

    @BeforeEach
    public void setUp(){
        memoryDataCenter = new MemoryDataCenter();
        memoryDataCenter.init();
    }
    @AfterEach
    public void tearDown(){
        memoryDataCenter = null;
    }
    //一个单元测试不一定只能测试一个方法,也可以测试多个方法,视情况而定:
    //交换机测试思路:先插入一个交换机,检测一下拿到的交换机是否是同一个;再将这个交换机删掉,检测一下是否查不到了
    @Test
    public void testExchange(){
        Exchange exchange = new Exchange();
        exchange.setName("111");
        exchange.setAutoDelete(false);
        exchange.setDurable(false);
        exchange.setType(ExchangeType.DIRECT);
        HashMap<String,Object> hash = new HashMap<>();
        hash.put("111",111);
        hash.put("222",222);
        hash.put("333",333);
        exchange.setArguments(hash);

        memoryDataCenter.insertExchange(exchange);
        Exchange realExchange = memoryDataCenter.getExchange("111");
        Assertions.assertEquals("111",realExchange.getName());
        Assertions.assertEquals(ExchangeType.DIRECT,realExchange.getType());
        Assertions.assertEquals(111,realExchange.getArguments("111"));
        Assertions.assertEquals(222,realExchange.getArguments("222"));
        memoryDataCenter.deleteExchange("111");
        Exchange realExchange2 = memoryDataCenter.getExchange("111");
        Assertions.assertEquals(null,realExchange2);
        System.out.println("[testExchange] 测试成功!!!");
    }
    //测试队列,和测试交换机思想差不多:
    @Test
    public void testQueue(){
        //先创建队列:
        MESGQueue queue = new MESGQueue();
        queue.setName("testQueue");
        queue.setExclusive(false);
        queue.setDurable(false);
        HashMap<String,Object> hash = new HashMap<>();
        hash.put("111",111);
        hash.put("222",222);
        hash.put("333",333);
        queue.setArguments(hash);

        //测试:
        memoryDataCenter.insertQueue(queue);
        MESGQueue realQueue = memoryDataCenter.getQueue("testQueue");
        Assertions.assertEquals("testQueue",realQueue.getName());
        Assertions.assertEquals(111,realQueue.getArguments("111"));
        Assertions.assertEquals(222,realQueue.getArguments("222"));
        memoryDataCenter.deleteQueue("testQueue");
        Assertions.assertEquals(null,memoryDataCenter.getQueue("testQueue"));
        System.out.println("[testQueue] 测试成功!!!");
    }
    //测试绑定:一样的思路,只不过因为绑定是嵌套的,所以略有不同:
    @Test
    public void testBinding() throws MqException {
        Binding binding = new Binding();
        binding.setExchangeName("testExchange");
        binding.setQueueName("testQueue");
        binding.setBindingKey("bindingKey");
        //先插入绑定:
        memoryDataCenter.insertBinding(binding);
        Binding realBinding  = memoryDataCenter.getBinding("testExchange","testQueue");
        Assertions.assertEquals(binding,realBinding);
        ConcurrentHashMap<String,Binding> hash = memoryDataCenter.getBindings("testExchange");
        Assertions.assertEquals(binding,hash.get("testQueue"));
        memoryDataCenter.deleteBinding(binding);
        Assertions.assertNull(memoryDataCenter.getBinding("testExchange","testQueue"));
        System.out.println("[testBinding] 测试成功!!!");
    }
    //测试消息:
    @Test
    public void testMessage(){
        Message message = new Message();
        message.setMessageId("123");
        message.setBody("abcd".getBytes());

        memoryDataCenter.addMessage(message);
        Message realMessage = memoryDataCenter.getMessage("123");
        Assertions.assertEquals(message,realMessage);
        memoryDataCenter.deleteMessage("123");
        Assertions.assertNull(memoryDataCenter.getMessage("123"));
        System.out.println("[testMessage] 测试成功!!!");
    }

    //为了测试这个方法,我们先将创建队列和消息的方法写出来:
    private MESGQueue createMESGQueue(String queueName){
        MESGQueue queue = new MESGQueue();
        queue.setName(queueName);
        queue.setExclusive(false);
        queue.setDurable(false);
        HashMap<String,Object> hash = new HashMap<>();
        hash.put("111",111);
        hash.put("222",222);
        return queue;
    }
    private Message createMessage(String messageId){
        Message message = new Message();
        message.setMessageId(messageId);
        message.setBody("abcd".getBytes());
        return message;
    }
    //由于测试这个方法需要 queue和message,先将他们创建出来
    //这个send方法的测试思路:先将10消息发送到指定队列,再将10条消息拿出来,逐个对比,
    @Test
    public void testSendMessage() throws MqException {
        MESGQueue queue = createMESGQueue("testQueue1234");
        LinkedList<Message> expectedMessages = new LinkedList<>();
        //发送消息:
        for(int i =0;i<10;i++){
            Message message = createMessage("testMessage"+i);
            memoryDataCenter.sendMessage(queue,message);
            expectedMessages.add(message);
        }
        Assertions.assertEquals(10,memoryDataCenter.countMessages("testQueue1234"));
        //拿出消息验证个数:
        LinkedList<Message> realMessages = new LinkedList<>();
        while(true){
            Message message = memoryDataCenter.pollMessage(queue.getName());
            if(message == null) break;
            realMessages.add(message);
        }
        Assertions.assertEquals(10,realMessages.size());
        for(int i =0;i<10;i++){
            Assertions.assertEquals(expectedMessages.get(i),realMessages.get(i));
        }
        System.out.println("[testSendMessage] 测试成功!!!");
    }
    @Test
    public void testMessageWaitAck(){
        //这里我们只是测试,由于MemoryDataCenter子给自足,直接不用new队列传入队列名字也是一样的,反正内部代码不存在终究会new出来:
        Message expectedMessage = createMessage("messageId123");
        memoryDataCenter.addMessageWaitAck("queueName123",expectedMessage);
        Message realMessage = memoryDataCenter.getMessageWaitAck("queueName123","messageId123");
        Assertions.assertEquals(expectedMessage,realMessage);
        memoryDataCenter.deleteMessageWaitAck("queueName123","messageId123");
        Assertions.assertNull(memoryDataCenter.getMessageWaitAck("queueName123","messageId123"));
        System.out.println("[testMessageWaitAck] 测试成功!!!");
    }
    @Test
    public void testRecovery() throws IOException, MqException, ClassNotFoundException {
        //由于后续要进行数据库操作,依赖MyBatis,就需要启动SpringApplication,这样才能进行后续的数据库操作:
        MqApplication.context = SpringApplication.run(MqApplication.class);
        //构造硬盘数据:
        DiskDataCenter diskDataCenter = new DiskDataCenter();
        diskDataCenter.init();

        //交互换机:
        Exchange expectedExchange = new Exchange();
        HashMap<String,Object> hash = new HashMap<>();
        hash.put("111",111);
        expectedExchange.setName("testExchange");
        expectedExchange.setArguments(hash);
        diskDataCenter.insertExchange(expectedExchange);
        //队列:
        MESGQueue expectedQueue = createMESGQueue("testQueue");
        diskDataCenter.insertMESGQueue(expectedQueue);
        //绑定:
        Binding expectedBinding = new Binding();
        expectedBinding.setExchangeName("testExchange");
        expectedBinding.setQueueName("testQueue");
        expectedBinding.setBindingKey("testBindingKey");
        diskDataCenter.insertBinding(expectedBinding);
        //消息:
        Message expectedMessage = createMessage("testMessage");
        diskDataCenter.sendMessage(expectedQueue,expectedMessage);

        //执行恢复操作:
        memoryDataCenter.recovery(diskDataCenter);

        //对比结果:
        //交换机:
        Exchange realExchange = memoryDataCenter.getExchange("testExchange");
        //此处不能直接判断expectedExchange和realExchange,因为从文件中拿出来的不是同一个对象,和在内存中拿不一样。
//        Assertions.assertEquals(expectedExchange,realExchange);
        Assertions.assertEquals(expectedExchange.getName(),realExchange.getName());
        Assertions.assertEquals(expectedExchange.getType(),realExchange.getType());
        Assertions.assertEquals(expectedExchange.getArguments("111"),realExchange.getArguments("111"));

        //队列:
        MESGQueue realQueue = memoryDataCenter.getQueue("testQueue");
        Assertions.assertEquals(expectedQueue.getName(),realQueue.getName());
        Assertions.assertEquals(expectedQueue.getArguments("222"),realQueue.getArguments("222"));

        //绑定:
        Binding realBinding = memoryDataCenter.getBinding("testExchange","testQueue");
        Assertions.assertEquals(expectedBinding.getExchangeName(),realBinding.getExchangeName());
        Assertions.assertEquals(expectedBinding.getQueueName(),realBinding.getQueueName());
        Assertions.assertEquals(expectedBinding.getBindingKey(),realBinding.getBindingKey());

        //消息:
        Message realMessage = memoryDataCenter.getMessage("testMessage");
        Assertions.assertEquals(expectedMessage.getMessageId(),realMessage.getMessageId());
        Assertions.assertArrayEquals(expectedMessage.getBody(),realMessage.getBody());

        Message realMessage2 = memoryDataCenter.pollMessage("testQueue");
        Assertions.assertEquals(expectedMessage.getMessageId(),realMessage2.getMessageId());
        Assertions.assertArrayEquals(expectedMessage.getBody(),realMessage2.getBody());

        //清理硬盘的数据,把整个data目录里的内容都删了(包含了meta.db)
        //FileUtils.deleteDirectory方法,即使目录里面有文件,会递归的删除所有。
        //记得将SpringApplicatioin关掉,运行着会删除文件失败。
        MqApplication.context.close();
        File dataDir = new File("./data");
        FileUtils.deleteDirectory(dataDir);
        System.out.println("[testRecovery] 测试成功!!!");

    }

}

测试结果:通过

相关推荐
咩咩啃树皮1 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚2 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白3 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司4 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地5 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun3141595 小时前
TCP超时重传机制是为了解决什么问题?
java
莫逸风8 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
隔窗听雨眠8 小时前
Spring Boot在云原生时代的编程范式革新研究
spring boot·后端·云原生
z123456789868 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin5211239 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python