项目实战 — 消息队列(6){内存数据管理}

目录

一、设计数据结构

二 、实现管理方法

1、实现交换机管理

2、实现队列管理

3、实现绑定管理

        插入绑定操作

        删除绑定

4、进行消息管理

5、发送消息到指定队列

6、表示“未被确认”的消息管理

7、从硬盘上读取数据

三、测试交换机操作

1、“准备工作”和收尾工作

2、测试 交换机

3、测试 队列

4、测试 绑定

5、测试 消息的增删查

6、测试 发送消息

7、测试 “未确认”的信息 

  8、测试 从硬盘上读取数据

四、小结


一、设计数据结构

对于MQ来说,主要是以内存存储数据为主,硬盘存储数据为辅。

关于内存数据管理,作出如下的数据结构:

交换机:使用HashMap,key是name, value是Exchange对象

队列:使用HashMap,key是name,value是MSGQueue对象

绑定:使用嵌套的HashMap,key是exchangeName,value是一个HashMap(其中key是queueName,value是Binding对象)

消息:使用HashMap,key是messageId,value是Message

表示队列和消息之间的关联:使用嵌套的HashMap,key是queueName,value是一个LinkedList。LinkedList中每个元素又是一个Message对象。

表示“未被确认”的消息:关于未被确认:存储了当前队列中哪些消息被消费者取走了,但是还没有应答。使用嵌套的HashMap,key是queueName,value是HashMap(其中key是messageId,value是Message对象)。后续实现消息确认的逻辑,需要根据ack响应的内容,会提供一个messageId,根据该messageId把结构中的Message对象找到并且移除

这里有两种应答模式(ACK):

 1.自动应答,消费者取了元素,该消息就算是被应答,就可以被删除了

 2.手动应答,消费者取了元素,该消息还不断被应答, 需要消费者主动再调用一个basicAck方法,此时才被认为是真的应答了,才能删除这个消息。

 项目实战 — 消息队列(6){内存数据管理}_第1张图片

由于, 这个类会涉及到多线程的请求,所以这里的HashMap都统一使用ConcurrentHashMap,因为HashMap是线程不安全的,ConcurrentHashMap相对而来线程安全,所以上面说的使用HashMap都使用ConcurrentHashMap

public class MemoryDataCenter {
//    交换机:key是exchangeMame,value是exchange对象
    private ConcurrentHashMap exchangeMap = new ConcurrentHashMap<>();
//    队列:key表示queueName,value表示MSGQueue对象
    private ConcurrentHashMap queueMap = new ConcurrentHashMap<>();
//    绑定:第一个key表示exchangeName,第二个key表示queueName,value都表示Binding对象
    private ConcurrentHashMap> bindingsMap = new ConcurrentHashMap<>();
//    消息:key是messageId,value是Message对象
    private ConcurrentHashMap messageMap = new ConcurrentHashMap<>();
//    表示队列和消息之间的关联:key表示queueName,value表示一个Message链表,里面存放的是Message对象
    private ConcurrentHashMap> queueMessageMap = new ConcurrentHashMap<>();
//  表示“未被确认”的消息:第一个key表示queueName,第二个key表示messageId,value表示Message对象
    private ConcurrentHashMap> queueMessageWaitAckMap = new ConcurrentHashMap<>();
}


二 、实现管理方法

1、实现交换机管理

 public void insertExchange(Exchange exchange){
        exchangeMap.put(exchange.getName(),exchange);
        System.out.println("[MemoryDataCenter]新交换机添加成功!exchangeName = " + exchange.getName());
    }

    public Exchange getExchange(String exchangeName){
        return exchangeMap.get(exchangeName);
    }

    public void deleteExchange(String exchangeName){
        exchangeMap.remove(exchangeName);
        System.out.println("[MemoryDataCenter]交换机删除成功! exchangeName = " + exchangeName);
    }


2、实现队列管理

  public void insertQueue(MSGQueue queue){
        queueMap.put(queue.getName(),queue);
        System.out.println("[MemoryDataCenter]队列删除成功!queueName = " + queue.getName());
    }

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

    public void deleteQueue(String queueName){
        queueMap.remove(queueName);
        System.out.println("[MemoryDataCenter]删除队列成功!queueName = " + queueName);
    }


3、实现绑定管理

        插入绑定操作

注意点:

(1)这里是嵌套的HashMap,所以再插入钱,要先使用exchangeName查找对应的哈希表是否存在,不存在就创建。

(2)线程安全问题。插入要先判断绑定是否存在,不存在才插入。不是原子操作,要加锁。

        得到绑定

注意:这里有两个版本

        (1)根据exchangeName和queueName确定一个binding

        (2)根据exchangeName获取到所有的binding

public Binding getBinding(String exchangeName,String queueName){
        ConcurrentHashMap bindingMap = bindingsMap.get(exchangeName);
        if (bindingMap == null){
            return null;
        }
        return bindingMap.get(queueName);
    }

public ConcurrentHashMap getBindings(String exchangeName){
        return bindingsMap.get(exchangeName);
    }

        删除绑定

 public void deleteBinding(Binding binding) throws MqException {
//        现根据exchangeName找到所有的binding
        ConcurrentHashMap bindingMap = bindingsMap.get(binding.getExchangeName());
        if (bindingMap == null){
//            该交换机没有绑定任何队列,报错
            throw new MqException("[MemoryDataCenter]绑定不存在!exchangeName = " + binding.getExchangeName()
                    + ",queueName" + binding.getQueueName());
        }
        bindingMap.remove(binding.getQueueName());
        System.out.println("[MemoryDataCenter]绑定删除成功!exchangeName = " + binding.getQueueName()
                + ",queueName = " + binding.getQueueName());
    }

4、进行消息管理

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

//    根据id查询消息
    public Message getMessage(String messageId){
        return messageMap.get(messageId);
    }

//    根据id删除消息
    public void removeMessage(String messageId){
        messageMap.remove(messageId);
        System.out.println("[MemoryDataCenter]消息被移除!messageId = " + messageId);
    }


5、发送消息到指定队列

//    发送消息到指定队列
    public void sendMessage(MSGQueue queue,Message message){
//        把消息放到对应的队列数据结构中
//        现根据队列的名字,找到该队列对应的消息链表
        LinkedList messages = queueMessageMap.computeIfAbsent(queue.getName(),k -> new LinkedList<>());
//        把数据加到messages里面
        synchronized (messages){
            messages.add(message);
        }
//            把该消息往消息中心中插入,假设message已经在消息中心存在,重复插入也没有关系
//        主要就是相同messageId,对应的message内容一定是一样的
        addMessage(message);
        System.out.println("[MemoryDataCenter]消息被投递到到队列中! messageId = " + message.getMessageId());
    }

    //    从队列中取消息
    public Message pollMessage(String queueName){
//        根据队列名,查找对应的消息链表
        LinkedList messages = queueMessageMap.get(queueName);
//        如果没找到,说明队列中没有任何消息
        if (messages == null) {
            return null;
        }
        synchronized (messages){
            if (messages.size() == 0){
                return null;
            }
//        链表中铀元素,就进行头删
            Message currentMessage = messages.remove(0);
            System.out.println("[MemoryDataCenter]消息从队列中取出!messageId = " + currentMessage.getMessageId());
            return currentMessage;
        }
    }

//    获取指定队列中的消息的个数
    public int getMessageCount(String queueName){
        LinkedList messages = queueMessageMap.get(queueName);
        if (messages == null){
//            队列中没有消息
            return 0;
        }
        synchronized (messages){
            return messages.size();
        }
    }


6、表示“未被确认”的消息管理

//    添加未确认的消息
    public void addMessageWaitAck(String queueName,Message message){
        ConcurrentHashMap messageHashMap = queueMessageWaitAckMap.computeIfAbsent(queueName,
                k -> new ConcurrentHashMap<>());
        messageHashMap.put(message.getMessageId(),message);
        System.out.println("[MemoryDataCenter]消息进入待确认队列!messageId = " + message.getMessageId());
    }

//    删除之前未确认,但是现在已经确认的消息
    public void removeMessageWaitAck(String queueName,String messageId){
        ConcurrentHashMap messageHashMap = queueMessageWaitAckMap.get(queueName);
        if (messageHashMap == null){
            return;
        }
        messageHashMap.remove(messageId);
        System.out.println("[MemoryDataCenter]消息从待确认队列删除!messageId = " + messageId);
    }

//    获取指定的未确认的消息
    public Message getMessageWaitAck(String queueName,String messageId){
        ConcurrentHashMap messageHashMap = queueMessageWaitAckMap.get(queueName);
        if (messageHashMap == null){
            return null;
        }
        return messageHashMap.get(messageId);
    }


7、从硬盘上读取数据

把之前硬盘中持久化存储的各个维度的数据都恢复到内存中来,

主要是以下几步:

(1)恢复所有的交换机数据

(2)恢复所有的队列

(3)恢复所有的绑定数据

(4)恢复所有的消息数据

注意:关于“未被确认的消息”:“未被确认的消息”是内存中的数据,不需要从硬盘上恢复,一旦在等待ack的过程中,服务器重启了,此时这些“未被确认的消息”,就会恢复成“未被取走的消息”,这些消息在硬盘在硬盘在硬盘上存储的时候,就当作是“未被”取走

//    从硬盘上读取数据,
//    把之前硬盘中持久化存储的各个维度的数据都恢复到内存中来
    public void recovery(DiskDataCenter diskDataCenter) throws IOException, MqException, ClassNotFoundException {
//        先清空之前的数据
        exchangeMap.clear();
        queueMap.clear();
        bindingsMap.clear();
        messageMap.clear();
        queueMessageMap.clear();

//        1.恢复所有的交换机数据
        List exchanges = diskDataCenter.selectAllExchanges();
        for (Exchange exchange : exchanges){
            exchangeMap.put(exchange.getName(),exchange);
        }
//        2.恢复所有的队列
        List queues = diskDataCenter.selectAllQueues();
        for (MSGQueue queue : queues) {
            queueMap.put(queue.getName(),queue);
        }
//        3.恢复所有的绑定数据
        List bindings = diskDataCenter.selectAllBindings();
        for (Binding binding : bindings){
            ConcurrentHashMap bindingMap = bindingsMap.computeIfAbsent(binding.getExchangeName(),
                    k -> new ConcurrentHashMap<>());
        bindingMap.put(binding.getQueueName(), binding);
        }
//        4.恢复所有的消息数据
//        遍历所有队列,再根据每个队列的名字获取到有的消息
        for (MSGQueue queue :queues){
            LinkedList messages = diskDataCenter.loadAllMessageFromQueue(queue.getName());
            queueMessageMap.put(queue.getName(),messages);
            for (Message message : messages){
                messageMap.put(message.getMessageId(),message);
            }
        }
    }


三、测试交换机操作

1、“准备工作”和收尾工作

项目实战 — 消息队列(6){内存数据管理}_第2张图片

@SpringBootTest
public class MemoryDataCenterTests {
    private MemoryDataCenter memoryDataCenter = null;

    @BeforeEach
    public void  setUp(){
        memoryDataCenter = new MemoryDataCenter();
    }

    @AfterEach
    public void tearDown(){
        memoryDataCenter =null;
    }
}

创建以一个测试交换机和队列,以便于后面使用:

//    创建测试交换机
    private Exchange createTestExchange(String exchangeName){
        Exchange exchange = new Exchange();
        exchange.setName(exchangeName);
        exchange.setType(ExchangeType.DIRECT);
        exchange.setDurable(true);
        return exchange;
    }

//    创建一个测试队列
    private MSGQueue createTestQueue(String queueName){
        MSGQueue queue = new MSGQueue();
        queue.setName(queueName);
        queue.setDurable(true);
        return queue;
    }


//    创建一个测试消息
 public Message createTestMessage(String content){
        Message message = Message.createMessageWithId("testRoutinKey",null,content.getBytes());
        return message;
    }


2、测试 交换机

//    针对交换机进行测试
    @Test
    public void testExchange(){
//        1.先构造一个交换机并且插入
        Exchange expectedExchange = createTestExchange("testExchange");
        memoryDataCenter.insertExchange(expectedExchange);
//        2.查询这个交换机,比较结果是否一致
        Exchange actualExchange = memoryDataCenter.getExchange("testExchange");
        Assertions.assertEquals(expectedExchange,actualExchange);
//        3.删除这个交换机
        memoryDataCenter.deleteExchange("testExchange");
//        4.再次查找
        actualExchange = memoryDataCenter.getExchange("testExchange");
        Assertions.assertNull(actualExchange);
    }

项目实战 — 消息队列(6){内存数据管理}_第3张图片


3、测试 队列

 @Test
    public void testQueu(){
//        1、创建一个队列并且插入
        MSGQueue expectedQueue = createTestQueue("testQueue");
        memoryDataCenter.insertQueue(expectedQueue);
//        2、查询该队列并且比较
        MSGQueue actualQueue = memoryDataCenter.getQueue("testQueue");
        Assertions.assertEquals(expectedQueue,actualQueue);
//        3、删除这个队列
         memoryDataCenter.deleteQueue("testQueue");
//        4、再次查询看是否能够查询到
        actualQueue = memoryDataCenter.getQueue("testQueue");
        Assertions.assertNull(actualQueue);
    }
//    针对绑定进行测试
    @Test
    public void testBinding() throws MqException {
        Binding expectedBinding = new Binding();
        expectedBinding.setExchangeName("testExchange");
        expectedBinding.setQueueName("testQueue");
        expectedBinding.setBindingKey("testBindingKey");
        memoryDataCenter.insertBinding(expectedBinding);

        Binding actualBinding = memoryDataCenter.getBinding("testExchange","testQueue");
        Assertions.assertEquals(expectedBinding,actualBinding);

        ConcurrentHashMap bindingMap = memoryDataCenter.getBindings("testExchange");
        Assertions.assertEquals(1,bindingMap.size());
        Assertions.assertEquals(expectedBinding,bindingMap.get("testQueue"));

        memoryDataCenter.deleteBinding(expectedBinding);

        actualBinding = memoryDataCenter.getBinding("testExchange","testQueue");
        Assertions.assertNull(actualBinding);
    }


4、测试 绑定

//    针对绑定进行测试
    @Test
    public void testBinding() throws MqException {
        Binding expectedBinding = new Binding();
        expectedBinding.setExchangeName("testExchange");
        expectedBinding.setQueueName("testQueue");
        expectedBinding.setBindingKey("testBindingKey");
        memoryDataCenter.insertBinding(expectedBinding);

        Binding actualBinding = memoryDataCenter.getBinding("testExchange","testQueue");
        Assertions.assertEquals(expectedBinding,actualBinding);

        ConcurrentHashMap bindingMap = memoryDataCenter.getBindings("testExchange");
        Assertions.assertEquals(1,bindingMap.size());
        Assertions.assertEquals(expectedBinding,bindingMap.get("testQueue"));

        memoryDataCenter.deleteBinding(expectedBinding);

        actualBinding = memoryDataCenter.getBinding("testExchange","testQueue");
        Assertions.assertNull(actualBinding);
    }

项目实战 — 消息队列(6){内存数据管理}_第4张图片


5、测试 消息的增删查

@Test
    public void testMessage(){
        Message expectedMessage = createTestMessage("testMessage");
        memoryDataCenter.addMessage(expectedMessage);

        Message actualMessage = memoryDataCenter.getMessage(expectedMessage.getMessageId());
        Assertions.assertEquals(expectedMessage,actualMessage);

        memoryDataCenter.removeMessage(expectedMessage.getMessageId());
        actualMessage = memoryDataCenter.getMessage(expectedMessage.getMessageId());
        Assertions.assertNull(actualMessage);
    }


6、测试 发送消息

//    测试发送消息
    @Test
    public void testSendMessage(){
//        1、创建一个队列,创建十条消息,把这些消息都插入到队列中
        MSGQueue queue = createTestQueue("testQueue");
        List expectedMessages = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Message message = createTestMessage("testMessage" + 1);
            memoryDataCenter.sendMessage(queue,message);
            expectedMessages.add(message);
        }
//        2、从队列中取出这些消息
        List actualMessages = new ArrayList<>();
        while (true){
            Message message = memoryDataCenter.pollMessage("testQueue");
            if (message == null){
                break;
            }
            actualMessages.add(message);
        }
//        3、比较取出的消息和之前的消息是否一致
        Assertions.assertEquals(expectedMessages.size(),actualMessages.size());

        for (int i = 0; i < expectedMessages.size(); i++) {
            Assertions.assertEquals(expectedMessages.get(i),actualMessages.get(i));
        }
    }


7、测试 “未确认”的信息 

  @Test
    public void testMessageWaitAck(){
        Message expectedMessage = createTestMessage("expectedMessage");
        memoryDataCenter.addMessageWaitAck("testQueue",expectedMessage);

        Message actualMessage = memoryDataCenter.getMessageWaitAck("testQueue",expectedMessage.getMessageId());
        Assertions.assertEquals(expectedMessage,actualMessage);

        memoryDataCenter.removeMessageWaitAck("testQueue",expectedMessage.getMessageId());
        actualMessage = memoryDataCenter.getMessageWaitAck("testQueue",expectedMessage.getMessageId());
        Assertions.assertNull(actualMessage);
    }


  8、测试 从硬盘上读取数据

 测试这个用例,主要分为3步:

        (1)在硬盘上构造好数据

        (2)执行恢复操作

        (3)对比结果

@Test
    public void testRecovery() throws IOException, MqException, ClassNotFoundException {
        //        后续操作雨MyBatis有关,所以需要启动SpringApplication
        TigerMqApplication.context = SpringApplication.run(TigerMqApplication.class);

        // 1. 在硬盘上构造好数据
        DiskDataCenter diskDataCenter = new DiskDataCenter();
        diskDataCenter.init();

        // 构造交换机
        Exchange expectedExchange = createTestExchange("testExchange");
        diskDataCenter.insertExchange(expectedExchange);

        // 构造队列
        MSGQueue expectedQueue = createTestQueue("testQueue");
        diskDataCenter.insertQueue(expectedQueue);

        // 构造绑定
        Binding expectedBinding = new Binding();
        expectedBinding.setExchangeName("testExchange");
        expectedBinding.setQueueName("testQueue");
        expectedBinding.setBindingKey("testBindingKey");
        diskDataCenter.insertBinding(expectedBinding);

        // 构造消息
        Message expectedMessage = createTestMessage("testContent");
        diskDataCenter.sendMessage(expectedQueue, expectedMessage);

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

        // 3. 对比结果
        Exchange actualExchange = memoryDataCenter.getExchange("testExchange");
        Assertions.assertEquals(expectedExchange.getName(), actualExchange.getName());
        Assertions.assertEquals(expectedExchange.getType(), actualExchange.getType());
        Assertions.assertEquals(expectedExchange.isDurable(), actualExchange.isDurable());
        Assertions.assertEquals(expectedExchange.isAutoDelete(), actualExchange.isAutoDelete());

        MSGQueue actualQueue = memoryDataCenter.getQueue("testQueue");
        Assertions.assertEquals(expectedQueue.getName(), actualQueue.getName());
        Assertions.assertEquals(expectedQueue.isDurable(), actualQueue.isDurable());
      

        Binding actualBinding = memoryDataCenter.getBinding("testExchange", "testQueue");
        Assertions.assertEquals(expectedBinding.getExchangeName(), actualBinding.getExchangeName());
        Assertions.assertEquals(expectedBinding.getQueueName(), actualBinding.getQueueName());
        Assertions.assertEquals(expectedBinding.getBindingKey(), actualBinding.getBindingKey());

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

        // 4. 清理硬盘的数据, 把整个 data 目录里的内容都删掉(包含了 meta.db 和 队列的目录).
        TigerMqApplication.context.close();
        File dataDir = new File("./data");
        FileUtils.deleteDirectory(dataDir);
    }

项目实战 — 消息队列(6){内存数据管理}_第5张图片 


 四、小结

  这一块的内容,主要就是借助内存中的一系列数据结构,保存、管理交换机、队列、绑定、消息,使用到了哈希表、链表、嵌套的结构等。

这里还频繁的使用了加锁的操作,具体场景考虑是否要加锁(特别是有插入操作)。

你可能感兴趣的:(项目,java,开发语言,项目,消息队列)