根据源码,模拟实现 RabbitMQ - 虚拟主机 + Consume设计 (7)

news2025/6/19 8:17:50

目录

一、虚拟主机 + Consume设计

1.1、承接问题

1.2、具体实现

1.2.1、消费者订阅消息实现思路

1.2.2、消费者描述自己执行任务方式实现思路

1.2.3、消息推送给消费者实现思路

1.2.4、消息确认


一、虚拟主机 + Consume设计


1.1、承接问题

前面已经实现了虚拟主机大部分功能以及转发规则的判定,也就是说,现在消息已经可以通过 转换机 根据对应的转发规则发送给对应的 队列 了.

那么接下来要解决的问题就是,消费者该如何订阅消息(队列),如何把消息推送给消费者,以及消费者如何描述自己怎么执行任务~

1.2、具体实现

1.2.1、消费者订阅消息实现思路

消费者是以队列为维度订阅消息的,并且一个队列可以被多个消费者订阅,那么一旦队列中有消息,这个消息到底因该给谁呢?此处就约定,消费者之间按照 “轮询” 的方式来进行消费.

这里我们就需要定义一个类(ConsumerEnv),用来描述一个消费者,如下

public class ConsumerEnv {
    private String consumerTag;
    private String queueName;
    private boolean autoAck;
    //通过这个回调来处理收到的消息
    private Consumer consumer;

    public ConsumerEnv(String consumerTag, String queueName, boolean autoAck, Consumer consumer) {
        this.consumerTag = consumerTag;
        this.queueName = queueName;
        this.autoAck = autoAck;
        this.consumer = consumer;
    }

    public String getConsumerTag() {
        return consumerTag;
    }

    public void setConsumerTag(String consumerTag) {
        this.consumerTag = consumerTag;
    }

    public String getQueueName() {
        return queueName;
    }

    public void setQueueName(String queueName) {
        this.queueName = queueName;
    }

    public boolean isAutoAck() {
        return autoAck;
    }

    public void setAutoAck(boolean autoAck) {
        this.autoAck = autoAck;
    }

    public Consumer getConsumer() {
        return consumer;
    }

    public void setConsumer(Consumer consumer) {
        this.consumer = consumer;
    }
}

 

再给每个队列对象(MSGQueue 对象)添加一个属性 List,用来包含若干个上述消费者(有哪些消费者订阅了当前队列),如下图:

    //当前队列都有哪些消费者订阅了
    private List<ConsumerEnv> consumerEnvList = new ArrayList<>();
    //记录当取到了第几个消费者(AtomicInteger 是线程安全的)
    private AtomicInteger consumerSeq = new AtomicInteger(0);

    /**
     * 添加一个新的订阅者
     * @param consumerEnv
     */
    public void addConsumerEnv(ConsumerEnv consumerEnv) {
        consumerEnvList.add(consumerEnv);
    }

    /**
     * 删除订阅者暂时先不考虑
     */

    /**
     * 挑选一个订阅者,来处理当前的消息(按照轮询的方式)
     * @return
     */
    public ConsumerEnv chooseConsumer() {
        if(consumerEnvList.size() == 0) {
            //该队列暂时没有人订阅
            return null;
        }
        //计算当前要取的下标
        int index = consumerSeq.get() % consumerEnvList.size();
        consumerSeq.getAndIncrement();// 自增
        return consumerEnvList.get(index);
    }

VirtualHost 中订阅消息实现

    /**
     * 订阅消息
     * 添加一个队列的订阅者,当队列收到消息之后,就要把消息推送给对应的订阅者
     * @param consumerTag 消费者的身份标识
     * @param queueName
     * @param autoAck 消息被消费之后,应答的方式,true 标识自动应答,false 标识手动应答
     * @param consumer 是一个回调函数,此处设定成函数式接口,这样后续调用 basicConsume 并且传实参的时候,就可以写作 lambda 样子了
     * @return
     */
    public boolean basicConsume(String consumerTag, String queueName, boolean autoAck, Consumer consumer) {
        //构造一个 ConsumerEnv 对象,把这个对应的队列找到,再把 Consumer 对象添加到队列中
        queueName = virtualHostName + queueName;
        try {
            consumerManager.addConsumer(consumerTag, queueName, autoAck, consumer);
            System.out.println("[VirtualHost] basicConsume 成功! queueName=" + queueName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("[VirtualHost] basicConsume 失败! queueName=" + queueName);
            return false;
        }
    }

1.2.2、消费者描述自己执行任务方式实现思路

当执行订阅消息的时候,我们就让消费者自己去实现处理消息的操作(消息的内容通过参数传递,具体要干啥,取决于消费者自己的业务路基),最后再让线程池来执行回调函数.

这里我们使用函数式接口(回调函数)的方式(lambda 表达式),让消费者在订阅消息的时候,就可以实现未来收到消息后如何去处理消息的操作.

@FunctionalInterface
public interface Consumer {

    /**
     * Delivery 的意思是 ”投递“,这个方法预期是在服务器收到消息之后来调用
     * 通过这个方法,把消息推送给对应的消费者
     * (注意,这里的方法名和参数,也都是参考 RabbitMQ 来展开的)
     * @param consumerTag
     * @param basicProperties
     * @param body
     */
    void handlerDelivery(String consumerTag, BasicProperties basicProperties, byte[] body);


}

为什么要这样实现?

一方面,这种思路也是参考 RabbitMQ。

另一方面,这是由于Java 的函数是不能脱离类存在的,为了实现这种 lambda,java 曲线救国,引入 函数式接口.

对于函数式接口来说:

  1. 首先是 interface 类型
  2. 只能有一个方法
  3. 添加 @FunctionalInterface 注解.

实际上,这也是 lambda 的底层实现(本质)

1.2.3、消息推送给消费者实现思路

这里我们可以添加一个扫描线程,让他来去队列中拿任务.

为什么用了扫描线程还需要用线程池?

如果就一个扫描线程,既要获取消息,又要执行回调,这一个线程可能会忙不过来,因为消费者给出的回调,具体干什么的,咱们是不知道的.

扫描线程怎么知道哪个队列来了新的消息?

  1. 一个简单粗暴的办法,就是直接让扫描线程不停的循环遍历所有队列,发现有元素就立即处理。
  2. 另一个更优雅的办法(我采取的办法),就是用一个阻塞队列,队列中的元素就是接收消息的队列的名字,扫描线程只需要盯住这一个阻塞对垒即可,此时阻塞队列中传递的队列名,就相当于 “令牌”

每次拿到令牌,才能调动一次军队,也就是从对应的队列中取一个消息.

具体的,实现一个 ConsumerManager 类,用来管理消费者的上述行为.

public class ConsumerManager {
    // 持有上层的 VirtualHost 对象的引用,用来操作数据
    private VirtualHost parent;
    // 指定一个线程池,负责取执行具体的回调任务
    private ExecutorService workerPool = Executors.newFixedThreadPool(4);
    //存放令牌的队列
    private BlockingQueue<String> tokenQueue = new LinkedBlockingQueue<>();
    //扫描线程
    private Thread scannerThread = null;


    /**
     * 初始化
     * @param parent
     */
    public ConsumerManager(VirtualHost parent) {
        this.parent = parent;

        //创建扫描线程,取队列中消费消息
        scannerThread = new Thread(() -> {
            while(true) {
                try {
                    //1.拿到令牌
                    String queueName = tokenQueue.take();
                    //2.根据令牌,找到队列
                    MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);
                    if(queue == null) {
                        throw new MqException("[ConsumerManager] 取到令牌后发现,该队列名不存在!queueName=" + queueName);
                    }
                    //3.从这个队列中消费一个消息
                    synchronized (queue) {
                        consumeMessage(queue);
                    }
                } catch (InterruptedException | MqException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //设置为后台线程
        scannerThread.setDaemon(true);
        scannerThread.start();
    }

    public void notifyConsume(String queueName) throws InterruptedException {
        tokenQueue.put(queueName);
    }

    /**
     * 添加消费者
     * 找到对应队列的 List 列表, 把消费者添加进去,最后判断,如果有消息,就立刻消费
     * @param consumerTag 消费者身份标识
     * @param queueName
     * @param autoAck 消息被消费之后,应答的方式,true 标识自动应答,false 标识手动应答
     * @param consumer 是一个回调函数,此处设定成函数式接口,这样后续调用 basicConsume 并且传实参的时候,就可以写作 lambda 样子了
     * @throws MqException
     */
    public void addConsumer(String consumerTag, String queueName, boolean autoAck, Consumer consumer) throws MqException {
        //找到对应的队列
        MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);
        if(queue == null) {
            throw new MqException("[ConsumerManager] 队列不存在! queueName=" + queueName);
        }
        ConsumerEnv consumerEnv = new ConsumerEnv(consumerTag, queueName, autoAck, consumer);
        synchronized (queue) {
            queue.addConsumerEnv(consumerEnv);
            //如果当前队列中已经有一些消息了,需要立即消费掉
            int n = parent.getMemoryDataCenter().getMessageCount(queueName);
            for(int i = 0; i < n; i++) {
                //这个方法调用一次就消费一条消息
                consumeMessage(queue);
            }
        }
    }

    /**
     * 扫描线程:找到对应的队列后,消费者从队列中拿出消息并消费
     * @param queue
     */
    private void consumeMessage(MSGQueue queue) {
        //1.按照轮询的方式,找个消费者出来
        ConsumerEnv luckDog = queue.chooseConsumer();
        if(luckDog == null) {
            //当前队列中没有消费者,暂时不用消费,等后面有消费者了再说
            return;
        }
        //2.从队列中取出一个消息
        Message message = parent.getMemoryDataCenter().pollMessage(queue.getName());
        if(message == null) {
            //当前队列中还没有消息,也不需要消费
            return;
        }
        //3.把消息带入到消费者的回调方法中,丢给线程池执行
        workerPool.submit(() -> {
            try {
                //1.把消息放到待确认的集合当中,这个操作一定要在执行回调之前(防止执行回调过程中出现异常,导致消息丢失)
                parent.getMemoryDataCenter().addMessageWaitAck(luckDog.getQueueName(), message);
                //2.真正执行回调操作
                luckDog.getConsumer().handlerDelivery(luckDog.getConsumerTag(), message.getBasicProperties(),
                        message.getBody());
                //3.如果当前是 ”自动应答“ ,就可以直接把消息删除了
                //  如果当前是 ”手动应答“ ,则先不处理,交给后续消费者调用 basicAck 方法来处理
                if(luckDog.isAutoAck()) {
                    //1) 删除硬盘上的消息
                    if(message.getDeliverMode() == 2) {
                        parent.getDiskDataCenter().deleteMessage(queue, message);
                    }
                    //2) 删除上面的待确认集合中的消息
                    parent.getMemoryDataCenter().removeMessageWaitAck(queue.getName(), message.getMessageId());
                    //3) 删除内存上的消息中心的消息
                    parent.getMemoryDataCenter().removeMessage(message.getMessageId());
                    System.out.println("[ConsumerManager] 消息被成功消费!queueName=" + queue.getName());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

1.2.4、消息确认

消息确认,就是保证消息被正确消费~~ 

正确消费就是指消费者的回调方法顺利执行完了(没有抛异常之类的),这条消息的使命就完成了,此时就可以删除了。

为了达成消息不丢失这样的效果,具体步骤如下:

  1. 在真正执行回调之前,把消息放到 “待确认的集合” 中,避免应为回调失败,导致消息丢失.
  2. 执行回调
  3. 当去消费者采取的是 autoAck=true ,就认为回调执行完毕不抛异常,就算消费成功,然后就可以删除消息了
    1. 硬盘
    2. 内存中的消息中心
    3. 待确认的消息集合
  4. 当前消费者若采取的是 autoAck=false,手动应答,需要消费者这边,在自己的回调方法内部,显式调用 basicAck 这个核心 API 表示应答.

 basicAck 完成主动应答

    /**
     * 确认消息
     * 各个维度删除消息即可
     * @param queueName
     * @param messageId
     * @return
     */
    public boolean basicAck(String queueName, String messageId) {
        queueName = virtualHostName + queueName;
        try {
            //1.获取消息和队列
            MSGQueue queue = memoryDataCenter.getQueue(queueName);
            if(queue == null) {
                throw new MqException("[VirtualHost] 要确认的队列不存在!queueName=" + queueName);
            }
            Message message = memoryDataCenter.getMessage(messageId);
            if(message == null) {
                throw new MqException("[VirtualHost] 要确认的消息不存在!messageId=" + messageId);
            }
            //2.各个维度删除消息
            if(message.getDeliverMode() == 2) {
                diskDataCenter.deleteMessage(queue, message);
            }
            memoryDataCenter.removeMessage(messageId);
            memoryDataCenter.removeMessageWaitAck(queueName, messageId);
            System.out.println("[VirtualHost] basicAck 成功,消息确认成功!queueName=" + queueName +
                    ", messageId=" + messageId);
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] basicAck 失败,消息确认失败!queueName=" + queueName +
                    ", messageId=" + messageId);
            e.printStackTrace();
            return false;
        }
    }

扫描线程完成自动应答

    /**
     * 扫描线程:找到对应的队列后,消费者从队列中拿出消息并消费
     * @param queue
     */
    private void consumeMessage(MSGQueue queue) {
        //1.按照轮询的方式,找个消费者出来
        ConsumerEnv luckDog = queue.chooseConsumer();
        if(luckDog == null) {
            //当前队列中没有消费者,暂时不用消费,等后面有消费者了再说
            return;
        }
        //2.从队列中取出一个消息
        Message message = parent.getMemoryDataCenter().pollMessage(queue.getName());
        if(message == null) {
            //当前队列中还没有消息,也不需要消费
            return;
        }
        //3.把消息带入到消费者的回调方法中,丢给线程池执行
        workerPool.submit(() -> {
            try {
                //1.把消息放到待确认的集合当中,这个操作一定要在执行回调之前(防止执行回调过程中出现异常,导致消息丢失)
                parent.getMemoryDataCenter().addMessageWaitAck(luckDog.getQueueName(), message);
                //2.真正执行回调操作
                luckDog.getConsumer().handlerDelivery(luckDog.getConsumerTag(), message.getBasicProperties(),
                        message.getBody());
                //3.如果当前是 ”自动应答“ ,就可以直接把消息删除了
                //  如果当前是 ”手动应答“ ,则先不处理,交给后续消费者调用 basicAck 方法来处理
                if(luckDog.isAutoAck()) {
                    //1) 删除硬盘上的消息
                    if(message.getDeliverMode() == 2) {
                        parent.getDiskDataCenter().deleteMessage(queue, message);
                    }
                    //2) 删除上面的待确认集合中的消息
                    parent.getMemoryDataCenter().removeMessageWaitAck(queue.getName(), message.getMessageId());
                    //3) 删除内存上的消息中心的消息
                    parent.getMemoryDataCenter().removeMessage(message.getMessageId());
                    System.out.println("[ConsumerManager] 消息被成功消费!queueName=" + queue.getName());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

如果在回调方法中抛异常了?

回调方法中抛异常了,后续逻辑执行不到,这个消息就会始终呆在待确认的集合中, RabbitMQ 的做法是另外搞一个扫描线程(其实 RabbitMQ 中不叫线程,人家是叫进程,但是注意,这个进程不是操作系统中的进程,而是 erlang 中的概念),负责关注这个 待确认集合中,每个消息待了多久了,如果超出了一定的时间范围,就会把这个消息放到一个特定的队列 —— “死信队列”(这里就不展示了,需要的可以私聊我)

如果在执行回调过程中,broker server 崩了,内存数据全没了?

此时硬盘的数据还在,broker server 重启之后,这个消息就又被加载回内存了,就像从来没有被消费过一样,消费者就又机会重新拿到这个消息,重新消费(重复消费的问题,是由消费者的业务代码负责保证的,broker server 管不了).

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/914666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vim学习(四)——命令使用技巧

命令模式 打开文本默认模式&#xff0c;按**【ESC】**重新进入 【/关键字】&#xff1a;搜索匹配关键字 G&#xff1a;最后一行 gg&#xff1a;第一行 hjkl:左下右上 yy: 复制一行 dd&#xff1a;删除一行 p:粘贴 u: 撤销插入模式 按**【i / a / o】**键均可进入文本编辑模式…

2023年国赛 高教社杯数学建模思路 - 案例:退火算法

文章目录 1 退火算法原理1.1 物理背景1.2 背后的数学模型 2 退火算法实现2.1 算法流程2.2算法实现 建模资料 ## 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 退火算法原理 1.1 物理背景 在热力学上&a…

STM32f103c6t6/STM32f103c8t6寄存器开发

目录 资料 寻址区 2区 TIMx RTC WWDG IWDG SPI I2S USART I2C USB全速设备寄存器 bxCAN BKP PWR DAC ADC ​编辑 EXTI ​编辑 GPIO AFIO SDIO DMA CRC RCC FSMC USB_OTG ETH&#xff08;以太网&#xff09; 7区 配置流程 外部中断 硬件中断 例子 点灯 …

【Jenkins】rpm方式安装Jenkins(2.401,jdk版本17)

目录 【Jenkins】rpm方式安装Jenkins 1、主机初始化 2、软件要求 RPM包安装的内容 配置文件说明 3、web操作 【Jenkins】rpm方式安装Jenkins 1、主机初始化 [rootlocalhost ~]# hostname jenkins[rootlocalhost ~]# bash[rootjenkins ~]# systemctl stop firewalld[roo…

Ribbon:使用Ribbon实现负载均衡

Ribbon实现的是实线走的 建立三个数据库 /* SQLyog Enterprise v12.09 (64 bit) MySQL - 5.7.25-log : Database - db01 ********************************************************************* *//*!40101 SET NAMES utf8 */;/*!40101 SET SQL_MODE*/;/*!40014 SET OLD_UNIQ…

RabbitMQ---基本消息模型

1、 基本消息模型 官方介绍&#xff1a; RabbitMQ是一个消息代理&#xff1a;它接受和转发消息。 你可以把它想象成一个邮局&#xff1a;当你把邮件放在邮箱里时&#xff0c;你可以确定邮差先生最终会把邮件发送给你的收件人。 在这个比喻中&#xff0c;RabbitMQ是邮政信箱&a…

shell脚本——循环语句、sed、函数、数组、免交互expect

目录 循环语句 for while 与 until sed 基本用法 sed脚本格式 函数 注意事项 定义函数和调用函数 脚本中函数的位置 查看函数 删除函数 函数返回值 函数的传参操作 使用函数文件 递归函数 数组 声明数组 数组切片 免交互expect 定义 基本命令 循环语句 …

学习笔记|认识蜂鸣器|控制原理|电磁炉LED实战|逻辑运算|STC32G单片机视频开发教程(冲哥)|第八集(上):蜂鸣器应用

文章目录 1.认识蜂鸣器区别 2.控制原理实现蜂鸣器控制原理 3.蜂鸣器实战应用需求分析代码编写步骤一代码编写及分析test.h的固定模板Tips:提示&#xff1a;“test\test.c(14): error C16: unprintable character 0xA3 skippedTips&#xff1a;“test\test.c(14): warning C137:…

Mysql with as定义子查询

文章目录 1. 定义2. 适用场景3. 语法4. 示例 1. 定义 使用with as 可以让子查询重用相同的with查询块&#xff0c; 并在select查询块中直接引用&#xff0c; 一般用在select查询块会多次使用某个查询sql时&#xff0c; 会把这个sql语句放在with as 中&#xff0c; 作为公用的表…

使用威胁搜寻增加网络安全

什么是威胁搜寻 威胁搜寻&#xff08;也称为网络威胁搜寻&#xff09;是一种主动网络安全方法&#xff0c;涉及主动搜索隐藏的威胁&#xff0c;例如组织网络或系统内的高级持续性威胁和入侵指标。威胁搜寻的主要目标是检测和隔离可能绕过网络外围防御的威胁&#xff0c;使管理…

代码随想录算法训练营第四十二天 | 01背包问题,01背包问题(滚动数组),416. 分割等和子集

代码随想录算法训练营第四十二天 01背包问题01 背包二维dp数组01背包 01背包问题(滚动数组)416. 分割等和子集 01背包问题 视频讲解 以下是几种背包&#xff0c;如下&#xff1a; 至于背包九讲其其他背包&#xff0c;面试几乎不会问&#xff0c;都是竞赛级别的了&#xff0c;…

Ribbon:自定义负载均衡

自定义负载均衡算法 package com.kuang.myconfig;import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server;import java.util.…

Java网络编程(二)NIO和Netty实现多人聊天功能

NIO实现 服务端 package com.bierce.io; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.nio.charset.Charset; import java.util.Iterator; import java.util.Set; //服务器端 publ…

基于Python科研论文绘制学习 - task2

Matplotlib 1、subplot&#xff08;&#xff09; matplotlib.pyplot模块提供了一个 subplot() 函数&#xff0c;它可以均等地划分画布&#xff0c;该函数的参数格式如下&#xff1a; plt.subplot(nrows, ncols, index) nrows 与 ncols 表示要划分几行几列的子区域&#xff0…

Jmeter性能 —— 事务控制器

统计性能测试结果一定会关注TPS&#xff0c;TPS表示&#xff1a;每秒处理事务数&#xff0c;JMeter默认每个事务对应一个请求。我们可以用逻辑控制器中的事务控制器将多个请求统计为一个事务。 1、添加事务控制器 2、事务控制器参数说明 Generate parent sample&#xff1a;如…

Spring Framework

文章目录 一 Spring Framework简介 1.1 Spring Framework系统架构 1.2 对spring的理解 1.3 设计理念 二 核心 1. IoC 容器 1.1. Spring IoC容器和Bean简介 1.2. 容器概述 1.2.1. 配置元数据 1.2.2. 实例化一个容器 构建基于XML的配置元数据 Groovy Bean Definitio…

传递函数零极点对系统的影响

传递函数的零点和极点分别对系统的影响的详细介绍&#xff1a; 零点&#xff08;Zero&#xff09;的影响&#xff1a;传递函数的零点是使得传递函数的分子为零的点。零点对系统的频率响应和稳定性产生影响。具体而言&#xff1a; 频率响应&#xff1a;零点的位置会影响系统在不…

Linux常用配置(持续记录)

写在前面&#xff1a;学的东西太多&#xff0c;一个健忘的程序猿&#xff0c;只记得那啥那啥&#xff0c;这东西好像是这个&#xff0c;哎&#xff0c;又忘了。现在就记在这个小本本上&#xff0c;方便日后来回顾。 全局配置host 命令&#xff1a; vim /etc/hosts 作用&#…

使用EventLog Analyzer 进行路由器监控

路由器是任何计算机网络的构建块&#xff0c;引导网络中的流量&#xff0c;管理员需要确保路由器已配置并正常工作&#xff0c;以确保网络安全。 监控路由器中的用户活动 在网络安全方面&#xff0c;与路由器相关的风险是一个严重的问题。具有松散安全策略的网络使入侵者可以…

PCI9054入门1:硬件引脚定义、时序、FPGA端驱动源码

文章目录 1&#xff1a;PCI9054的FPGA侧&#xff08;local侧引脚定义&#xff09;2&#xff1a;PCI9054的C模式下的读写时序3&#xff1a;FPGA代码部分具体代码&#xff1a; 1&#xff1a;PCI9054的FPGA侧&#xff08;local侧引脚定义&#xff09; 而PCI9054的本地总线端的主要…