ソースコードに基づいて、RabbitMQ をシミュレートおよび実装する - 仮想ホスト設計 (5)

目次

1. 仮想ホストの設計

1.1. 需要分析

1.1.1. コア API

1.1.2. 仮想ホストは何に使用されますか?

1.1.3. スイッチと仮想ホスト間の関係をどのように表現するか?

2. VirtualHost クラスを実装する

2.1. プロパティ

2.2. オブジェクトをロックする

2.3. 公開例

2.4. 仮想ホストの構築方法

2.5. スイッチ関連の操作

2.5. キュー関連の操作

2.6. バインディング関連の操作

2.7. メッセージ関連の操作

2.8. スレッドの安全性に関する補足的な問題


1. 仮想ホストの設計


1.1. 需要分析

1.1.1. コア API

仮想ホストの概念は、スイッチ、キュー、バインディング、メッセージを論理的に分離する MySQL のデータベースに似ています。そのため、データを管理するだけでなく、上位層のコードが呼び出すためのいくつかのコア API を提供する必要もあります。

コア API (以前に書き込まれたメモリ上のデータ管理とハードディスク上のデータ管理を接続するためのもの):

  1. スイッチ交換宣言の作成
  2. スイッチ交換の削除削除
  3. キューの作成キュー宣言
  4. キューの削除queueDelete
  5. バインディングキューの作成Bind
  6. バインディングキューを削除アンバインド
  7. メッセージを送信基本公開
  8. メッセージを購読する基本Cosume
  9. 確認メッセージbasicAck

1.1.2. 仮想ホストは何に使用されますか?

仮想ホストの目的は分離を確保することであり、異なる仮想ホスト間のコンテンツが影響を受けないようにすることです。

たとえば、仮想ホスト 1 に「testExchange」という名前の交換機が作成され、仮想ホスト 2 にも「testExchange」という名前の交換機が作成されており、名前は同じですが、異なる空間にある交換機です。

 呼び出し側の観点から見ると、2 つの交換は作成時に testExchange という名前でしたが、仮想ホスト内で自動的に処理され、「virtualHost1testExchange」および「virtualHost2testExchange」というプレフィックスが付けられます。

これにより、異なるスイッチが区別され、異なるキューがさらに区別され(1 つのスイッチが複数のキューに対応)、さらにバインディングが分離され(バインディングがスイッチとキューに関連する)、さらにメッセージがキューと強く関連するようになり、メッセージは自然に区別されます。

1.1.3. スイッチと仮想ホスト間の関係をどのように表現するか?

解決策 1: スイッチ テーブル、仮想ホスト ID/名前への属性の追加など、データベースの「1 対多」ソリューションの設計を参照してください。

解決策 2 (RabbitMQ によって採用された戦略): 再合意、スイッチの名前 = 仮想ホストの名前 + スイッチの名前

オプション 3: より洗練された方法は、各仮想ホストに異なるデータベースとファイルのセットを割り当てることですが、少し面倒です。

ここでは、RabbitMQ が採用している戦略に従います~~

2. VirtualHost クラスを実装する


2.1. プロパティ

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

Router クラスは、routingKey と bindingKey が正当であるかどうか、およびメッセージとキューの間の一致ルールを記述します。

2.2. オブジェクトをロックする

スイッチ操作、キュー操作、バインディング操作、メッセージ操作におけるスレッド セーフティの問題を処理するために使用されます。

例:マルチスレッド操作「スイッチの追加」と「削除しながら追加」のロック操作(マルチスレッドの同時削除は実際には何もありません。SQL削除で削除されるため、複数回削除しても何もありません)副作用)

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

2.3. 公開例

これにより、上位層コードは呼び出し時に仮想ホスト名、メモリ データ処理センター、およびハードディスク データ処理センターをそれぞれ取得できるようになります。


    public String getVirtualHostName() {
        return virtualHostName;
    }

    public MemoryDataCenter getMemoryDataCenter() {
        return memoryDataCenter;
    }

    public DiskDataCenter getDiskDataCenter() {
        

2.4. 仮想ホストの構築方法

主にホスト名、ハードディスク、メモリデータ処理センターの初期化操作を実行します。

    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. スイッチ関連の操作

スイッチの作成: 対応するスイッチをメモリから取得し、存在しない場合は作成し、存在する場合は直接返します。

スイッチの削除: まずスイッチが存在するかどうかを確認し、存在しない場合は例外がスローされ、存在する場合は削除します。

Ps: 戻り値はブール型で、true は成功を示し、false は失敗を示します。


    /**
     * 创建交换机
     * @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. キュー関連の操作

ここでの動作ロジックはスイッチの動作ロジックと似ています~~

    /**
     * 创建队列
     * @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. バインディング関連の操作

バインディングを作成します: まず、そのようなバインディングがメモリに存在するかどうかを確認します。存在する場合、作成は失敗します。存在しない場合は、スイッチとキューが同時に存在するかどうかを確認する必要があります。存在する場合は、作成は成功する可能性があります。

バインディングの削除: まず、このバインディングがメモリ内に存在するかどうかを確認します。例外はスローされません。存在する場合、スイッチとキューが存在するかどうかを確認する必要はありません (手順が複雑になります)。バインディングはハードディスク上に存在します。ハードディスクにデータがなくても副作用はないため、メモリとハードディスクから直接削除するだけです (本質的には、MyBatis の最下層が削除を呼び出します)削除するステートメント)。

    /**
     * 创建队列交换机绑定关系
     * @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. メッセージ関連の操作

メッセージのパブリッシュ: 本質は、指定されたスイッチ/キューにメッセージを送信することです。まず、routingKey が正当であるかどうかを判断し、次にスイッチを取得し、さまざまなスイッチのタイプに応じてさまざまな処理を実行します (さまざまなスイッチがメッセージを送信します)キューをさまざまな方法で呼び出します)、最後にメッセージ送信メソッドを呼び出します

メッセージの配信: メッセージの送信は、基本的にメッセージをメモリ/ハードディスクに書き込むことを意味します。メッセージをハードディスクに書き込む必要があるかどうかは、メッセージが永続性をサポートしているかどうかによって決まります (deliverMode が 1 の場合は永続性なしを意味し、deliverMode が 2 の場合は永続性を意味します)最後に、メッセージを消費できることをコンシューマーに通知するロジックを追加する必要があります (ここでは追加しません。次の章で説明します)。

Ps: ここでのダイレクト スイッチの場合、キュー名として routingKey を使用し、指定されたキューにメッセージを直接送信することでバインディング関係を直接無視できるようにすることが合意されています(これは実際の開発で最も一般的なシナリオです。ここではこのように設定されています)

    /**
     * 发送消息到指定的 交换机/队列 中
     * @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. スレッドの安全性に関する補足的な問題

ロックの粒度が大きすぎます。調整する必要がありますか?

ここには多くのロックが追加されており、ロックの粒度は依然として非常に大きいですが、ロックの粒度を細かく調整することができますが、ここでスイッチが作成され、バインディングが作成され、キューが作成されるため、影響は大きくありません。スイッチは削除されています...これらはすべて低頻度の操作です。

これは低頻度の操作であるため、両方のスレッドがキューを作成するために動作している状況に遭遇する可能性は非常に低いです。また、同期自体もロック状態に偏っており、ロックが実際にロックされるのは、ロックが存在する場合のみです。実際の競合であるため、ロック粒度を調整する必要はありません。

まれに起こる極端な状況に対処するために、ここにロックを追加する必要があります。

コードの VirtualHost 層がロックされているため、内部の MemoryDataCenter の操作をロックする必要はありませんか? 事前にロックすることに意味はありますか?

このクラスのメソッドがどのクラスに対して呼び出されているかはわかりません。現状、VirtualHost自体がスレッドセーフを保証しています。この時、VirtualHostは内部でMemoryDataCenterを呼び出しており、ロックされない問題は大きくありませんが、別のクラスであれば, 複数のスレッドからMemoryDataCenterを呼び出すと、予測不能になります~~

おすすめ

転載: blog.csdn.net/CYK_byte/article/details/132397422