添加redis命令的支持

This commit is contained in:
孙小云 2025-12-18 14:49:43 +08:00
parent 3a074b52da
commit c218d2ae81
5 changed files with 593 additions and 45 deletions

View File

@ -1,31 +1,81 @@
package com.tuoheng.machine.mqtt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tuoheng.machine.mqtt.store.MqttCallbackInfo;
import com.tuoheng.machine.mqtt.store.MqttCallbackStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
/**
* MQTT回调注册中心
* 用于注册和管理MQTT消息的回调处理器,他的 handleMessage 需要被真实的MQTT回调去调用
*
* 架构说明
* - 回调元数据存储在 MqttCallbackStore 支持内存/Redis
* - Consumer<Object> 回调函数存储在本地内存中无法序列化
* - 多节点部署时通过 Redis Pub/Sub 在节点间传递消息
*/
@Slf4j
@Component
public class MqttCallbackRegistry {
/**
* 主题 -> 回调处理器列表
* 回调存储层支持内存Redis等多种实现
*/
private final Map<String, CopyOnWriteArrayList<MqttCallbackHandler>> topicHandlers = new ConcurrentHashMap<>();
private final MqttCallbackStore callbackStore;
/**
* 回调ID -> 回调处理器
* 回调ID -> 本地消息处理器Consumer 无法序列化只能存储在本地
*/
private final Map<String, MqttCallbackHandler> handlerMap = new ConcurrentHashMap<>();
private final Map<String, Consumer<Object>> localHandlers = new ConcurrentHashMap<>();
/**
* 当前节点ID用于 Redis Pub/Sub 路由
*/
private String nodeId;
/**
* ObjectMapper 用于序列化消息
*/
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${machine.node.id:#{null}}")
private String configuredNodeId;
public MqttCallbackRegistry(MqttCallbackStore callbackStore) {
this.callbackStore = callbackStore;
}
@PostConstruct
public void init() {
// 初始化节点ID
if (configuredNodeId != null && !configuredNodeId.isEmpty()) {
nodeId = configuredNodeId;
} else {
// 自动生成节点ID主机名 + UUID
try {
String hostname = InetAddress.getLocalHost().getHostName();
nodeId = hostname + "-" + UUID.randomUUID().toString().substring(0, 8);
} catch (Exception e) {
nodeId = "node-" + UUID.randomUUID().toString().substring(0, 8);
}
}
// 订阅当前节点的消息用于 Redis Pub/Sub
callbackStore.subscribeNodeMessages(nodeId, this::handleNodeMessage);
log.info("MQTT回调注册中心初始化完成节点ID: {}, 存储实现: {}",
nodeId, callbackStore.getClass().getSimpleName());
}
/**
* 注册回调
@ -37,18 +87,23 @@ public class MqttCallbackRegistry {
*/
public String registerCallback(String topic, Consumer<Object> messageHandler, long timeoutMs) {
String callbackId = UUID.randomUUID().toString();
MqttCallbackHandler handler = new MqttCallbackHandler(
callbackId,
topic,
messageHandler,
timeoutMs,
System.currentTimeMillis()
);
topicHandlers.computeIfAbsent(topic, k -> new CopyOnWriteArrayList<>()).add(handler);
handlerMap.put(callbackId, handler);
// 1. 创建回调信息并存储到存储层
MqttCallbackInfo callbackInfo = MqttCallbackInfo.builder()
.callbackId(callbackId)
.topic(topic)
.timeoutMs(timeoutMs)
.registerTime(System.currentTimeMillis())
.nodeId(nodeId)
.build();
log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}", callbackId, topic, timeoutMs);
callbackStore.registerCallback(callbackInfo);
// 2. Consumer 存储到本地内存
localHandlers.put(callbackId, messageHandler);
log.debug("注册MQTT回调: callbackId={}, topic={}, timeoutMs={}, nodeId={}",
callbackId, topic, timeoutMs, nodeId);
return callbackId;
}
@ -58,68 +113,127 @@ public class MqttCallbackRegistry {
* @param callbackId 回调ID
*/
public void unregisterCallback(String callbackId) {
MqttCallbackHandler handler = handlerMap.remove(callbackId);
if (handler != null) {
CopyOnWriteArrayList<MqttCallbackHandler> handlers = topicHandlers.get(handler.getTopic());
if (handlers != null) {
handlers.remove(handler);
if (handlers.isEmpty()) {
topicHandlers.remove(handler.getTopic());
}
}
log.debug("取消注册MQTT回调: callbackId={}, topic={}", callbackId, handler.getTopic());
}
// 1. 从存储层删除回调信息
callbackStore.unregisterCallback(callbackId);
// 2. 从本地内存删除 Consumer
localHandlers.remove(callbackId);
log.debug("取消注册MQTT回调: callbackId={}", callbackId);
}
/**
* 处理接收到的MQTT消息
* 处理接收到的MQTT消息由真实的 MQTT 客户端调用
*
* @param topic 主题
* @param messageBody 消息体
*/
public void handleMessage(String topic, Object messageBody) {
CopyOnWriteArrayList<MqttCallbackHandler> handlers = topicHandlers.get(topic);
if (handlers == null || handlers.isEmpty()) {
// 1. 从存储层获取所有等待该 topic 的回调信息
List<MqttCallbackInfo> callbacks = callbackStore.getCallbacksByTopic(topic);
if (callbacks.isEmpty()) {
return;
}
log.debug("处理MQTT消息: topic={}, handlerCount={}", topic, handlers.size());
log.debug("处理MQTT消息: topic={}, callbackCount={}", topic, callbacks.size());
for (MqttCallbackHandler handler : handlers) {
// 2. 序列化消息体用于跨节点传递
String messageBodyJson;
try {
messageBodyJson = objectMapper.writeValueAsString(messageBody);
} catch (Exception e) {
log.error("序列化消息体失败: topic={}", topic, e);
return;
}
// 3. 处理每个回调
for (MqttCallbackInfo callbackInfo : callbacks) {
try {
// 检查是否超时
if (handler.isTimeout()) {
log.warn("MQTT回调已超时: callbackId={}, topic={}", handler.getCallbackId(), topic);
unregisterCallback(handler.getCallbackId());
if (callbackInfo.isTimeout()) {
log.warn("MQTT回调已超时: callbackId={}, topic={}",
callbackInfo.getCallbackId(), topic);
unregisterCallback(callbackInfo.getCallbackId());
continue;
}
// 执行回调
handler.getMessageHandler().accept(messageBody);
// 判断回调是在本节点还是其他节点
if (nodeId.equals(callbackInfo.getNodeId())) {
// 本节点的回调直接执行
executeLocalCallback(callbackInfo.getCallbackId(), messageBody);
} else {
// 其他节点的回调通过 Redis Pub/Sub 转发
callbackStore.publishMessageToNode(
callbackInfo.getNodeId(),
callbackInfo.getCallbackId(),
messageBodyJson
);
log.debug("转发消息到节点: nodeId={}, callbackId={}",
callbackInfo.getNodeId(), callbackInfo.getCallbackId());
}
} catch (Exception e) {
log.error("执行MQTT回调失败: callbackId={}, topic={}", handler.getCallbackId(), topic, e);
log.error("处理MQTT回调失败: callbackId={}, topic={}",
callbackInfo.getCallbackId(), topic, e);
}
}
}
/**
* 执行本地回调
*
* @param callbackId 回调ID
* @param messageBody 消息体
*/
private void executeLocalCallback(String callbackId, Object messageBody) {
Consumer<Object> handler = localHandlers.get(callbackId);
if (handler != null) {
try {
handler.accept(messageBody);
log.debug("执行本地回调成功: callbackId={}", callbackId);
} catch (Exception e) {
log.error("执行本地回调失败: callbackId={}", callbackId, e);
}
} else {
log.warn("本地回调处理器不存在: callbackId={}", callbackId);
}
}
/**
* 处理从 Redis Pub/Sub 接收到的节点消息
*
* @param callbackId 回调ID
* @param messageBodyJson 消息体JSON 字符串
*/
private void handleNodeMessage(String callbackId, String messageBodyJson) {
try {
// 反序列化消息体
Object messageBody = objectMapper.readValue(messageBodyJson, Object.class);
// 执行本地回调
executeLocalCallback(callbackId, messageBody);
} catch (Exception e) {
log.error("处理节点消息失败: callbackId={}", callbackId, e);
}
}
/**
* 清理超时的回调
*/
public void cleanupTimeoutCallbacks() {
handlerMap.values().removeIf(handler -> {
if (handler.isTimeout()) {
log.warn("清理超时的MQTT回调: callbackId={}, topic={}", handler.getCallbackId(), handler.getTopic());
unregisterCallback(handler.getCallbackId());
return true;
List<MqttCallbackInfo> allCallbacks = callbackStore.getAllCallbacks();
for (MqttCallbackInfo callbackInfo : allCallbacks) {
if (callbackInfo.isTimeout()) {
log.warn("清理超时的MQTT回调: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic());
unregisterCallback(callbackInfo.getCallbackId());
}
return false;
});
}
}
/**
* 获取当前注册的回调数量
*/
public int getCallbackCount() {
return handlerMap.size();
return localHandlers.size();
}
}

View File

@ -0,0 +1,85 @@
package com.tuoheng.machine.mqtt.store;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* 基于内存的 MQTT 回调存储实现
* 适用于单节点部署或开发测试环境
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "memory", matchIfMissing = true)
public class InMemoryMqttCallbackStore implements MqttCallbackStore {
/**
* 主题 -> 回调信息列表
*/
private final Map<String, CopyOnWriteArrayList<MqttCallbackInfo>> topicCallbacks = new ConcurrentHashMap<>();
/**
* 回调ID -> 回调信息
*/
private final Map<String, MqttCallbackInfo> callbackMap = new ConcurrentHashMap<>();
@Override
public void registerCallback(MqttCallbackInfo callbackInfo) {
topicCallbacks.computeIfAbsent(callbackInfo.getTopic(), k -> new CopyOnWriteArrayList<>())
.add(callbackInfo);
callbackMap.put(callbackInfo.getCallbackId(), callbackInfo);
log.debug("注册MQTT回调到内存: callbackId={}, topic={}",
callbackInfo.getCallbackId(), callbackInfo.getTopic());
}
@Override
public void unregisterCallback(String callbackId) {
MqttCallbackInfo callbackInfo = callbackMap.remove(callbackId);
if (callbackInfo != null) {
CopyOnWriteArrayList<MqttCallbackInfo> callbacks = topicCallbacks.get(callbackInfo.getTopic());
if (callbacks != null) {
callbacks.remove(callbackInfo);
if (callbacks.isEmpty()) {
topicCallbacks.remove(callbackInfo.getTopic());
}
}
log.debug("从内存中取消注册MQTT回调: callbackId={}, topic={}",
callbackId, callbackInfo.getTopic());
}
}
@Override
public List<MqttCallbackInfo> getCallbacksByTopic(String topic) {
CopyOnWriteArrayList<MqttCallbackInfo> callbacks = topicCallbacks.get(topic);
return callbacks != null ? new ArrayList<>(callbacks) : new ArrayList<>();
}
@Override
public MqttCallbackInfo getCallbackById(String callbackId) {
return callbackMap.get(callbackId);
}
@Override
public List<MqttCallbackInfo> getAllCallbacks() {
return new ArrayList<>(callbackMap.values());
}
@Override
public void publishMessageToNode(String nodeId, String callbackId, String messageBody) {
// 内存实现中不需要跨节点通信此方法为空操作
log.trace("内存实现不需要发布消息到节点: nodeId={}, callbackId={}", nodeId, callbackId);
}
@Override
public void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener) {
// 内存实现中不需要订阅节点消息此方法为空操作
log.trace("内存实现不需要订阅节点消息: nodeId={}", nodeId);
}
}

View File

@ -0,0 +1,52 @@
package com.tuoheng.machine.mqtt.store;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* MQTT 回调信息可序列化到 Redis
* 不包含 Consumer只包含回调的元数据
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MqttCallbackInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 回调ID用于取消注册
*/
private String callbackId;
/**
* 监听的主题
*/
private String topic;
/**
* 超时时间毫秒
*/
private long timeoutMs;
/**
* 注册时间
*/
private long registerTime;
/**
* 注册该回调的节点ID用于 Redis Pub/Sub 路由
*/
private String nodeId;
/**
* 是否已超时
*/
public boolean isTimeout() {
return System.currentTimeMillis() - registerTime > timeoutMs;
}
}

View File

@ -0,0 +1,79 @@
package com.tuoheng.machine.mqtt.store;
import java.util.List;
/**
* MQTT 回调存储接口
* 提供回调信息的存储和获取抽象支持多种实现内存Redis等
*/
public interface MqttCallbackStore {
/**
* 注册回调信息
*
* @param callbackInfo 回调信息
*/
void registerCallback(MqttCallbackInfo callbackInfo);
/**
* 取消注册回调
*
* @param callbackId 回调ID
*/
void unregisterCallback(String callbackId);
/**
* 根据 topic 获取所有等待该 topic 的回调信息
*
* @param topic MQTT 主题
* @return 回调信息列表
*/
List<MqttCallbackInfo> getCallbacksByTopic(String topic);
/**
* 根据 callbackId 获取回调信息
*
* @param callbackId 回调ID
* @return 回调信息如果不存在返回 null
*/
MqttCallbackInfo getCallbackById(String callbackId);
/**
* 获取所有回调信息用于清理超时回调
*
* @return 所有回调信息列表
*/
List<MqttCallbackInfo> getAllCallbacks();
/**
* 发布消息到指定节点用于 Redis Pub/Sub
* 在内存实现中此方法为空操作
*
* @param nodeId 节点ID
* @param callbackId 回调ID
* @param messageBody 消息体JSON 字符串
*/
void publishMessageToNode(String nodeId, String callbackId, String messageBody);
/**
* 订阅当前节点的消息用于 Redis Pub/Sub
* 在内存实现中此方法为空操作
*
* @param nodeId 当前节点ID
* @param messageListener 消息监听器
*/
void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener);
/**
* 节点消息监听器
*/
interface NodeMessageListener {
/**
* 处理接收到的消息
*
* @param callbackId 回调ID
* @param messageBody 消息体JSON 字符串
*/
void onMessage(String callbackId, String messageBody);
}
}

View File

@ -0,0 +1,218 @@
package com.tuoheng.machine.mqtt.store;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 基于 Redis MQTT 回调存储实现
* 适用于多节点部署的生产环境
*
* 架构说明
* 1. 回调信息存储在 Redis Hash mqtt:callback:{callbackId} -> MqttCallbackInfo (JSON)
* 2. Topic 索引存储在 Redis Set mqtt:topic:{topic} -> Set<callbackId>
* 3. 使用 Redis Pub/Sub 在节点间传递 MQTT 消息mqtt:node:{nodeId} -> {callbackId, messageBody}
*
* 工作流程
* - 节点A 注册回调 -> 存储到 Redis
* - 节点B 收到 MQTT 消息 -> Redis 查询等待该 topic 的回调 -> 通过 Pub/Sub 发送到对应节点
* - 节点A 收到 Pub/Sub 消息 -> 执行本地的 Consumer 回调
*
* 使用方式
* 1. application.properties 中配置machine.state.store.type=redis
* 2. 配置 Redis 连接信息
* 3. 实现 Redis 相关的序列化和 Pub/Sub 逻辑
*
* 注意当前为空实现需要在生产环境部署时完善
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "machine.state.store.type", havingValue = "redis")
public class RedisMqttCallbackStore implements MqttCallbackStore {
// TODO: 注入 RedisTemplate StringRedisTemplate
// private final RedisTemplate<String, String> redisTemplate;
// private final StringRedisTemplate stringRedisTemplate;
// TODO: 注入 ObjectMapper 用于序列化
// private final ObjectMapper objectMapper;
// Redis key 前缀
private static final String CALLBACK_KEY_PREFIX = "mqtt:callback:";
private static final String TOPIC_INDEX_PREFIX = "mqtt:topic:";
private static final String NODE_CHANNEL_PREFIX = "mqtt:node:";
// TODO: 配置回调信息的过期时间可选
// private static final long EXPIRE_SECONDS = 3600; // 1小时
public RedisMqttCallbackStore() {
log.warn("使用 Redis MQTT 回调存储实现,但当前为空实现,请在生产环境部署前完善");
}
@Override
public void registerCallback(MqttCallbackInfo callbackInfo) {
// TODO: 实现注册回调到 Redis
// 1. MqttCallbackInfo 序列化为 JSON
// String json = objectMapper.writeValueAsString(callbackInfo);
//
// 2. 存储到 Redis Hash
// String callbackKey = CALLBACK_KEY_PREFIX + callbackInfo.getCallbackId();
// stringRedisTemplate.opsForValue().set(callbackKey, json, EXPIRE_SECONDS, TimeUnit.SECONDS);
//
// 3. 添加到 Topic 索引
// String topicKey = TOPIC_INDEX_PREFIX + callbackInfo.getTopic();
// stringRedisTemplate.opsForSet().add(topicKey, callbackInfo.getCallbackId());
// stringRedisTemplate.expire(topicKey, EXPIRE_SECONDS, TimeUnit.SECONDS);
//
// log.debug("注册MQTT回调到Redis: callbackId={}, topic={}",
// callbackInfo.getCallbackId(), callbackInfo.getTopic());
log.warn("Redis MQTT 回调存储未实现,跳过注册: callbackId={}", callbackInfo.getCallbackId());
}
@Override
public void unregisterCallback(String callbackId) {
// TODO: 实现从 Redis 删除回调
// 1. 获取回调信息
// MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
// if (callbackInfo == null) {
// return;
// }
//
// 2. Topic 索引中移除
// String topicKey = TOPIC_INDEX_PREFIX + callbackInfo.getTopic();
// stringRedisTemplate.opsForSet().remove(topicKey, callbackId);
//
// 3. 删除回调信息
// String callbackKey = CALLBACK_KEY_PREFIX + callbackId;
// stringRedisTemplate.delete(callbackKey);
//
// log.debug("从Redis中取消注册MQTT回调: callbackId={}, topic={}",
// callbackId, callbackInfo.getTopic());
log.warn("Redis MQTT 回调存储未实现,跳过取消注册: callbackId={}", callbackId);
}
@Override
public List<MqttCallbackInfo> getCallbacksByTopic(String topic) {
// TODO: 实现从 Redis 获取指定 topic 的所有回调
// 1. Topic 索引获取所有 callbackId
// String topicKey = TOPIC_INDEX_PREFIX + topic;
// Set<String> callbackIds = stringRedisTemplate.opsForSet().members(topicKey);
// if (callbackIds == null || callbackIds.isEmpty()) {
// return new ArrayList<>();
// }
//
// 2. 批量获取回调信息
// List<MqttCallbackInfo> callbacks = new ArrayList<>();
// for (String callbackId : callbackIds) {
// MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
// if (callbackInfo != null) {
// callbacks.add(callbackInfo);
// }
// }
// return callbacks;
log.warn("Redis MQTT 回调存储未实现,返回空列表: topic={}", topic);
return new ArrayList<>();
}
@Override
public MqttCallbackInfo getCallbackById(String callbackId) {
// TODO: 实现从 Redis 获取回调信息
// String callbackKey = CALLBACK_KEY_PREFIX + callbackId;
// String json = stringRedisTemplate.opsForValue().get(callbackKey);
// if (json == null) {
// return null;
// }
//
// try {
// return objectMapper.readValue(json, MqttCallbackInfo.class);
// } catch (JsonProcessingException e) {
// log.error("反序列化回调信息失败: callbackId={}", callbackId, e);
// return null;
// }
log.warn("Redis MQTT 回调存储未实现,返回 null: callbackId={}", callbackId);
return null;
}
@Override
public List<MqttCallbackInfo> getAllCallbacks() {
// TODO: 实现获取所有回调信息用于清理超时回调
// 1. 扫描所有 mqtt:callback:* key
// Set<String> keys = stringRedisTemplate.keys(CALLBACK_KEY_PREFIX + "*");
// if (keys == null || keys.isEmpty()) {
// return new ArrayList<>();
// }
//
// 2. 批量获取回调信息
// List<MqttCallbackInfo> callbacks = new ArrayList<>();
// for (String key : keys) {
// String callbackId = key.substring(CALLBACK_KEY_PREFIX.length());
// MqttCallbackInfo callbackInfo = getCallbackById(callbackId);
// if (callbackInfo != null) {
// callbacks.add(callbackInfo);
// }
// }
// return callbacks;
log.warn("Redis MQTT 回调存储未实现,返回空列表");
return new ArrayList<>();
}
@Override
public void publishMessageToNode(String nodeId, String callbackId, String messageBody) {
// TODO: 实现通过 Redis Pub/Sub 发布消息到指定节点
// 1. 构造消息体
// Map<String, String> message = new HashMap<>();
// message.put("callbackId", callbackId);
// message.put("messageBody", messageBody);
//
// 2. 序列化消息
// String json = objectMapper.writeValueAsString(message);
//
// 3. 发布到节点频道
// String channel = NODE_CHANNEL_PREFIX + nodeId;
// stringRedisTemplate.convertAndSend(channel, json);
//
// log.debug("发布消息到节点: nodeId={}, callbackId={}, channel={}",
// nodeId, callbackId, channel);
log.warn("Redis MQTT 回调存储未实现,跳过发布消息: nodeId={}, callbackId={}", nodeId, callbackId);
}
@Override
public void subscribeNodeMessages(String nodeId, NodeMessageListener messageListener) {
// TODO: 实现订阅当前节点的 Redis Pub/Sub 消息
// 1. 创建消息监听器
// MessageListener redisMessageListener = (message, pattern) -> {
// try {
// String json = new String(message.getBody());
// Map<String, String> data = objectMapper.readValue(json,
// new TypeReference<Map<String, String>>() {});
//
// String callbackId = data.get("callbackId");
// String messageBody = data.get("messageBody");
//
// messageListener.onMessage(callbackId, messageBody);
// } catch (Exception e) {
// log.error("处理Redis Pub/Sub消息失败", e);
// }
// };
//
// 2. 订阅节点频道
// String channel = NODE_CHANNEL_PREFIX + nodeId;
// redisTemplate.getConnectionFactory().getConnection()
// .subscribe(redisMessageListener, channel.getBytes());
//
// log.info("订阅节点消息: nodeId={}, channel={}", nodeId, channel);
log.warn("Redis MQTT 回调存储未实现,跳过订阅节点消息: nodeId={}", nodeId);
}
}