Basado en el código fuente, simule e implemente RabbitMQ: diseño de host virtual (5)

Tabla de contenido

1. Diseño de host virtual

1.1 Análisis de la demanda

1.1.1 API principal

1.1.2 ¿Para qué se utiliza el host virtual?

1.1.3 ¿Cómo expresar la afiliación entre el switch y el host virtual?

2. Implementar la clase VirtualHost

2.1 Propiedades

2.2 Bloquear objeto

2.3 Ejemplos públicos

2.4 Método de construcción del host virtual

2.5 Operaciones relacionadas con el interruptor

2.5 Operaciones relacionadas con la cola

2.6 Operaciones vinculantes

2.7 Operaciones relacionadas con mensajes

2.8 Problemas de seguridad de subprocesos complementarios


1. Diseño de host virtual


1.1 Análisis de la demanda

1.1.1 API principal

El concepto de host virtual es similar a la base de datos MySQL, que aísla lógicamente conmutadores, colas, enlaces, mensajes... por lo que no solo necesita administrar datos, sino que también debe proporcionar algunas API centrales para que las llame el código de la capa superior.

API principal (que consiste en conectar la gestión de datos previamente escrita en la memoria y la gestión de datos en el disco duro):

  1. Crear intercambio de interruptorDeclarar
  2. eliminar switchexchagneEliminar
  3. Crear colacolaDeclarar
  4. Eliminar colacolaEliminar
  5. Crear cola de enlaceBind
  6. Eliminar cola de enlaceDesvincular
  7. Enviar mensajebásicoPublicar
  8. Suscríbete a mensajesbasicCosume
  9. Mensaje de confirmación basicAck

1.1.2 ¿Para qué se utiliza el host virtual?

El propósito de los hosts virtuales es garantizar el aislamiento y el contenido entre diferentes hosts virtuales no debería verse afectado.

Por ejemplo, se crea un intercambio en el host virtual 1, llamado "testExchange", y también se crea un intercambio en el host virtual 2, llamado "testExchange", aunque tienen el mismo nombre, son intercambios en espacios diferentes.

 Desde la perspectiva de la persona que llama, aunque los dos intercambios se denominaron testExchange cuando se crearon, se procesan automáticamente dentro del host virtual y tienen el prefijo "virtualHost1testExchange" y "virtualHost2testExchange".

De esta manera, se pueden distinguir diferentes conmutadores y se pueden distinguir aún más diferentes colas (un conmutador corresponde a varias colas). Además, los enlaces están aislados (los enlaces están relacionados con conmutadores y colas) y otros mensajes están fuertemente relacionados con las colas. Los mensajes se distinguen naturalmente.

1.1.3 ¿Cómo expresar la afiliación entre el switch y el host virtual?

Solución 1: consulte el diseño de la base de datos, solución "uno a muchos", como agregar un atributo a la tabla de conmutación, ID/nombre del host virtual...

Solución 2 (la estrategia adoptada por RabbitMQ): Reacuerdo, el nombre del conmutador = el nombre del host virtual + el nombre del conmutador

Opción 3: una forma más elegante es asignar un conjunto diferente de bases de datos y archivos a cada host virtual, pero es un poco problemático.

Aquí seguimos la estrategia adoptada por RabbitMQ ~~

2. Implementar la clase VirtualHost


2.1 Propiedades

    private String virtualHostName;
    private MemoryDataCenter memoryDataCenter = new MemoryDataCenter();
    private DiskDataCenter diskDataCenter = new DiskDataCenter();
    private Router router;

La clase Router describe si enrutamientoKey y vinculanteKey son legales, así como las reglas de coincidencia entre mensajes y colas.

2.2 Bloquear objeto

Se utiliza para manejar problemas de seguridad de subprocesos en operaciones de conmutación, operaciones de cola, operaciones de enlace y operaciones de mensajes.

Por ejemplo: operaciones de bloqueo para operaciones de subprocesos múltiples "agregar interruptor" y "agregar mientras se elimina" (la eliminación de subprocesos múltiples al mismo tiempo en realidad no es nada, porque se elimina mediante la eliminación de SQL, incluso si se elimina varias veces, no hay efectos secundarios)

    //交换机锁对象
    private final Object exchangeLocker = new Object(); //final 修饰是为了防止程序员修改引用,导致不是一个锁对象(其实可有可无,自己注意就 ok)
    //队列锁对象
    private final Object queueLocker = new Object();

2.3 Ejemplos públicos

Esto permite que el código de la capa superior obtenga el nombre del host virtual, el centro de procesamiento de datos de la memoria y el centro de procesamiento de datos del disco duro, respectivamente, al llamar.


    public String getVirtualHostName() {
        return virtualHostName;
    }

    public MemoryDataCenter getMemoryDataCenter() {
        return memoryDataCenter;
    }

    public DiskDataCenter getDiskDataCenter() {
        

2.4 Método de construcción del host virtual

Realiza principalmente operaciones de inicialización en el nombre del host, el disco duro y el centro de procesamiento de datos de la memoria.

    public VirtualHost(String name) {
        this.virtualHostName = name;
        //对于 MemoryDataCenter 来说,不需要进行初始化工作,只需要 new 出来即可
        //但是,对于 DiskDataCenter 来说,需要进行初始化操作,建库建表和初始数据的设定
        diskDataCenter.init();

        //另外还需要针对硬盘初始化的数据,恢复到内存中
        try {
            memoryDataCenter.recovery(diskDataCenter);
        } catch (IOException | MqException | ClassNotFoundException e) {
            e.printStackTrace();
            System.out.println("[VirtualHost] 恢复内存数据失败!");
        }
    }

2.5 Operaciones relacionadas con el interruptor

Crear un switch: Obtenga el switch correspondiente de la memoria, si no existe, créelo, si existe, devuélvalo directamente.

Eliminar el interruptor: Primero verifique si el interruptor existe. Si no existe, se generará una excepción. Si existe, elimínelo.

Ps: el valor de retorno es de tipo booleano, verdadero indica éxito, falso indica fracaso


    /**
     * 创建交换机
     * @param exchangeName
     * @param exchangeType
     * @param durable
     * @param autoDelete
     * @param arguments
     * @return
     */
    public boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType, boolean durable, boolean autoDelete,
                                   Map<String, Object> arguments) {
        //1.把交换机加上虚拟主机的名字作为前缀(虚拟主机的隔离效果)
        exchangeName = virtualHostName + exchangeName;
        try {
            synchronized (exchangeLocker) {
                //2.通过内存查询交换机是否存在
                Exchange exchange = memoryDataCenter.getExchange(exchangeName);
                if(exchange != null) {
                    //已经存在就直接返回
                    System.out.println("[VirtualHost] 交换机已经存在!exchangeName=" + exchangeName);
                    return true;
                }
                //3.不存在就创建一个
                exchange = new Exchange();
                exchange.setName(exchangeName);
                exchange.setType(exchangeType);
                exchange.setDurable(durable);
                exchange.setAutoDelete(autoDelete);
                exchange.setArguments(arguments);

                //4.内存中是一定要创建的,硬盘中是否存储,就要传来的参数 durable 是否为 true 了(持久化)
                if(durable) {
                    diskDataCenter.insertExchange(exchange);
                }
                memoryDataCenter.insertExchange(exchange);
                System.out.println("[VirtualHost] 交换机创建成功!exchangeName=" + exchangeName);
                //上述逻辑中,先写硬盘,后写内存,就是因为硬盘更容易写失败,如果硬盘写失败了,内存就不用写了
                //但要是先写内存,一旦内存写成功了,硬盘写失败了,还需要把内存中的数据给删掉,比较麻烦
            }
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] 交换机创建失败!exchangeName=" + exchangeName);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除交换机
     * @param exchangeName
     * @return
     */
    public boolean exchangeDelete(String exchangeName) {
        //1.虚拟主机前缀
        exchangeName = virtualHostName + exchangeName;
        try {
            synchronized (exchangeLocker) {
                //2.检查当前内存中是否存在该交换机
                Exchange exchange = memoryDataCenter.getExchange(exchangeName);
                if(exchange == null) {
                    throw new MqException("[VirtualHost] 该交换机不存在无法删除!exchangeName=" + exchangeName);
                }
                //3.如果持久化到硬盘上,就把硬盘的先删除了
                if(exchange.isDurable()) {
                    diskDataCenter.deleteExchange(exchangeName);
                }
                //4.存在就删除
                memoryDataCenter.deleteExchange(exchangeName);
                System.out.println("[VirtualHost] 交换机删除成功!exchangeName=" + exchangeName);
            }
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] 交换机删除失败!exchangeName=" + exchangeName);
            e.printStackTrace();
            return false;
        }
    }

2.5 Operaciones relacionadas con la cola

La lógica de funcionamiento aquí es similar a la de un interruptor ~~

    /**
     * 创建队列
     * @param queueName
     * @param durable
     * @param exclusive
     * @param autoDelete
     * @param arguments
     * @return
     */
    public boolean queueDeclare(String queueName, boolean durable, boolean exclusive, boolean autoDelete,
                                Map<String, Object> arguments) {
        //1.虚拟主机前缀
        queueName = virtualHostName + queueName;
        try {
            synchronized (queueLocker) {
                //2.先检查内存是否存在此队列
                MSGQueue queue = memoryDataCenter.getQueue(queueName);
                if(queue != null) {
                    System.out.println("[VirtualHost] 当前队列已存在!queueName=" + queueName);
                    return true;
                }
                //3.不存在则创建并赋值
                queue = new MSGQueue();
                queue.setName(queueName);
                queue.setExclusive(exclusive);
                queue.setDurable(durable);
                queue.setAutoDelete(autoDelete);
                queue.setArguments(arguments);
                //4.如果设置了持久化,就先在硬盘上保存一份
                if(durable) {
                    diskDataCenter.insertQueue(queue);
                }
                //5.写内存
                memoryDataCenter.insertQueue(queue);
                System.out.println("[VirtualHost] 队列创建成功!queueName=" + queueName);
            }
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] 队列创建失败!queueName=" + queueName);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 队列删除
     * @param queueName
     * @return
     */
    public boolean queueDelete(String queueName) {
        //1.虚拟主机前缀
        queueName = virtualHostName + queueName;
        try {
            synchronized (queueLocker) {
                //2.判断队列是否存在,若不存在则抛出异常
                MSGQueue queue = memoryDataCenter.getQueue(queueName);
                if(queue == null) {
                    throw new MqException("[VirtualHost] 队列不存在,删除失败!queueName=" + queueName);
                }
                //3.存在则判断是否持久化,若持久化到硬盘上,则先删硬盘
                if(queue.isDurable()) {
                    diskDataCenter.deleteQueue(queueName);
                }
                //4.删内存
                memoryDataCenter.deleteQueue(queueName);
                System.out.println("[VirtualHost] 队列删除成功!queueName=" + queueName);
            }
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] 队列删除失败!queueName=" + queueName);
            e.printStackTrace();
            return false;
        }
    }

2.6 Operaciones vinculantes

Cree un enlace: primero determine si dicho enlace existe en la memoria. Si existe, la creación fallará. Si no existe, debe verificar si el conmutador y la cola existen al mismo tiempo. Si existen, el la creación puede tener éxito.

Eliminar enlace: primero determine si este enlace existe en la memoria. No se generará ninguna excepción. Si existe, no es necesario determinar si el conmutador y la cola existen (lo que complica los pasos), y no es necesario determinar si el enlace existe en el disco duro, simplemente elimínelo directamente de la memoria y el disco duro, porque incluso si no hay datos en el disco duro, no habrá efectos secundarios (la esencia es que la capa inferior de MyBatis llama a eliminar declaración para eliminar).

    /**
     * 创建队列交换机绑定关系
     * @param queueName
     * @param exchangeName
     * @param bindingKey
     * @return
     */
    public boolean queueBind(String queueName, String exchangeName, String bindingKey) {
        //1.虚拟主机前缀
        queueName = virtualHostName + queueName;
        exchangeName = virtualHostName + exchangeName;
        try {
            synchronized (exchangeLocker) {
                synchronized (queueLocker) {
                    //2.检测内存中是否存在绑定
                    Binding existsBinding = memoryDataCenter.getBinding(exchangeName, queueName);
                    if(existsBinding != null) {
                        throw new MqException("[VirtualHost] 绑定已存在,创建失败!exchangeName=" + exchangeName +
                                "queueName=" + queueName);
                    }
                    //3.不存在,先检验 bindingKey 是否合法
                    if(!router.checkBindingKey(bindingKey)) {
                        throw new MqException("[VirtualHost] bindingKey 非法!bingdingKey=" + bindingKey);
                    }
                    //4.检验队列和交换机是否存在,不存在抛出异常
                    Exchange existsExchange = memoryDataCenter.getExchange(exchangeName);
                    if(existsExchange == null) {
                        throw new MqException("[VirtualHost] 交换机不存在!exchangeName=" + exchangeName);
                    }
                    MSGQueue existsQueue = memoryDataCenter.getQueue(queueName);
                    if(existsQueue == null) {
                        throw new MqException("[VirtualHost] 队列不存在!queueName=" + queueName);
                    }
                    //5.创建绑定
                    Binding binding = new Binding();
                    binding.setBindingKey(bindingKey);
                    binding.setQueueName(queueName);
                    binding.setExchangeName(exchangeName);
                    //6.硬盘中创建绑定的前提是队列和交换机都是持久化的,否则绑定在硬盘中无意义
                    if(existsQueue.isDurable() && existsExchange.isDurable()) {
                        diskDataCenter.insertBinding(binding);
                    }
                    //7.写内存
                    memoryDataCenter.insertBinding(binding);
                    System.out.println("[VirtualHost] 绑定创建成功!exchangeName=" + exchangeName +
                            "queueName=" + queueName);
                }
            }
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] 绑定创建失败!exchangeName=" + exchangeName +
                    "queueName=" + queueName);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除绑定关系
     * @param queueName
     * @param exchangeName
     * @return
     */
    public boolean queueUnBind(String queueName, String exchangeName) {
        //1.虚拟主机前缀
        queueName = virtualHostName + queueName;
        exchangeName = virtualHostName + exchangeName;
        try {
            //这里的加锁顺序一定要和上面的两次加锁保持一致(防止死锁)
            synchronized (exchangeLocker) {
                synchronized (queueLocker) {
                    //2.检查绑定是否存在,不存在则抛出异常
                    Binding binding = memoryDataCenter.getBinding(exchangeName, queueName);
                    if(binding == null) {
                        throw new MqException("[VirtualHost] 绑定不存在, 删除失败!binding=" + binding);
                    }
                    //3.直接删除硬盘数据,不用判断是否存在,因为即使不存在绑定,直接删也不会有什么副作用,因为本质就是调用 sql 删除
                    diskDataCenter.deleteBinding(binding);
                    //4.删内存
                    memoryDataCenter.deleteBinding(binding);
                    System.out.println("[VirtualHost] 绑定删除成功!queueName=" + queueName +
                            ", exchangeName=" + exchangeName);
                }
            }
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] 绑定删除失败!queueName=" + queueName +
                    ", exchangeName=" + exchangeName);
            e.printStackTrace();
            return false;
        }
    }

2.7 Operaciones relacionadas con mensajes

Publicar un mensaje: la esencia es enviar un mensaje a un conmutador / cola designado. Primero, determine si la clave de enrutamiento es legal, luego obtenga el conmutador y luego realice un procesamiento diferente según el tipo de diferentes conmutadores (diferentes conmutadores envían mensajes a la cola de diferentes maneras), y finalmente llamar al método de envío de mensaje

Entregar un mensaje: enviar un mensaje esencialmente significa escribir el mensaje en la memoria/disco duro. La necesidad de escribir el mensaje en el disco duro depende de si el mensaje admite la persistencia (deliverMode de 1 significa sin persistencia y deliveryMode de 2 significa persistencia) , y finalmente se debe agregar una lógica para notificar al consumidor que el mensaje se puede consumir (no lo agregaré aquí, hablaremos de ello en el próximo capítulo).

Ps: para el cambio directo aquí, se acuerda usar routeKey como nombre de la cola y enviar el mensaje directamente a la cola especificada , de modo que la relación vinculante pueda ignorarse directamente (este es el escenario más común en el desarrollo real, por lo que está configurado así aquí)

    /**
     * 发送消息到指定的 交换机/队列 中
     * @param exchangeName
     * @return
     */
    public boolean basicPublish(String exchangeName, String routingKey, BasicProperties basicProperties, byte[] body) {
        //1.虚拟主机前缀
        exchangeName = virtualHostName + exchangeName;
        try {
            //2.判定 routingKey 是否合法
            if(!router.checkRoutingKey(routingKey)) {
                throw new MqException("[VirtualHost] routingKey 非法!routingKey=" + routingKey);
            }
            //3.获取交换机
            Exchange exchange = memoryDataCenter.getExchange(exchangeName);
            if(exchange == null) {
                throw new MqException("[VirtualHost] 交换机不存在,消息发送失败!exchangeName=" + exchangeName);
            }
            //4.判定交换机类型
            if(exchange.getType() == ExchangeType.DIRECT) {
                //直接交换机转发消息,以 routingKey 作为队列名字
                //直接把消息写入指定队列,也就可以无视绑定关系(这是实际开发环境中最常用的,因此这里这样设定)

                String queueName = virtualHostName + routingKey;
                //5.构造消息对象
                Message message = Message.createMessageWithId(routingKey, basicProperties, body);
                //6.查找该队列名对应的对象
                MSGQueue queue = memoryDataCenter.getQueue(queueName);
                if(queue == null) {
                    throw new MqException("[VirtualHost] 队列不存在!queueName=" + queueName);
                }
                //7.发送消息
                sendMessage(queue, message);
            } else {
                // 按照 fanout 和 topic 的方式转发
                //5.找到该交换机的所有绑定,并遍历绑定对象
                ConcurrentHashMap<String, Binding> bindingsMap = memoryDataCenter.getBindings(exchangeName);
                for(Map.Entry<String, Binding> entry : bindingsMap.entrySet()) {
                    // 1) 获取到绑定对象,判定对应的队列是否存在
                    Binding binding = entry.getValue();
                    MSGQueue queue = memoryDataCenter.getQueue(binding.getQueueName());
                    if(queue == null) {
                        //此处不抛异常了,因为又多个这样的队列
                        //希望不要因为一个队列匹配失败,影响到其他队列
                    System.out.println("[VirtualHost] basicPublish 发送消息时,发现队列不存在!queueName=" + binding.getQueueName());
                    }
                    // 2) 构造消息对象
                    Message message = Message.createMessageWithId(routingKey, basicProperties, body);
                    // 3) 判定这个消息是否能转发给该队列
                    // 如果是 fanout。所有绑定的队列都要转发
                    // 如果是 topic,还需要判定下,bindingKey 和 routingKey 是不是匹配
                    if(!router.route(exchange.getType(), binding, message)) {
                        continue;
                    }
                    sendMessage(queue, message);
                }
            }
            return true;
        } catch (Exception e) {
            System.out.println("[VirtualHost] 消息发送失败!exchangeName=" + exchangeName +
                    ", routingKey=" + routingKey);
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 发送消息
     * 实际上就是把消息写道 硬盘 和 内存 上
     * @param queue
     * @param message
     */
    private void sendMessage(MSGQueue queue, Message message) throws IOException, MqException {
        //判断当前消息是否要进行持久化
        //deliverMode 为 1 表示不持久化,deliverMode 为 2 表示要持久化
        int deliverMode = message.getDeliverMode();
        if(deliverMode == 1) {
            diskDataCenter.sendMessage(queue, message);
        }
        //写内存
        memoryDataCenter.sendMessage(queue, message);
        //TODO 此处还需要补充一个逻辑,通知消费者可以消费消息了
    }

2.8 Problemas de seguridad de subprocesos complementarios

La granularidad de la cerradura es demasiado grande. ¿Es necesario ajustarla?

Se agregan muchos bloqueos aquí, y la granularidad del bloqueo aún es bastante grande, pero se pueden realizar ajustes para refinar la granularidad del bloqueo, pero el impacto no será grande, porque aquí se crean interruptores, se crean enlaces, se crean colas, Los interruptores se eliminan... ¡Estos son todos operaciones de baja frecuencia!

Dado que es una operación de baja frecuencia, la probabilidad de encontrar una situación en la que ambos subprocesos estén operando para crear una cola es muy baja. Además, la sincronización en sí también está sesgada hacia el estado de bloqueo, y el bloqueo solo se bloquea realmente cuando hay es competencia real, por lo que no es necesario ajustar la granularidad de la cerradura.

Todavía es necesario agregar un candado aquí para hacer frente a algunas situaciones extremas raras.

Dado que la capa de código de VirtualHost está bloqueada, ¿no es necesario bloquear las operaciones en el MemoryDataCenter interno? ¿Tiene sentido bloquearlo antes?

No sabemos para qué clase se llama el método de esta clase, actualmente el propio VirtualHost garantiza la seguridad de los subprocesos, en este momento VirtualHost llama internamente a MemoryDataCenter, y el problema de no bloquearlo no es grande, pero si es otra clase. , Si llama a MemoryDataCenter desde varios subprocesos, será impredecible ~~

Supongo que te gusta

Origin blog.csdn.net/CYK_byte/article/details/132397422
Recomendado
Clasificación