模拟实现RabbitMQ,2024年最新轻松拿到了阿里大数据开发高级开发工程师的offer

private long offsetEnd;
//表示此消息在文件中是否有效(采用逻辑删除)
//0x00 表示无效, 0x01 表示有效
private byte isValid = 0x01;

//通过一个"工厂方法"来创建message对象,随机生成Id
public static Message createMessageWithId(BasicProperties basicProperties, String routingKey, byte[] body){
Message message = new Message();
if(basicProperties != null){
message.setBasicProperties(basicProperties);
}
message.getBasicProperties().setMessageId(“M_” + UUID.randomUUID().toString());
message.getBasicProperties().setRoutingKey(routingKey);
message.setBody(body);
//此处 offsetBeg,offsetEnd,isValid 作用在持久化操作
//当进行持久化时,才需要赋值
return message;
}

public String getMessageId() {
return this.basicProperties.getMessageId();
}

public void setMessageId(String messageId) {
this.basicProperties.setMessageId(messageId);
}

public String getRoutingKey() {
return this.basicProperties.getRoutingKey();
}

public void setRoutingKey(String routingKey) {
this.basicProperties.setRoutingKey(routingKey);
}

public boolean isDurable() {
return this.basicProperties.isDurable();
}

public void setDurable(boolean durable) {
this.basicProperties.setDurable(durable);
}
}

在传输消息的时候,都是通过二进制来传输,所以消息本质上就是一个byte[],其他都是辅助字段。

offsetBeg、offsetEnd、isValid字段是用来描述这个消息在文件中是如何存储的,以及该消息是否已经被删除了。

由于在文件中使用类似数组中的删除元素的操作会大大增加我们程序的运行时间,在此我们选择采用逻辑删除的方式,即通过一个标记字段来表示是否删除。

BasicProperties:

@Data
public class BasicProperties {
//为防止重复,使用UUID生成
private String MessageId;
private boolean isDurable = false;
//口令
private String routingKey;
}

后续的操作都是围绕这几个核心概念来增删改查的。

数据库管理

由于MySQL数据库比较重量,在此就选择使用相对轻量的数据库SQLite. MySQL是客户端服务器结构的程序,而SQLite是一个本地的数据库,操作它相当于是直接操作本地文件。

在Java中使用SQLite,也不需要额外的安装,可以直接使用maven,将相关依赖引入,然后进行配置就可以正常使用了。

SQLite的依赖:

org.xerial sqlite-jdbc 3.42.0.0

application.yml:

spring:
datasource:
url: jdbc:sqlite:./data/meta.db
username:
password:
driver-class-name: org.sqlite.JDBC

SQLite是把数据存储在当前硬盘的某个指定文件中,在这里就存储在了工作路径的data目录中的meta.db文件了。

SQLite也不需要指定用户名和密码,毕竟是自己一个人用的,只有本地主机才能使用。

接下来就是建库和建表。在MySQL中,需要自己手动进行创建database,但在SQLite中,则无需手动建库了,每一个数据库文件就是一个数据库,即当前的meta.db文件就相当于是MySQL中的database。

为了能够即用即创建表,在这考虑部署的时候就自动创建表,即通过mybatis的update注解来建表并添加一些基础的查询、增加、删除操作。

@Mapper
public interface MetaMapper {
//建表操作
@Update(“create table if not exists exchange(” +
“name varchar(50) primary key,” +
“type int,” +
“durable boolean,” +
“autoDelete boolean,” +
“arguments varchar(1024))”)
public void createExchangeTable();

@Update(“create table if not exists queue(” +
“name varchar(50) primary key,” +
“exclusive boolean,” +
“durable boolean,” +
“autoDelete boolean,” +
“arguments varchar(1024))”)
public void createQueueTable();

@Update(“create table if not exists binding(” +
“exchangeName varchar(50),” +
“queueName varchar(50),” +
“bindingKey varchar(256))”)
public void createBindingTable();

//基础添加、删除、查询操作
@Insert(“insert into exchange values(#{name}, #{type}, #{durable}, #{autoDelete}, #{arguments})”)
public void insertExchange(Exchange exchange);

@Delete(“delete from exchange where name = #{exchangeName}”)
public void deleteExchange(String exchangeName);

@Select(“select * from exchange”)
public List selectAllExchanges();

@Insert(“insert into queue values(#{name}, #{exclusive}, #{durable}, #{autoDelete}, #{arguments})”)
public void insertQueue(MSGQueue msgQueue);

@Delete(“delete from queue where name = #{queueName}”)
public void deleteQueue(String queueName);

@Select(“select * from queue”)
public List selectAllQueues();

@Insert(“insert into binding values(#{exchangeName}, #{queueName}, #{bindingKey})”)
public void insertBinding(Binding binding);

@Delete(“delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName}”)
public void deleteBinding(String exchangeName, String queueName);

@Select(“select * from binding”)
public List selectAllBindings();
}

由于SQLite中也不支持HashMap这种数据结构,因此使用String来存储。

Mybatis在写入数据的时候,会自动的调用对象的getter方法,而在读出数据的时候,会自动调用setter方法,因此为了能够将arguments这个HashMap类型的数据存入到数据库中,我们需要转成字符串。

在此使用jackson来处理,引入相关依赖即可。

jackson依赖:

com.fasterxml.jackson.core jackson-databind 2.9.6

getter和setter:

public String getArguments() {
ObjectMapper objectMapper = new ObjectMapper();
try {
objectMapper.writeValueAsString(arguments);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return “{}”;
}

public void setArguments(String arguments) {
ObjectMapper objectMapper = new ObjectMapper();
try {
this.arguments = objectMapper.readValue(arguments, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
e.printStackTrace();
}
}

为了使程序的结构更加清晰,程序员更方便的操作数据库,考虑创建一个DataBaseManager类来封装数据库的相关操作。

DataBaseManager:

//封装数据库操作
//DataBaseManager不交给Spring管理,由程序员手动管理
@Slf4j
public class DataBaseManager {
//从Spring上下文中手动获取MetaMapper的Bean
private MetaMapper metaMapper;

//带有业务逻辑的初始化操作
//完成建库建表操作
//如果不存在表就进行创建,并添加默认初始数据
public void init(){
metaMapper = MqBlogApplication.context.getBean(MetaMapper.class);
if(!checkDBExists()){
//创建data目录
File file = new File(“./data”);
file.mkdirs();
//建表并初始化数据
createTable();
createDefaultData();
log.info(“[DataBaseManager] 数据库初始化完成”);
}else{
log.info(“[DataBaseManager] 数据库已经初始化”);
}
}

//删除数据库目录
//删除目录前,需要保证目录是空的
public void deleteDB(){
File file = new File(“./data/meta.db”);
//删除目录里面的文件
boolean isSuccess = file.delete();
if(isSuccess){
log.info(“[DataBaseManager] 删除数据库文件成功”);
}else{
log.info(“[DataBaseManager] 删除数据库文件失败”);
}
//删除目录
file = new File(“./data”);
isSuccess = file.delete();
if(isSuccess){
log.info(“[DataBaseManager] 删除数据库目录成功”);
}else {
log.info(“[DataBaseManager] 删除数据库目录失败”);
}

}

//判断数据库(meta.db)是否存在
private boolean checkDBExists() {
File DBFile = new File(“./data/meta.db”);
return DBFile.exists();
}

//对 Exchange,Queue,Binding 进行建表
private void createTable() {
metaMapper.createExchangeTable();
metaMapper.createQueueTable();
metaMapper.createBindingTable();
log.info(“[DataBaseManager] 创建表完成”);
}

//RabbitMQ初始带有一个匿名交换机, 类型为 Direct
private void createDefaultData() {
Exchange exchange = new Exchange();
exchange.setName(“”);
exchange.setType(ExchangeType.DIRECT);
exchange.setDurable(true);
exchange.setAutoDelete(false);
metaMapper.insertExchange(exchange);
log.info(“[DataBaseManager] 初始数据构造完成”);
}

//增删查操作
public void insertExchange(Exchange exchange){
metaMapper.insertExchange(exchange);
}

public void deleteExchange(String exchangeName){
metaMapper.deleteExchange(exchangeName);
}

public List selectAllExchanges(){
List exchanges = metaMapper.selectAllExchanges();
return exchanges;
}

public void insertQueue(MSGQueue queue){
metaMapper.insertQueue(queue);
}

public void deleteQueue(String queueName){
metaMapper.deleteQueue(queueName);
}

public List selectAllQueues(){
return metaMapper.selectAllQueues();
}

public void insertBinding(Binding binding){
metaMapper.insertBinding(binding);
}

public void deleteBinding(String exchangeName, String queueName){
metaMapper.deleteBinding(exchangeName, queueName);
}

public List selectAllBindings(){
return metaMapper.selectAllBindings();
}
}

文件管理

文件管理主要是对消息进行存储,消息是进行序列化存储的数据,不便于读取,因此我们无法区分一条消息的开始和结束。如果对每条消息都单独保存在一个文件中,可能会创建大量的文件。

我们现在做出如下约定:一条消息在硬盘上存储需要先花费4个字节存储消息体的长度,然后再存储消息本体。

在定义消息的时候,我们定义了两个辅助字段:offsetBeg、offsetEnd,分别记录一个消息在文件中存储的开始和结束。

通过上述分析,我们可以知道,需要对message对象进行序列化存储在文件中,而offsetBeg和offsetEnd则不需要,保留原始值即可。需要对Message类实现serializable接口,由于BasicProperties也包含在类中,也需要实现接口,而offsetBeg,offsetEnd使用transient关键字描述,程序就不会再序列化了。

@Data
public class Message implements Serializable {
//消息的基本属性
private BasicProperties basicProperties;
//存储数据
private byte[] body;
//位置偏移量,便于在文件中取出数据
//采用[offsetBeg, offsetEnd)区间
//这两个属性是帮助从文件中读取数据的,不需要进行序列化
private transient long offsetBeg;
private transient long offsetEnd;
//表示此消息在文件中是否有效(采用逻辑删除)
//0x00 表示无效, 0x01 表示有效
private byte isValid = 0x01;
}

@Data
public class BasicProperties implements Serializable {
//为防止重复,使用UUID生成
private String MessageId;
private boolean isDurable = false;
//口令
private String routingKey;
}

BinaryTool:

public class BinaryTool {
//将一个Java对象序列化成一个字节数组
//这个类必须实现了Serializable
public static byte[] toBytes(Object object) throws IOException {
//此处的 ByteArrayOutputStream 可以不用关闭,是一个纯内存的对象,不涉及到文件描述符
try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
//此处 writeObject 会将 object 对象进行序列化,生成的二进制字节数据,写入到objectOutputStream中
//objectOutputStream 关联到了 byteArrayOutputStream, 最终就写到了 byteArrayOutputStream
objectOutputStream.writeObject(object);
}
//将 byteArrayOutputStream 中序列化后的二进制数据 转成byte[] 数组
return byteArrayOutputStream.toByteArray();
}
}

public static Object fromBytes(byte[] data) throws IOException, ClassNotFoundException {
try(ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data)){
try(ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)){
//此处的 readObject 将 data byte[] 转成对象
return objectInputStream.readObject();
}
}
}
}

由于每条消息都是存储在队列上的,因此就按照队列的维度进行存储。即每个队列都是一个目录,在每个目录下都有两个文件,一个是data数据文件,用来存储消息,另一个是state文件,用来描述data数据文件中的情况,为了后面可以进行gc。

此处的data文件是二进制文件,而state文件则是字符型文件,里面用一个数表示消息的总数,再用一个数表示消息的有效个数。因为采用的是逻辑删除的方式,也就是说被删除的消息不会真正意义上删除,当文件中存储很多数据的时候,就会大大影响我们的读取效率,因此我们需要使用GC来真正删除垃圾消息。在这规定:当文件有1000个消息的时候,并且垃圾消息占比50%,我们就进行一次GC。

在这我们也封装一个类来帮助我们处理文件的操作。

MessageFileManager:

@Slf4j
//对硬盘上的message进行管理
//规定每个队列都对应一个目录,
//每个目录下都包含两个文件:queue_data.txt, queue_state.txt,分别用来存储数据和统计数据个数
//
public class MessageFileManager {
public void init() {
//为了接口的统一性
//暂时不处理,后续可自行添加
}

//表示该队列下统计的文件信息
public static class State{
public int totalCount = 0; //总消息数量
public int validCount = 0; //有效消息数量
}

//获取消息所处队列的目录
private String getQueueDir(String queueName){
return “./data/” + queueName;
}

//获取消息所处队列的数据文件路径
private String getQueueDataPath(String queueName){
return getQueueDir(queueName) + “/queue_data.txt”;
}

//获取消息所处队列的统计文件路径
private String getQueueStatePath(String queueName){
return getQueueDir(queueName) + “/queue_state.txt”;
}

//创建队列对应的目录和文件
public void createQueueFiles(String queueName) throws IOException {
//1.创建队列对应目录
File directory = new File(getQueueDir(queueName));
if(!directory.exists()){
boolean success = directory.mkdirs();
if(!success){
throw new IOException(“[MessageFileManager] 创建目录失败” + directory.getAbsoluteFile());
}
}
//2.创建队列对应数据文件
File dataFile = new File(getQueueDataPath(queueName));
if(!dataFile.exists()){
boolean success = dataFile.createNewFile();
if(!success){
throw new IOException(“[ManagerFileManager] 创建文件失败” + dataFile.getAbsoluteFile());
}
}
//3.创建队列对应统计文件
File stateFile = new File(getQueueStatePath(queueName));
if(!stateFile.exists()){
boolean success = stateFile.createNewFile();
if(!success){
throw new IOException(“[ManagerFileManager] 创建文件失败” + stateFile.getAbsoluteFile());
}
}
//4.初始化队列文件
State state = new State();
writeState(queueName, state);
}

//删除队列对应的目录和文件
public void destroyQueueFiles(String queueName) throws IOException {
//要删除目录需要先删除里面的文件
File dataFile = new File(getQueueDataPath(queueName));
boolean success1 = dataFile.delete();
File stateFile = new File(getQueueStatePath(queueName));
boolean success2 = stateFile.delete();
File directory = new File(getQueueDir(queueName));
boolean success3 = directory.delete();
//确保文件都删除了
if(!success1 || !success2 || !success3){
throw new IOException(“[MessageFileManager] 删除队列文件失败” + dataFile.getAbsoluteFile());
}
}

//从文件中读取统计信息
private State readState(String queueName){
//state文件是文本文件,可以使用Scanner来读写
State state = new State();
try(InputStream inputStream = new FileInputStream(getQueueStatePath(queueName))){
Scanner scanner = new Scanner(inputStream);
state.totalCount = scanner.nextInt();
state.validCount = scanner.nextInt();
return state;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

//向文件中写入统计信息
private void writeState(String queueName, State state){
//OutputStream默认情况打开文件,会清空文件
try(OutputStream outputStream = new FileOutputStream(getQueueStatePath(queueName))){
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.print(state.totalCount + “\t” + state.validCount);
printWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}

//判断队列对应的数据文件和统计文件都存在
private boolean checkQueueFilesExist(String name) {
//如果两个文件都存在了,则说明目录也存在,不需要额外判断
File dataFile = new File(getQueueDataPath(name));
File stateFile = new File(getQueueStatePath(name));
if(!dataFile.exists() || !stateFile.exists()){
return false;
}
return true;
}

//将消息写入对应的队列文件
public void sendMessage(MSGQueue queue, Message message) throws IOException, MqException {
//1.判断对应的队列文件是否存在
if(!checkQueueFilesExist(queue.getName())){
throw new MqException(“[MessageFileManager] 队列文件不存在 queueName:” + queue.getName());
}
//2.将消息转成二进制
synchronized (queue){
byte[] data = BinaryTool.toBytes(message);
//3.获取到文件末尾位置,设置文件位置的偏移量
File dataFile = new File(getQueueDataPath(queue.getName()));
//每条消息前4个字节用来表示消息的长度
message.setOffsetBeg(dataFile.length() + 4);
message.setOffsetEnd(dataFile.length() + 4 + data.length);
//4.写入消息文件
try(OutputStream outputStream = new FileOutputStream(dataFile, true)){
try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
//写入消息的长度(4个字节)
dataOutputStream.writeInt(data.length);
//写入消息本体
dataOutputStream.write(data);
}
}
//5.更新统计文件
State state = readState(queue.getName());
state.totalCount += 1;
state.validCount += 1;
writeState(queue.getName(), state);
}
}

//从硬盘上删除消息(逻辑删除)
//此处传的消息必须为有效消息
public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException, MqException {
//1.判断队列对应的文件是否存在
if(!checkQueueFilesExist(queue.getName())){
throw new MqException(“[MessageFileManager] 队列文件不存在 queueName:” + queue.getName());
}
synchronized (queue){
//2.从文件中获取到消息
byte[] data = new byte[(int)message.getOffsetEnd() - (int)message.getOffsetBeg()];
try(RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), “rw”)){
//移动文件光标
randomAccessFile.seek(message.getOffsetBeg());
randomAccessFile.read(data);
//3.将消息反序列化并标记为无效
Message diskMessage = (Message)BinaryTool.fromBytes(data);
diskMessage.setIsValid((byte)0x00);
//4.将消息重新写入文件
randomAccessFile.seek(message.getOffsetBeg());
randomAccessFile.write(BinaryTool.toBytes(diskMessage));
}
//5.更新统计文件
State state = readState(queue.getName());
//逻辑删除,不需要修改totalCount
state.validCount -= 1;
writeState(queue.getName(), state);
}
}

//从队列文件中读取所有 有效消息
//在程序启动时,进行调用,读取文件中的所有有效消息,加载到内存中
public LinkedList loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
LinkedList messages = new LinkedList<>();
try(InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))){
try(DataInputStream dataInputStream = new DataInputStream(inputStream)){
//记录当前文件光标,方便赋值Message中offsetBeg和offsetEnd值
long currentOffset = 0;
while(true){
//1.读取当前消息的长度
//当 readInt 读取到文件末尾的时候,会抛出异常 EOFException
int messageSize = dataInputStream.readInt();
//2.根据读取到的长度,在文件中读取
byte[] buffer = new byte[messageSize];
int actualSize = dataInputStream.read(buffer);
if(messageSize != actualSize){
throw new MqException(“[MessageFileManager] 文件读取错误 queueName:” + queueName);
}
//3.将字节数组反序列化成对象
Message message = (Message) BinaryTool.fromBytes(buffer);
//4.判断当前消息是否为有效消息
if(message.getIsValid() == 0x00){
currentOffset += (4 + messageSize);
continue;
}
//5.计算在文件中偏移量并赋值,将消息添加到链表中
message.setOffsetBeg(currentOffset + 4);
message.setOffsetEnd(currentOffset + 4 + messageSize);
currentOffset += (4 + messageSize);
messages.add(message);
}
} catch (EOFException e){
//文件读取到末尾,并非真的异常
log.info(“[MessageFileManager] 文件读取完毕”);
}
}
return messages;
}

//获取新的队列数据文件
private String getQueueNewDataPath(String queueName){
return getQueueDir(queueName) + “/new_queued_data.txt”;
}

//约定 当文件中总消息数超过 1000 并且 无效消息的个数 超过一半 就进行一次gc
public boolean checkGC(String queueName){
State state = readState(queueName);
if(state.totalCount > 1000 && (double)state.validCount / (double) state.totalCount > 0.5){
return true;
}
return false;
}

//对消息文件进行gc操作
//此处使用复制算法的方式进行
public void gc(MSGQueue queue) throws IOException, MqException, ClassNotFoundException {
//当一个线程进行GC的时候,其他线程不允许进行操作
synchronized (queue){
//由于gc操作比较耗时,在这输出一下gc的时间
//计算gc开始时间
long gcBeg = System.currentTimeMillis();
//1.创建新文件
File newDataFile = new File(getQueueNewDataPath(queue.getName()));
if(newDataFile.exists()){
log.info(“[MessageFileManager] 存在复制的目标文件”);
newDataFile.delete();
}
if(!newDataFile.createNewFile()){
throw new MqException(“[MessageFileManager] 复制文件创建失败,queueName:” + queue.getName());
}
//2.从旧文件中读取消息
//此处读取到的信息都是有效信息
LinkedList messages = loadAllMessageFromQueue(queue.getName());
//3.将有效消息写入新文件
try(OutputStream outputStream = new FileOutputStream(newDataFile)){
try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
for (Message message : messages) {
//不调用sendMessage,防止outputStream频繁关闭影响性能
byte[] buffer = BinaryTool.toBytes(message);
dataOutputStream.writeInt(buffer.length);
dataOutputStream.write(buffer);
}
}
}
//4.让新文件替换就旧文件
File oldDataFile = new File(getQueueDataPath(queue.getName()));
if(!oldDataFile.delete()){
throw new MqException(“[MessageFileManager] 旧数据文件删除失败 oldDataFile:” + oldDataFile.getAbsoluteFile());
}
if(!newDataFile.renameTo(oldDataFile)){
throw new MqException(“[MessageFileManager] 文件重命名失败 newDataFile:” + newDataFile.getAbsoluteFile());
}
//5.更新统计信息的文件
State state = readState(queue.getName());
state.totalCount = state.validCount;
writeState(queue.getName(), state);

//计算gc结束时间
long gcEnd = System.currentTimeMillis();
log.info("[MessageFileManager] " + queue.getName() + "gc完成, 总耗时: " + (gcEnd - gcBeg) + “ms”);
}
}
}

封装硬盘

为了方便后续对数据进行持久化,即不用去关注数据到底是存在数据库还是文件中,在此再封装一个DiskDataCenter类来统一对数据持久化。

/*

  • 通过这个类来管理文件中的数据和数据库数据
  • 调用者不用再关注数据是储存在硬盘文件中还是数据库中
  • 通过这个类完成对 交换机、队列、绑定、消息 的增删查
    */
    public class DiskDataCenter {
    private DataBaseManager dataBaseManager = new DataBaseManager();
    private MessageFileManager messageFileManager = new MessageFileManager();

//对实例进行初始化
public void init(){
dataBaseManager.init();
messageFileManager.init();
}

//交换机
public void insertExchange(Exchange exchange){
dataBaseManager.insertExchange(exchange);
}

public void deleteExchange(String exchangeName){
dataBaseManager.deleteExchange(exchangeName);
}

public List selectAllExchanges(){
return dataBaseManager.selectAllExchanges();
}

//队列
public void insertQueue(MSGQueue queue) throws IOException {
dataBaseManager.insertQueue(queue);
//每个队列都关联一个目录文件(子文件为queue_data.txt、queue_state.txt)
//需要将对应的文件创建
messageFileManager.createQueueFiles(queue.getName());
}

public void deleteQueue(String queueName) throws IOException {
dataBaseManager.deleteQueue(queueName);
//需要将队列对应的文件都删除
messageFileManager.destroyQueueFiles(queueName);
}

public List selectAllQueues(){
return dataBaseManager.selectAllQueues();
}

//绑定
public void insertBinding(Binding binding){
dataBaseManager.insertBinding(binding);
}

public void deleteBinding(Binding binding){
dataBaseManager.deleteBinding(binding.getExchangeName(), binding.getQueueName());
}

public List selectAllBindings(){
return dataBaseManager.selectAllBindings();
}

//消息
public void insertMessage(MSGQueue queue, Message message) throws IOException, MqException {
messageFileManager.sendMessage(queue, message);
}

public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException, MqException {
messageFileManager.deleteMessage(queue, message);
//判断是否需要GC
if(messageFileManager.checkGC(queue.getName())){
messageFileManager.gc(queue);
}
}

public LinkedList loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
return messageFileManager.loadAllMessageFromQueue(queueName);
}
}

内存管理

上述只是完成了持久化操作,但是真正的主战场还是在内存上,硬盘只是用来保证服务器重启后,数据不丢失。

为了方便后续对象的查找,我们可以使用HashMap的数据结构来存储。

交换机:使用HashMap,key 为 exchangeName,value 为 exchange 对象.

队列:使用HashMap,key 为 queueName,value 为 queue 对象.

绑定:使用嵌套HashMap, 第一个 key 为exchangeName ,第一个 value 为 HashMap<String, Binding>

第二个 key 为 queueName,第二个 value 为 binding 对象.

表示的含义是,先查找交换机邦迪的所有队列,然后再找到指定绑定.

消息:使用HashMap,key 为 messageId, value 为 message 对象.

由于我们的消息是依附于队列的,我们还需要使用数据结构来表示这样的关系,即在队列中存储着消息。

队列和消息之间的关系:使用HashMap,key 为 queueName, value 为 LinkedList,里面存储message对象.

待确认的消息:使用嵌套HashMap,第一个 key 为 queueName,第一个 value 为 HashMap<String, Message>

第二个 key 为 messageId,第二个 value 为 message对象.

当消费者从队列中取了一个消息,我们无法保证有没有丢失或读错,但可以通过应答的方式来保证可靠性,即消费者消费完了这个消息后,发送一个确认应答告诉服务器,后续服务器就可以删除了,但在一些对可靠性不高的场景中,手动应答的方式就会比较鸡肋,于是我们在这里提供两种应答方式:1.自动应答 2.手动应答

/*

  • 管理内存中的数据
  • 由于会在多线程的场景下使用,需要注意线程安全
    */
    @Slf4j
    public class MemoryDataCenter {
    //在内存中需要管理 交换机、队列、绑定、消息

//key: exchangeName, value: exchange对象
private ConcurrentHashMap<String, Exchange> exchangeMap = new ConcurrentHashMap<>();
//key: queueName, value: queue对象
private ConcurrentHashMap<String, MSGQueue> queueMap = new ConcurrentHashMap<>();
//key: exchangeName, value: 该交换机与队列的所有绑定关系映射集合(HashMap)
private ConcurrentHashMap<String, ConcurrentHashMap<String, Binding>> bindingMap = new ConcurrentHashMap<>();
//key: messageId, value: message对象
private ConcurrentHashMap<String, Message> messageMap = new ConcurrentHashMap<>();
//key: queueName, value: 该队列下的消息对象的集合
private ConcurrentHashMap<String, LinkedList> queueMessageMap = new ConcurrentHashMap<>();
//key: queueName, value: 该队列下未应答的消息映射集合
//第二个key: messageId(UUID表示)
private ConcurrentHashMap<String, ConcurrentHashMap<String, Message>> queueMessageWaitAckMap = new ConcurrentHashMap<>();

//交换机
public void insertExchange(Exchange exchange){
exchangeMap.put(exchange.getName(), exchange);
log.info(“[MemoryDataCenter] 交换机插入成功, exchangeName:” + exchange.getName());
}

public void deleteExchange(String exchangeName){
exchangeMap.remove(exchangeName);
log.info(“[MemoryDataCenter] 交换机删除成功, exchangeName:” + exchangeName);
}

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

//队列
public void insertQueue(MSGQueue queue){
queueMap.put(queue.getName(), queue);
log.info(“[MemoryDataCenter] 队列插入成功, queueName:” + queue.getName());
}

public void deleteQueue(String queueName){
queueMap.remove(queueName);
log.info(“[MemoryDataCenter] 队列删除成功, queueName:” + queueName);
}

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

//绑定
public void insertBinding(Binding binding) throws MqException {
// ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.get(binding.getExchangeName());
ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.computeIfAbsent(binding.getExchangeName(), k -> new ConcurrentHashMap<>());
//检查嵌套的哈希表是否存在
synchronized (queueBindingMap){
//这样写需要再创建一把锁
// if(queueBindingMap == null){
// queueBindingMap = new ConcurrentHashMap<>();
// bindingMap.put(binding.getExchangeName(), queueBindingMap);
// }
if(queueBindingMap.get(binding.getQueueName()) != null){
throw new MqException(“[MemoryDataCenter] 绑定关系已经存在 exchangeName:” + binding.getExchangeName() +
“, queueName” + binding.getQueueName());
}
queueBindingMap.put(binding.getQueueName(), binding);
log.info(“[MemoryDataCenter] 绑定关系插入成功 exchangeName:”+ binding.getExchangeName() +
“, queueName” + binding.getQueueName());
}
}

public void deleteBinding(Binding binding) throws MqException {
ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.get(binding.getExchangeName());
if(queueBindingMap == null){
throw new MqException(“[MemoryDataCenter] 该绑定不存在 exchangeName:” + binding.getExchangeName() +
“, queueName” + binding.getQueueName());
}
bindingMap.remove(binding.getExchangeName());
log.info(“[MemoryDataCenter] 绑定关系删除成功 exchangeName:”+ binding.getExchangeName() +
“, queueName” + binding.getQueueName());
}

//两个版本的获取绑定:
//1.获取队列和交换机之间的唯一绑定
public Binding getBinding(String exchangeName, String queueName) throws MqException {
ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.get(exchangeName);
if(queueBindingMap == null){
return null;
}
return queueBindingMap.get(queueName);
}
//2.获取当前当前交换机下的所有绑定
public ConcurrentHashMap<String, Binding> getBindings(String exchangeName){
return bindingMap.get(exchangeName);
}

//消息
//向消息中心添加消息
public void addMessage(Message message){
messageMap.put(message.getMessageId(), message);
log.info(“[MemoryDataCenter] 消息添加完成 messageId:” + message.getMessageId());
}

public void removeMessage(String messageId){
messageMap.remove(messageId);
log.info(“[MemoryDataCenter] 消息删除完成 messageId:” + messageId);
}

public Message getMessage(String messageId){
return messageMap.get(messageId);
}

//将消息发送到指定队列
public void sendMessage(String queueName, Message message){
// LinkedList messages = queueMessageMap.get(queueName);
LinkedList messages = queueMessageMap.computeIfAbsent(queueName, k -> new LinkedList<>());
synchronized (messages){
//queueMessageMap.put(queueName, messages);
//将消息添加到队列中
messages.add(message);
}
//将消息添加到消息中心
addMessage(message);
log.info(“[MemoryDataCenter] 消息已成功添加到队列 queueName:” + queueName + “, 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 message = messages.remove(0);
log.info(“[MemoryDataCenter] 消息已成功从队列中取出 queueName:” + queueName + “, messageId:” + message.getMessageId());
return message;
}
}

//获取指定队列中消息的个数
public int getMessageSize(String queueName){
LinkedList messages = queueMessageMap.get(queueName);
if(messages == null){
return 0;
}
//LinkedList 线程不安全
synchronized (messages){
return messages.size();
}
}

//添加未被确认消息
public void addMessageWaitAck(String queueName, Message message){
// ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.get(queueName);
ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.computeIfAbsent(queueName, k -> new ConcurrentHashMap<>());
synchronized (messageWaitAck){
// if(messageWaitAck == null){
// messageWaitAck = new ConcurrentHashMap<>();
// queueMessageWaitAckMap.put(queueName, messageWaitAck);
// }
messageWaitAck.put(message.getMessageId(), message);
}
log.info(“[MemoryDataCenter] 消息进入待确认队列 queueName:” + queueName + “, messageId:” + message.getMessageId());
}

//删除未确认消息(用户确认了消息)
public void deleteMessageWaitAck(String queueName, String messageId){
ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.get(queueName);
if(messageWaitAck == null){
return;
}
messageWaitAck.remove(messageId);
log.info(“[MemoryDataCenter] 消息已从待确认队列中删除 queueName:” + queueName + “, messageId:” + messageId);
}

//从待确认队列中获取指定消息
public Message getMessageWaitAck(String queueName, String messageId){
ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.get(queueName);
if(messageWaitAck == null){
return null;
}
return messageWaitAck.get(messageId);
}

//从硬盘上恢复数据到内存中
public void recovery(DiskDataCenter diskDataCenter) throws MqException, IOException, ClassNotFoundException {
//1.清空之前的数据
exchangeMap.clear();
queueMap.clear();
bindingMap.clear();
messageMap.clear();
queueMessageMap.clear();
//2.恢复交换机
List exchangeList = diskDataCenter.selectAllExchanges();
for(Exchange exchange : exchangeList){
insertExchange(exchange);
}
//3.恢复队列
List queueList = diskDataCenter.selectAllQueues();
for(MSGQueue queue : queueList){
insertQueue(queue);
}
//4.恢复绑定
List bindingList = diskDataCenter.selectAllBindings();
for(Binding binding : bindingList){
insertBinding(binding);
}
//5.恢复消息
for(MSGQueue queue : queueList){
LinkedList messageList = diskDataCenter.loadAllMessageFromQueue(queue.getName());
queueMessageMap.put(queue.getName(), messageList);
for(Message message : messageList){
addMessage(message);
}
}
}
}

在内存中,需要考虑很多多线程的情况。对于add操作,往往会涉及到判空或者判断是否存在,有很大概率出现线程不安全的情况,因此需要加锁,而对于delete、select操作,加锁不一定是很必要的,需要具体问题具体分析。能用线程安全的尽量使用,但也要考虑锁粒度是否会影响效率~

虚拟主机

在虚拟主机中需要实现对内存硬盘的管理,并提供对交换机、队列、绑定、消息管理的核心API,供上层服务器调用(服务器主要处理网络请求,然后调用这边的API实现业务)。使用虚拟主机的目的是为了隔离交换机、队列、绑定、消息,一个服务器上可以有多个虚拟主机,在RabbitMQ中,就提供了对虚拟主机的创建、删除操作,但在本项目中先不实现。

要想实现隔离的效果,我们得知道这些交换机是从属于哪个虚拟主机的。不·难发现,一个虚拟机可以对应多个交换机,而一个交换机只能对应一个虚拟主机,这是一个"一对多"的关系。

方案一:参照数据库中“一对多”的关系来设计,给交换机增加一个属性,表示从属的虚拟主机;

方案二:给交换机增加一个前缀名,即为虚拟主机的名字,然后可以根据前缀知道从属的虚拟主机;

方案三:为每个虚拟主机单独分配数据库和文件,从物理上真正实现隔离。

此处选择的是方案二。


先来对交换机、队列、绑定的增删.

public class VirtualHost {
private String virtualHostName;
//硬盘数据管理
private DiskDataCenter diskDataCenter = new DiskDataCenter();
//内存数据管理
private MemoryDataCenter memoryDataCenter = new MemoryDataCenter();

private Router router = new Router();

private final Object exchangeLocker = new Object();

private final Object queueLocker = new Object();

public VirtualHost(String name){
//需要对数据进行初始化(数据库建库建表操作)
this.virtualHostName = name;
diskDataCenter.init();
//将硬盘上的数据恢复到内存中
try {
memoryDataCenter.recovery(diskDataCenter);
} catch (MqException | IOException | ClassNotFoundException e) {
log.error(“[VirtualHost] 恢复内存数据失败”);
e.printStackTrace();
}
}

//创建交换
public boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType, boolean durable,
boolean autoDelete, Map<String, Object> arguments) {
//为了进行虚拟主机的隔离,此处采用在交换机的前缀加上虚拟主机名的方式
exchangeName = virtualHostName + “_” + exchangeName;
try{
synchronized (exchangeLocker){
//1.判断交换机是否存在
Exchange exsitsExchange = memoryDataCenter.getExchange(exchangeName);
if(exsitsExchange != null){
throw new MqException(“[VirtualHost] 交换机已经存在 exchangeName:” + exchangeName);
}
//2.创建交换机
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);
log.info(“[VirtualHost] 交换机创建成功 exchangeName:” + exchangeName);
return true;
}
} catch (Exception e){
log.error(“[VirtualHost] 交换机创建失败 exchangeName:” + exchangeName);
e.printStackTrace();
return false;
}
}

//删除交换机
public boolean exchangeDelete(String exchangeName){
exchangeName = virtualHostName + “_” + exchangeName;
try{
synchronized (exchangeLocker){
//1.判断交换机是否存在
Exchange existsExchange = memoryDataCenter.getExchange(exchangeName);
if(existsExchange == null){
throw new MqException(“[VirtualHost] 交换机不存在 exchangeName:” + exchangeName);
}
//2.将交换机从硬盘上删除
//此处不要取反,isDurable 表示 是否存在硬盘上
//如果对象存储在硬盘上才要删除,没有存在硬盘上就不用
if(existsExchange.isDurable()){
diskDataCenter.deleteExchange(exchangeName);
}
//3.将交换机从内存中删除
memoryDataCenter.insertExchange(existsExchange);
log.info(“[VirtualHost] 交换机删除成功 exchangeName:” + exchangeName);
}
return true;
} catch (Exception e){
log.error(“[VirtualHost] 交换机删除失败 exchangeName:” + exchangeName);
e.printStackTrace();
return false;
}
}

//创建队列
public boolean queueDeclare(String queueName, boolean exclusive, boolean durable, boolean autoDelete,
Map<String, Object> arguments){
queueName = virtualHostName + “_” + queueName;
try{
synchronized (queueLocker){
MSGQueue existsQueue = memoryDataCenter.getQueue(queueName);
if(existsQueue != null){
throw new MqException(“[VirtualHost] 队列已经存在 queueName:” + queueName);
}
//创建队列
MSGQueue queue = new MSGQueue();
queue.setName(queueName);
queue.setDurable(durable);
queue.setExclusive(exclusive);
queue.setAutoDelete(autoDelete);
queue.setArguments(arguments);
//写入硬盘
if(durable){
diskDataCenter.insertQueue(queue);
}
//写入内存
memoryDataCenter.insertQueue(queue);
log.info(“[VirtualHost] 队列创建成功 queueName:” + queueName);
}
return true;
} catch (Exception e){
log.error(“[VirtualHost] 队列创建失败 queueName:” + queueName);
e.printStackTrace();
return false;
}
}

//删除队列
public boolean queueDelete(String queueName){
queueName = virtualHostName + “_” + queueName;
try{
synchronized (queueLocker){
MSGQueue existsQueue = memoryDataCenter.getQueue(queueName);
if(existsQueue == null){
throw new MqException(“[VirtualHost] 队列不存在 queueName:” + queueName);
}
if(existsQueue.isDurable()){
diskDataCenter.deleteQueue(queueName);
}
memoryDataCenter.deleteQueue(queueName);
log.error(“[VirtualHost] 队列删除成功 queueName:” + queueName);
return true;
}
}catch (Exception e){
log.error(“[VirtualHost] 队列删除失败 queueName:” + queueName);
e.printStackTrace();
return false;
}
}

//添加绑定关系
public boolean queueBind(String exchangeName, String queueName, String bindingKey){
exchangeName = virtualHostName + “" + exchangeName;
queueName = virtualHostName + "
” + queueName;
try{
synchronized (exchangeLocker){
synchronized (queueLocker){
//1.验证转发规则
//等下实现~
if(!router.checkBindingKey(bindingKey)){
throw new MqException(“[VirtualHost] bindingKey非法 bindingKey:” + bindingKey);
}
//2.判断绑定关系是否已经存在
Binding existsBinding = memoryDataCenter.getBinding(exchangeName, queueName);
if(existsBinding != null){
throw new MqException(“[VirtualHost] 绑定已经存在 exchangeName:” + exchangeName + “, queueName:” + queueName);
}
//3.判断交换机和队列是否都存在
Exchange existsExchange = memoryDataCenter.getExchange(exchangeName);
MSGQueue existsQueue = memoryDataCenter.getQueue(queueName);
if(existsExchange == null){
throw new MqException(“[VirtualHost] 交换机不存在 exchangeName:” + exchangeName);
}
if(existsQueue == null){
throw new MqException(“[VirtualHost] 队列不存在 queueName:” + queueName);
}
//4.创建绑定
Binding binding = new Binding();
binding.setExchangeName(exchangeName);
binding.setQueueName(queueName);
binding.setBindingKey(bindingKey);
//5.将绑定写入硬盘
diskDataCenter.insertBinding(binding);
//6.将绑定写入内存
memoryDataCenter.insertBinding(binding);
log.info(“[VirtualHost] 绑定添加成功 exchangeName:” + exchangeName + “, queueName:” + queueName);
}
}
return true;
} catch (Exception e){
log.error(“[VirtualHost] 绑定创建失败 exchangeName:” + exchangeName + “, queueName:” + queueName);
e.printStackTrace();
}
return false;
}

//解除绑定
public boolean queueUnbind(String exchangeName, String queueName){
exchangeName = virtualHostName + “" + exchangeName;
queueName = virtualHostName + "
” + queueName;
try{
synchronized (exchangeLocker){
synchronized (queueLocker){
//检查是否有这个绑定
Binding existsBinding = memoryDataCenter.getBinding(exchangeName, queueName);
if(existsBinding == null){
throw new MqException(“[VirtualHost] 绑定不存在 exchangeName:” + exchangeName + “, queueName:” + queueName);
}
//从硬盘中删除绑定关系
diskDataCenter.deleteBinding(existsBinding);
//从内存中删除绑定关系
memoryDataCenter.deleteBinding(existsBinding);
log.info(“[VirtualHost] 绑定解除成功 exchangeName:” + exchangeName + “, queueName:” + queueName);
}
}
return true;
} catch (Exception e){
log.error(“[VirtualHost] 绑定解除失败 exchangeName:” + exchangeName + “, queueName:” + queueName);
e.printStackTrace();
return false;
}
}

虽然我们在MemoryDataCenter中已经进行过了加锁操作,但是现在又再VirtualHost中又进行了加锁,是否之前的锁就没有意义了呢?

当然不是,因为无法事先知道什么时候、在哪去调用这些API,如果一个其他线程不安全的类去调用MemoryDataCenter中的一些API,就会引发线程安全问题。

在VirtualHost中定义了一把 交换机锁、 一把队列锁,这些锁的力度比较大,当一个线程去操作交换机A,此时另一个线程即使操作的是交换机B,也会进行阻塞,这会大大降低程序效率。不过影响不是很大,因为上述的这些操作都属于是低频操作,消息队列主要还是消息的转发~


上述实现了一些辅助消息交换转发的API,现在要来实现消息队列中最核心的两个API:发布消息 basicPublish 和 订阅消息 basicConsume.

basicPublish发布消息

发布消息是指生产者将一个消息发送到指定交换机上,然后交换机根据自身的类型,进行消息的转发,发送到与之绑定的队列中。

交换机有3种类型(本项目中实现了3种):

1.Dircet 直接交换机,消息中的routinKey就是队列名,直接将消息发送给队列即可.

2.Fanout 扇出交换机,消息会发送到所有与之绑定的队列中,此时消息中的routingKey无效.

3.Topic 主题交换机,消息会发送给所有与之绑定且binding中的bindingKey 与 消息中的 rouingKey 匹配的队列.

首先我们先来实现路由转发的规则,大致包含如下几个API:checkRoutingKey、checkBindingKey、route.

Router:

这个类用来实现路由转发的规则
/
public class Router {
//规定:
//bindingKey 由 数字,字母,下划线组成,通过.来分隔,允许通配符
,#存在
//形如: 123.abc..aa.#
public boolean checkBindingKey(String bindingKey) {
for (int i = 0; i < bindingKey.length(); i++) {
char ch = bindingKey.charAt(i);
if ((ch >= ‘a’ && ch <= ‘z’) || (ch >= ‘0’ && ch <= ‘9’) || (ch >= ‘A’ && ch <= ‘Z’)) {
continue;
}
if (ch == ‘.’ || ch == '
’ || ch == '
’ || ch == ‘#’) {
continue;
}
return false;
}
//这里处理通配符不合法的情况
//在正则表达式中需要对.进行转义 -> .
//而 \ 也需要进行转义,再添加一个 \
String[] words = bindingKey.split(“\.”);
for (int i = 0; i < words.length; i++) {
String word = words[i];
if (word.length() > 1 && (word.contains(““) || word.contains(”#“))) {
return false;
}
if (word.equals(”#“)) {
if (i + 1 < words.length && (words[i + 1].equals(”#“) || words[i + 1].equals(”
”))) {
return false;
}
}
}
return true;
}

//规定:
//routingKey 由 数字,字母,下划线组成,通过.来分隔
//形如: 123.abc.123
public boolean checkRoutingKey(String routingKey) {
for (int i = 0; i < routingKey.length(); i++) {
char ch = routingKey.charAt(i);
if ((ch >= ‘a’ && ch <= ‘z’) || (ch >= ‘0’ && ch <= ‘9’) || (ch >= ‘A’ && ch <= ‘Z’)) {
continue;
}
if (ch == ‘.’ || ch == '
') {
continue;
}
return false;
}
return true;
}

//进行路由
public boolean route(ExchangeType exchangeType, String routingKey, String bindingKey) {
if (exchangeType == ExchangeType.FANOUT) {
return true;
}
if (exchangeType == ExchangeType.TOPIC) {
if (routeTopic(routingKey, bindingKey)) {
return true;
}
}
return false;
}

//主题交换机的路由
//可以代表两个.之间的任意的字符串
//#可以代表0个或多个

private boolean routeTopic(String routingKey, String bindingKey) {
String[] routingTokens = routingKey.split(“\.”);
String[] bindingTokens = bindingKey.split(“\.”);
int routingIndex = 0;
int bindingIndex = 0;
while(routingIndex < routingTokens.length && bindingIndex < bindingTokens.length){
if(bindingTokens[bindingIndex].equals(“#”)){
if(bindingIndex == bindingTokens.length - 1){
return true;
}else {
bindingIndex++;
for(int i = routingIndex; i < routingTokens.length; i++){
if(bindingTokens[bindingIndex].equals(routingTokens[routingIndex])){
break;
}
}
if(routingIndex == routingTokens.length){
return false;
}
}
}else{
if(!routingTokens[routingIndex].equals(bindingTokens[bindingIndex])
&& !bindingTokens[bindingIndex].equals(“*”)){
return false;
}
}
routingIndex++;
bindingIndex++;
}
if(routingIndex == routingTokens.length && bindingIndex == bindingTokens.length){
return true;
}
return false;
}
}

上述routingKey、bindingKey、路由规则 都是源自于AMQP协议

接下来实现消息的发布。

basicPublish:

//向指定交换机发送消息,并根据交换机的转发规则,向队列发送消息
public boolean basicPublish(String exchangeName, String routingKey, BasicProperties basicProperties, byte[] body){
exchangeName = virtualHostName + “" + exchangeName;
try{
//1.判断交换机是否存在
Exchange exchange = memoryDataCenter.getExchange(exchangeName);
if(exchange == null){
throw new MqException(“[VirtualHost] 交换机不存在 exchangeName:” + exchangeName);
}
//2.判断routingKey是否合法
if(!router.checkRoutingKey(routingKey)){
throw new MqException(“[VirtualHost] routingKey非法 routingKey:” + routingKey);
}
//3.检查交换机类型
if(exchange.getType() == ExchangeType.DIRECT){
//直接交换机, routingKey为queueName
//4.查找指定队列
String queueName = virtualHostName + "
” + routingKey;
MSGQueue queue = memoryDataCenter.getQueue(queueName);
if(queue == null){
throw new MqException(“[VirtualHost] 队列不存在 queueName:” + queueName);
}
//5.构造message对象
Message message = Message.createMessageWithId(basicProperties, routingKey, body);
//6.发送消息到指定队列
sendMessage(queue, message);
}else{
//4.获取到所有绑定关系
ConcurrentHashMap<String, Binding> bindings = memoryDataCenter.getBindings(exchangeName);
for(Map.Entry<String, Binding> entry : bindings.entrySet()){

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数大数据工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)
img

row new MqException(“[VirtualHost] 交换机不存在 exchangeName:” + exchangeName);
}
//2.判断routingKey是否合法
if(!router.checkRoutingKey(routingKey)){
throw new MqException(“[VirtualHost] routingKey非法 routingKey:” + routingKey);
}
//3.检查交换机类型
if(exchange.getType() == ExchangeType.DIRECT){
//直接交换机, routingKey为queueName
//4.查找指定队列
String queueName = virtualHostName + “_” + routingKey;
MSGQueue queue = memoryDataCenter.getQueue(queueName);
if(queue == null){
throw new MqException(“[VirtualHost] 队列不存在 queueName:” + queueName);
}
//5.构造message对象
Message message = Message.createMessageWithId(basicProperties, routingKey, body);
//6.发送消息到指定队列
sendMessage(queue, message);
}else{
//4.获取到所有绑定关系
ConcurrentHashMap<String, Binding> bindings = memoryDataCenter.getBindings(exchangeName);
for(Map.Entry<String, Binding> entry : bindings.entrySet()){

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数大数据工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-XAvIJ8wV-1712583308315)]
[外链图片转存中…(img-T6TzJWKW-1712583308316)]
[外链图片转存中…(img-BU1bf5hN-1712583308316)]
[外链图片转存中…(img-W8m3b1n0-1712583308316)]
[外链图片转存中…(img-PGLVndG1-1712583308317)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上大数据开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注大数据获取)
[外链图片转存中…(img-PgMWReVf-1712583308317)]

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值