目录
引言
- 虚拟主机的概念类似于 MySQL 的 database,用于将 交换机、队列、绑定、消息 进行逻辑上的隔离
- 虚拟主机不仅仅要管理数据,还需要提供一些 核心 API,供上层代码进行调用
- 就是将之前写的 内存 和 硬盘 的数据管理给串起来
- 即整个程序的核心业务逻辑
核心 API:
- 创建交换机 exchangeDeclare
- 删除交换机 exchangeDelete
- 创建队列 queueDeclare
- 删除队列 queueDelete
- 创建绑定 queueBind
- 删除绑定 queueUnbind
- 发送消息 basicPublish
- 订阅消息 basicConsume
- 确认消息 basicAck
注意点一:
- 此处各 API 的取名,各 API 中设定的参数,均参考于 RabbitMQ
注意点二:
- 此处我们仅实现单个虚拟主机,并不打算实现添加/删除虚拟主机的 API
- 但是会在设计数据结构上留下这样的扩展空间
实例理解
- 虚拟主机存在目的,就是为了保证隔离,即不同虚拟主机之间的内容互不影响
- 当 虚拟主机1 中创建了一个名为 "testExchange" 的交换机
- 而 虚拟主机2 中也创建了一个名为 "testExchange" 的交换机
- 虽然这两个交换机的名字相同,但是却处于不同虚拟主机中,所以需要区分开来
问题:
- 如何表示 交换机 与 虚拟主机 之间的从属关系?
可选方案:
- 方案一:参考数据库设计 "一对多" 的方案,给交换机表添加个属性,虚拟主机 id/name
- 方案二:重新约定交换机的名字,即 新交换机名字 = 虚拟主机名字 + 交换机真实名字
- 方案三:给每个虚拟主机,分配一组不同的数据库和文件(比方案二麻烦,但更优雅)
回答:
- 此处我们选择方案二!
- 约定在 VirtualHost 中的核心 api 里,对 exchangeName 和 queueName 做一个转换
- 在该层代码中进行转换后,后续代码 MemoryDataCenter、DiskDataCenter 无需调整
- 按照这个方式,也可以去区分不同的队列
- 绑定 与 交换机和队列 相关,通过上述操作,绑定自然也就被隔离开了!
- 消息 与 队列 相关,因为队列名已经区分开了,消息自然也就被区分开了!
实现 VirtualHost 类
属性
@Getter public class VirtualHost { private String virtualHostName; private MemoryDataCenter memoryDataCenter = new MemoryDataCenter(); private DiskDataCenter diskDataCenter = new DiskDataCenter(); private Router router = new Router(); private ConsumerManager consumerManager = new ConsumerManager(this); // 操作交换机的锁对象 private final Object exchangeLocker = new Object(); // 操作队列的锁对象 private final Object queueLocker = new Object(); 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] 恢复内存数据失败!"); } } }
- Router 类用于实现交换机的转发规则,验证 bindingKey 和 routingKey 的合法性
- ConsumerManager 类用于实现消费消息的核心逻辑
交换机相关操作
// 创建交换机 // 如果交换机不存在就创建,如果存在 则直接返回 // 返回值是 boolean,创建成功,返回 true,失败返回 false public boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType, boolean durable, boolean autoDelete, Map<String,Object> arguments) { // 把交换机的名字,加上虚拟主机作为前缀 exchangeName = virtualHostName + exchangeName; try { synchronized (exchangeLocker) { // 1、判定该交换机是否已经存在,直接通过内存查询 Exchange existsExchange = memoryDataCenter.getExchange(exchangeName); if (existsExchange != null) { // 该交换机已经存在 System.out.println("[VirtualHost] 交换机已经存在!exchangeName = " + exchangeName); return true; } // 2、真正创建交换机,先构造 Exchange 对象 Exchange exchange = new Exchange(); exchange.setName(exchangeName); exchange.setType(exchangeType); exchange.setDurable(durable); exchange.setAutoDelete(autoDelete); exchange.setArguments(arguments); // 3、把交换机对象写入硬盘 if(durable) { diskDataCenter.insertExchange(exchange); } // 4、把交换机对象写入内存 memoryDataCenter.insertExchange(exchange); System.out.println("[VirtualHost] 交换机创建完成!exchangeName = " + exchangeName); // 上述逻辑,先写硬盘,后写内存,目的就是因为硬盘更容易写失败,如果硬盘写失败了,内存就不写了 // 要是先写内存,内存写成功了,硬盘写失败了,还需要把内存的数据给再删掉,就比较麻烦了 } return true; }catch (Exception e) { System.out.println("[VirtualHost] 交换机创建失败!exchangeName = " + exchangeName); e.printStackTrace(); return false; } } // 删除交换机 public boolean exchangeDelete(String exchangeName) { exchangeName = virtualHostName + exchangeName; try { synchronized (exchangeLocker) { // 1、先找到对应的交换机 Exchange toDelete = memoryDataCenter.getExchange(exchangeName); if(toDelete == null) { throw new MqException("[VirtualHost] 交换机不存在无法删除!"); } // 2、删除硬盘上的数据 if(toDelete.isDurable()) { diskDataCenter.deleteExchange(exchangeName); } // 3、删除内存中的交换机数据 memoryDataCenter.deleteExchange(exchangeName); System.out.println("[VirtualHost] 交换机删除成功!exchangeName = " + exchangeName); } return true; }catch (Exception e) { System.out.println("[VirtualHost] 交换机删除失败!exchangeName = " + exchangeName); e.printStackTrace(); return false; } }