消息队列(Message Queue,简称MQ)指保存消息的一个容器,其实本质就是一个保存数据的队列。消息中间件是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的构建。
消息中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削峰等问题,实现高性能,高可用,可伸缩和最终一致性的系统架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ等。
特性 | Kafka | RocketMQ | RabbitMQ | ActiveMQ |
---|---|---|---|---|
单机吞吐量 | 10万级 | 10万级 | 万级 | 10万级 |
开发语言 | Scala | Java | Erlang | Java |
高可用 | 分布式 | 分布式 | 主从 | 分布式 |
消息延迟 | ms级 | ms级 | us级 | ms级 |
消息丢失 | 理论上不会丢失 | 理论上不会丢失 | 低 | 低 |
消费模式 | 拉取 | 推拉 | 推拉 | |
持久化 | 文件 | 内存,文件 | 内存,文件,数据库 | |
支持协议 | 自定义协议 | 自定义协议 | AMQP,XMPP, SMTP,STOMP | AMQP,MQTT,OpenWire,STOMP |
社区活跃度 | 高 | 中 | 高 | 高 |
管理界面 | web console | 好 | 一般 | |
部署难度 | 中 | 低 | ||
部署方式 | 独立 | 独立 | 独立 | 独立,嵌入 |
成熟度 | 成熟 | 比较成熟 | 成熟 | 成熟 |
综合评价 | 优点:拥有强大的性能及吞吐量,兼容性很好。 缺点:由于支持消息堆积,导致延迟比较高。 | 优点:性能好,稳定可靠,有活跃的中文社区,特点响应快。 缺点:兼容性较差,但随着影响力的扩大,该问题会有改善。 | 优点:产品成熟,容易部署和使用,拥有灵活的路由配置。 缺点:性能和吞吐量较差,不易进行二次开发。 | 优点:产品成熟,支持协议多,支持多种语言的客户端。 缺点:社区不活跃,存在消息丢失的可能。 |
RocketMQ MQTT 概览
传统的消息队列MQ主要应用于服务(端)之间的消息通信,比如电商领域的交易消息、支付消息、物流消息等等。然而在消息这个大类下,还有一个非常重要且常见的消息领域,即IoT类终端设备消息。近些年,我们看到随着智能家居、工业互联而兴起的面向IoT设备类的消息正在呈爆炸式增长,而且已经发展十余年的移动互联网的手机APP端消息仍然是数量级庞大。面向终端设备的消息数量级比传统服务端的消息要大很多量级并仍然在快速增长。
如果可以有一个统一的消息系统(产品)来提供多场景计算(如stream、event)、多场景(IoT、APP)接入,其实是非常有价值的,因为消息也是一种重要数据,数据如果只存在一个系统内,可以最大地降低存储成本,同时可以有效地避免数据因在不同系统间同步带来的一致性难题和挑战。
基于此,我们引入了RocketMQ-MQTT这个扩展项目来实现RocketMQ统一接入IoT设备和服务端的消息,提供一体化消息存储和互通能力。
MQTT协议
在IoT终端场景,目前业界广泛使用的是MQTT协议,是起源于物联网IoT场景,OASIS联盟定义的标准的开放式协议。因为IoT设备种类繁多,运行环境各异,一个标准的接入协议尤为关键。
MQTT协议定义的是一个Pub/Sub的通信模型,这个与RocketMQ是类似的,不过其在订阅方式上比较灵活,可以支持多级Topic订阅(如 “/t/t1/t2”),甚至可以支持通配符订阅(如 “/t/t1/+”)。
模型介绍
队列存储模型
我们设计了一种多维度分发的Topic队列模型,如上图所示,消息可以来自各个接入场景(如服务端的MQ/AMQP、客户端的MQTT),但只会写一份存到commitlog里面,然后分发出多个需求场景的队列索引(ConsumerQueue),如服务端场景(MQ/AMQP)可以按照一级Topic队列进行传统的服务端消费,客户端MQTT场景可以按照MQTT多级Topic以及通配符订阅进行消费消息。
这样的一个队列模型就可以同时支持服务端和终端场景的接入和消息收发,达到一体化的目标。
推拉模型
上图展示的是一个推拉模型,图中的P节点是一个协议网关或broker插件,终端设备通过MQTT协议连到这个网关节点。消息可以来自多种场景(MQ/AMQP/MQTT)发送过来,存到Topic队列后会有一个notify逻辑模块来实时感知这个新消息到达,然后会生成消息事件(就是消息的Topic名称),将该事件推送至网关节点,网关节点根据其连上的终端设备订阅情况进行内部匹配,找到哪些终端设备能匹配上,然后会触发pull请求去存储层读取消息再推送至终端设备。
架构概览
我们的目标是期望基于RocketMQ实现一体化且自闭环,但不希望Broker被侵入更多场景逻辑,我们抽象了一个协议计算层,这个计算层可以是一个网关,也可以是一个broker插件。Broker专注解决Queue的事情以及为了满足上面的计算需求做一些Queue存储的适配或改造。协议计算层负责协议接入,并且要可插拔部署。
RocketMQ MQTT 使用
- 64位 JDK 1.8+
MQTT客户端启动订阅消息
import org.apache.rocketmq.mqtt.common.util.HmacSHA1Util;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* MQTT消费者类,用于接收MQTT消息
*/
public class MqttConsumer {
/**
* 主函数,负责初始化MQTT客户端并连接到MQTT代理
*
* @param args 命令行参数
* @throws MqttException 当MQTT操作失败时抛出
* @throws NoSuchAlgorithmException 当找不到算法时抛出
* @throws InvalidKeyException 当密钥无效时抛出
*/
public static void main(String[] args) throws MqttException, NoSuchAlgorithmException, InvalidKeyException {
// 构造MQTT代理URL和主题
String brokerUrl = "tcp://" + System.getenv("host") + ":1883";
String firstTopic = System.getenv("topic");
// 使用内存持久化
MemoryPersistence memoryPersistence = new MemoryPersistence();
String recvClientId = "recv01";
// 构造MQTT连接选项
MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(recvClientId);
// 创建MQTT客户端
MqttClient mqttClient = new MqttClient(brokerUrl, recvClientId, memoryPersistence);
mqttClient.setTimeToWait(5000L);
// 设置回调处理连接状态和消息接收
mqttClient.setCallback(new MqttCallbackExtended() {
@Override
public void connectComplete(boolean reconnect, String serverURI) {
System.out.println(recvClientId + " connect success to " + serverURI);
try {
// 订阅主题
final String topicFilter[] = {firstTopic + "/r1", firstTopic + "/r/+", firstTopic + "/r2"};
final int[] qos = {1, 1, 2};
mqttClient.subscribe(topicFilter, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
try {
// 处理接收到的消息
String payload = new String(mqttMessage.getPayload());
String[] ss = payload.split("_");
System.out.println(now() + "receive:" + topic + "," + payload);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
});
try {
// 尝试连接到MQTT代理
mqttClient.connect(mqttConnectOptions);
} catch (Exception e) {
e.printStackTrace();
System.out.println("connect fail");
}
}
/**
* 构建MQTT连接选项
*
* @param clientId 客户端ID
* @return MQTT连接选项对象
* @throws NoSuchAlgorithmException 当找不到算法时抛出
* @throws InvalidKeyException 当密钥无效时抛出
*/
private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setKeepAliveInterval(60);
connOpts.setAutomaticReconnect(true);
connOpts.setMaxInflight(10000);
connOpts.setUserName(System.getenv("username"));
connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("password")).toCharArray());
return connOpts;
}
/**
* 获取当前时间字符串
*
* @return 当前时间字符串
*/
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}
MQTT客户端启动发布消息
/**
* MqttProducer 类用于演示如何使用 MQTT 协议向消息队列发送消息
*/
public class MqttProducer {
/**
* 程序的入口点
*
* @param args 命令行参数
* @throws InterruptedException 当线程被中断时抛出此异常
* @throws MqttException 当 MQTT 操作失败时抛出此异常
* @throws NoSuchAlgorithmException 当无法找到所需的加密算法时抛出此异常
* @throws InvalidKeyException 当密钥无效时抛出此异常
*/
public static void main(String[] args) throws InterruptedException, MqttException, NoSuchAlgorithmException, InvalidKeyException {
// 使用内存持久化来存储 MQTT 客户端的会话信息
MemoryPersistence memoryPersistence = new MemoryPersistence();
// 从环境变量中获取 MQTT 代理的 URL 和主题
String brokerUrl = "tcp://" + System.getenv("host") + ":1883";
String firstTopic = System.getenv("topic");
String sendClientId = "send01";
// 构建 MQTT 连接选项
MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(sendClientId);
// 创建 MQTT 客户端实例
MqttClient mqttClient = new MqttClient(brokerUrl, sendClientId, memoryPersistence);
// 设置客户端的等待时间和回调接口
mqttClient.setTimeToWait(5000L);
mqttClient.setCallback(new MqttCallbackExtended() {
// 连接成功时调用
@Override
public void connectComplete(boolean reconnect, String serverURI) {
System.out.println(sendClientId + " connect success to " + serverURI);
}
// 连接丢失时调用
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
// 消息到达时调用(此处未实现具体逻辑)
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) {
}
// 消息发送完成时调用(此处未实现具体逻辑)
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
});
// 尝试连接到 MQTT 代理
try {
mqttClient.connect(mqttConnectOptions);
} catch (Exception e) {
e.printStackTrace();
}
// 设置消息发送的间隔时间
long interval = 1000;
// 循环发送不同类型和质量的消息到不同的主题
for (int i = 0; i < 1000; i++) {
// 发送第一种类型的消息
String msg = "r1_" + System.currentTimeMillis() + "_" + i;
MqttMessage message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
message.setQos(1);
String mqttSendTopic = firstTopic + "/r1";
mqttClient.publish(mqttSendTopic, message);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
Thread.sleep(interval);
// 发送第二种类型的消息
mqttSendTopic = firstTopic + "/r/wc";
msg = "wc_" + System.currentTimeMillis() + "_" + i;
MqttMessage messageWild = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
messageWild.setQos(1);
mqttClient.publish(mqttSendTopic, messageWild);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
Thread.sleep(interval);
// 发送第三种类型的消息
mqttSendTopic = firstTopic + "/r2";
msg = "msgQ2_" + System.currentTimeMillis() + "_" + i;
message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
message.setQos(2);
mqttClient.publish(mqttSendTopic, message);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
Thread.sleep(interval);
}
}
/**
* 构建 MQTT 连接选项
*
* @param clientId MQTT 客户端的 ID
* @return MQTT 连接选项对象
* @throws NoSuchAlgorithmException 当无法找到所需的加密算法时抛出此异常
* @throws InvalidKeyException 当密钥无效时抛出此异常
*/
private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setKeepAliveInterval(60);
connOpts.setAutomaticReconnect(true);
connOpts.setMaxInflight(10000);
connOpts.setUserName(System.getenv("username"));
connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("password")).toCharArray());
return connOpts;
}
/**
* 获取当前时间的字符串表示
*
* @return 当前时间的字符串表示
*/
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}
MQTT遗嘱消息,保留消息的消费
遗嘱消息(Will Message)
定义:遗嘱消息是指当一个 MQTT 客户端意外断开连接时,MQTT Broker 会自动向指定的主题发布一条预定义的消息。
用途:通常用于通知其他订阅者某个客户端已离线,例如设备状态监控系统可以通过遗嘱消息报告设备的离线状态。
保留消息(Retained Message)
定义:保留消息是指当一个客户端发布消息到某个主题时,Broker 会保存该消息的最新版本。后续订阅该主题的新客户端会立即收到这条保留消息。
用途:适用于需要客户端获取最新状态或配置信息的场景,例如智能家居系统中的设备状态更新。
/**
* MQTT意志保留消费者类
* 该类展示了如何使用MQTT协议连接到消息代理,并消费保留的消息
*/
public class MqttWillRetainConsumer {
/**
* 主函数执行MQTT客户端的连接和消息消费
* @param args 命令行参数
* @throws MqttException 如果MQTT客户端操作失败
* @throws NoSuchAlgorithmException 如果找不到所需的加密算法
* @throws InvalidKeyException 如果加密密钥无效
*/
public static void main(String[] args) throws MqttException, NoSuchAlgorithmException, InvalidKeyException {
// 构造broker URL和主题
String brokerUrl = "tcp://" + System.getenv("host") + ":1883";
String firstTopic = System.getenv("topic");
MemoryPersistence memoryPersistence = new MemoryPersistence();
String recvClientId = "recv02";
// 构造MQTT连接选项
MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(recvClientId);
// 创建MQTT客户端实例
MqttClient mqttClient = new MqttClient(brokerUrl, recvClientId, memoryPersistence);
mqttClient.setTimeToWait(5000L);
// 设置回调处理连接状态和消息消费
mqttClient.setCallback(new MqttCallbackExtended() {
@Override
public void connectComplete(boolean reconnect, String serverURI) {
System.out.println(recvClientId + " connect success to " + serverURI);
try {
// 订阅主题
final String topicFilter[] = {firstTopic + "/retainTopicR",firstTopic + "/retainTopic/+", firstTopic + "/willTopic1",};
final int[] qos = {1, 1, 1};
mqttClient.subscribe(topicFilter, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
try {
// 处理接收到的消息
String payload = new String(mqttMessage.getPayload());
System.out.println(now() + "receive:" + topic + "," + payload);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
// 处理消息交付完成的情况
}
});
try {
// 尝试连接到broker
mqttClient.connect(mqttConnectOptions);
} catch (Exception e) {
e.printStackTrace();
System.out.println("connect fail");
}
}
/**
* 构建MQTT连接选项
* @param clientId 客户端ID
* @return MQTT连接选项对象,配置了客户端的连接参数
* @throws NoSuchAlgorithmException 如果找不到所需的加密算法
* @throws InvalidKeyException 如果加密密钥无效
*/
private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setKeepAliveInterval(60);
connOpts.setAutomaticReconnect(true);
connOpts.setMaxInflight(10000);
connOpts.setUserName(System.getenv("username"));
connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("password")).toCharArray());
return connOpts;
}
/**
* 获取当前时间字符串
* @return 当前时间的字符串表示,格式为"yyyy-MM-dd HH:mm:ss:SSS"
*/
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}
MQTT遗嘱消息,保留消息的发布
import org.apache.rocketmq.mqtt.common.util.HmacSHA1Util;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* MqttWillRetainProducer 类用于演示如何使用 MQTT 协议发布具有遗嘱消息和保留消息功能的消息
*/
public class MqttWillRetainProducer {
/**
* 程序的入口点
* @param args 命令行参数
* @throws InterruptedException 当线程被中断时抛出此异常
* @throws MqttException 当 MQTT 操作失败时抛出此异常
* @throws NoSuchAlgorithmException 当指定的算法不存在时抛出此异常
* @throws InvalidKeyException 当密钥无效时抛出此异常
*/
public static void main(String[] args) throws InterruptedException, MqttException, NoSuchAlgorithmException, InvalidKeyException {
// 使用内存持久化
MemoryPersistence memoryPersistence = new MemoryPersistence();
// 从环境变量中获取 MQTT 经纪人的 URL 和主题
String brokerUrl = "tcp://" + System.getenv("host") + ":1883";
String firstTopic = System.getenv("topic");
String sendClientId = "send02";
// 构建 MQTT 连接选项
MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(sendClientId);
// 设置遗愿消息,当客户端断连时,broker 会发布此消息
mqttConnectOptions.setWill(firstTopic + "/willTopic1", "will message: hello".getBytes(), 1, false);
// 创建 MQTT 客户端实例
MqttClient mqttClient = new MqttClient(brokerUrl, sendClientId, memoryPersistence);
// 设置客户端的等待时间和回调
mqttClient.setTimeToWait(5000L);
mqttClient.setCallback(new MqttCallbackExtended() {
// 连接成功时调用
@Override
public void connectComplete(boolean reconnect, String serverURI) {
System.out.println(sendClientId + " connect success to " + serverURI);
}
// 连接丢失时调用
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
// 消息到达时调用,此处未实现
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) {
}
// 消息投递完成时调用,此处未实现
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
});
// 尝试连接到 MQTT 经纪人
try {
mqttClient.connect(mqttConnectOptions);
} catch (Exception e) {
e.printStackTrace();
}
// 构建带有时间戳的消息,并设置为保留消息
String msg = "r_" + System.currentTimeMillis();
MqttMessage message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
message.setQos(1);
message.setRetained(true);
String mqttSendTopic = firstTopic + "/retainTopicR";
mqttClient.publish(mqttSendTopic, message);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
Thread.sleep(1000);
// 再次构建消息并发布到不同的主题
message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
message.setQos(1);
message.setRetained(true);
mqttSendTopic = firstTopic + "/retainTopic/wc";
mqttClient.publish(mqttSendTopic, message);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
// 发布一个空的保留消息到另一个主题
message = new MqttMessage();
message.setQos(1);
message.setRetained(true);
mqttSendTopic = firstTopic + "/retainTopic/2";
mqttClient.publish(mqttSendTopic, message);
}
/**
* 构建 MQTT 连接选项
* @param clientId 客户端 ID
* @return MQTT 连接选项对象
* @throws NoSuchAlgorithmException 当指定的算法不存在时抛出此异常
* @throws InvalidKeyException 当密钥无效时抛出此异常
*/
private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setKeepAliveInterval(60);
connOpts.setAutomaticReconnect(true);
connOpts.setMaxInflight(10000);
// 从环境变量中获取用户名和密码,并使用 HmacSHA1 算法对密码进行加密
connOpts.setUserName(System.getenv("username"));
connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("password")).toCharArray());
return connOpts;
}
/**
* 获取当前时间的字符串表示
* @return 当前时间的字符串表示
*/
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}