前言
在大学时期用过NIO手写消息队列,那时候还不会Netty,用原生的NIO出现了很多问题,例如自定义消息协议后socket无法正常关闭,需要手动修改缓冲区大小等问题。最近刚好学习了Netty,准备用Netty去重写这个消息队列。我将会带着大家从零开始,手写一款高性能、高安全性、持久化的消息队列。源码会同步到Github:https://github.com/Lyx0912/XY-MQ。感兴趣的可以点个Star!!!
介绍
在当时Nio编写完成后,就已经拥有超高性能了,每秒钟可以生产和消费消息20000+。在使用Netty完成基本架构后尝试发送100万消息也是几秒钟的事,而且还是经过本地磁盘存储的(要是早点知道Netty就好了)。这次用Netty重写,是想要写一款拥有高性能、高并发、高可靠性、轻量级的消息队列。
而且,在手写消息队列的时候使用了很多的设计模式,例如策略模式、观察者模式、工厂模式等,在阅读源码的时候可以了解这些设计模式的实际场景!!!
技术选型
Netty+LevelDb+SpringBoot+FastJson。消息队列的吞吐量局限于持久化的方式,为了提高性能,就放弃了传统的数据库,选择了高性能的KV数据库:LevelDb。选型SpringBoot就是为了方便后期搭建可视化Web页面,同时还可以直接使用定时任务和异步任务。FastJson用来作为消息对象的解析和封装,后面可能会换成Protobuf协议。
系统架构

持久化模型

文件结构
服务端:

客户端:

公共模块:

部分代码
服务端核心类
/**
* 服务端初始化工作
*
* @return void
* @author 黎勇炫
* @create 2022/7/9
* @email 1677685900@qq.com
*/
public void init() {
ServerBootstrap server = new ServerBootstrap();
try {
server.group(bossGroup, ioGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, backLog)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ServerInitializer());
ChannelFuture sync = server.bind(port);
// 数据恢复
recoveryMessage();
// 开始推送消息
sendMessageToClients();
sendDelayMessageToClient();
sendMessageToSubscriber();
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException("netty服务端启动失败");
} finally {
// 关闭两个工作线程组
bossGroup.shutdownGracefully();
ioGroup.shutdownGracefully();
}
}
消息持久化
public void putMessage(Long key, Message message) {
byte[] messageByte = JSON.toJSONBytes(message, new SerializerFeature[]{SerializerFeature.DisableCircularReferenceDetect});
try {
db.put(String.valueOf(key).getBytes(charset), messageByte);
} catch (UnsupportedEncodingException e) {
logger.error("消息编号{}持久化失败",message.getMessageId());
throw new XyException(ExceptionEnum.FAILED_TO_STORAGE);
}
}
推送消息(队列)
public void sendMessageToClients() {
// 异步执行,遍历队列消息容器
CompletableFuture.runAsync(() -> {
try {
while (!bossGroup.isShutdown()) {
// 遍历整个队列容器
for (Map.Entry<String, LinkedBlockingDeque<Message>> entry : queueContainer.entrySet()) {
// key就是队列名
String key = entry.getKey();
LinkedBlockingDeque<Message> queue = entry.getValue();
// 当消息队列中有数据并且该队列存在消费者,就调用线程池,负责为该队列推送消息
while (queue.size() > 0 && consumerContainer.containsKey(key)) {
if (consumerContainer.get(key).size() != 0) {
Channel channel = getChannel(consumerContainer.get(key));
// 只有连接在活跃状态下才开始推送消息
if (channel.isActive()) {
// 发送消息到未断开连接的消费者
Message message = queue.poll();
MessageUtils.message2Protocol(message);
channel.writeAndFlush(MessageUtils.message2Protocol(message));
}
}
}
}
}
} catch (Exception e) {
logger.error("消息推送失败");
}
}, taskExecutor);
}
消息持久化接口
public interface StorageHelper {
/**
* 存储消息到队列消息容器
* @param queueContainer 队列消息容器
* @param message 消息对象
* @return void
* @author 黎勇炫
* @create 2022/7/10
* @email 1677685900@qq.com
*/
public void storeQueueMessage(ConcurrentHashMap<String, LinkedBlockingDeque<Message>> queueContainer, Message message);
/**
* 存储消息到主题消息容器
* @param topicContainer 主题消息容器
* @param message 消息对象
* @return void
* @author 黎勇炫
* @create 2022/7/10
* @email 1677685900@qq.com
*/
public void storeTopicMessage(ConcurrentHashMap<String, LinkedBlockingDeque<Message>> topicContainer, Message message);
/**
* 存储消息到延时队列容器
* @param delayQueueMap 延时队列容器
* @param message 消息对象
* @return void
* @author 黎勇炫
* @create 2022/7/10
* @email 1677685900@qq.com
*/
public void storeDelayMessage(ConcurrentHashMap<String, DelayQueue<Message>> delayQueueMap, Message message);
/**
* 存储消息到延时主题消息容器
* @param delayTopicMap 主题消息容器
* @param message 消息对象
* @return void
* @author 黎勇炫
* @create 2022/7/10
* @email 1677685900@qq.com
*/
public void storeDelayTopicMessage(ConcurrentHashMap<String, DelayQueue<Message>> delayTopicMap, Message message);
}
待优化
1.延迟消息BUG:延时消息基于jdk自带的delayQueue实现,系统宕机重启后服务端读取leveldb中的消息后将消息重新放回延时队列,会重新设置到期时间。例如:设置一条消息5分钟后推送,中途系统宕机,系统重启后会从当前时间开始重新计时5分钟。
2.异步推送下消息乱序:原先设想是每条队列来消息时,就会交给线程池专门用一条线程负责推送这条队列的消息,直到消息推送完毕。结果会打乱消息顺序。
3.可视化界面:在所有的功能开发完后会开发一个可视化界面。
3137

被折叠的 条评论
为什么被折叠?



