增加历史消息消费接口
This commit is contained in:
+598
@@ -0,0 +1,598 @@
|
||||
package com.tongran.rocketmq.consumer;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.tongran.common.core.enums.MsgEnum;
|
||||
import com.tongran.rocketmq.domain.DeviceMessage;
|
||||
import com.tongran.rocketmq.domain.InitialBandwidthTraffic;
|
||||
import com.tongran.rocketmq.enums.MessageCodeEnum;
|
||||
import com.tongran.rocketmq.handler.MessageHistoryDataHandler;
|
||||
import com.tongran.rocketmq.utils.JsonDataParser;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
|
||||
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
|
||||
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
|
||||
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
|
||||
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
|
||||
import org.apache.rocketmq.common.message.MessageExt;
|
||||
import org.apache.rocketmq.common.message.MessageQueue;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 时间范围重消费消费者(两阶段优化版)
|
||||
* 第一阶段:并行消费并缓存所有消息
|
||||
* 第二阶段:按设备顺序处理缓存的消息
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TimeRangeReConsumer {
|
||||
|
||||
|
||||
@Autowired(required = false)
|
||||
private MessageHistoryDataHandler messageHandler;
|
||||
|
||||
// 时间范围配置
|
||||
private static final String START_TIME = "2026-02-02 06:00:00";
|
||||
private static final String END_TIME = "2026-02-02 12:00:00";
|
||||
|
||||
// 统计信息
|
||||
private final AtomicLong receivedCount = new AtomicLong(0); // 已接收消息数
|
||||
private final AtomicLong skippedCount = new AtomicLong(0);
|
||||
private final AtomicLong processedCount = new AtomicLong(0); // 已处理消息数
|
||||
private final Map<Integer, Long> queueCurrentOffsets = new ConcurrentHashMap<>();
|
||||
private final Set<Integer> completedQueues = ConcurrentHashMap.newKeySet();
|
||||
|
||||
// 控制是否继续消费
|
||||
private AtomicBoolean shouldContinue = new AtomicBoolean(true);
|
||||
private AtomicBoolean isProcessingPhase = new AtomicBoolean(false); // 第二阶段处理标记
|
||||
|
||||
// 消息缓存:按clientId分组,按时间戳排序
|
||||
private final ConcurrentHashMap<String, PriorityBlockingQueue<MessageWrapper>> messageCache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 第二阶段顺序处理器线程池
|
||||
private final ExecutorService sequentialProcessor = Executors.newFixedThreadPool(
|
||||
Math.min(Runtime.getRuntime().availableProcessors() * 4, 32)
|
||||
);
|
||||
|
||||
// 第二阶段处理跟踪
|
||||
private final Map<String, CompletableFuture<Void>> processingFutures = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger activeSequentialTasks = new AtomicInteger(0);
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class MessageWrapper implements Comparable<MessageWrapper> {
|
||||
private String msgId;
|
||||
private DeviceMessage message;
|
||||
private MessageExt messageExt;
|
||||
private long timestamp; // 从data中解析的时间戳
|
||||
private long bornTimestamp; // 消息产生时间
|
||||
|
||||
@Override
|
||||
public int compareTo(MessageWrapper other) {
|
||||
return Long.compare(this.timestamp, other.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 添加一个标识字段
|
||||
private final AtomicBoolean isRunning = new AtomicBoolean(false);
|
||||
|
||||
// 暴露启动方法
|
||||
public String startReconsumeTask(String startTime, String endTime, String namesrvAddr) {
|
||||
if (isRunning.compareAndSet(false, true)) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
doReconsume(startTime, endTime, namesrvAddr);
|
||||
} catch (Exception e) {
|
||||
log.error("重消费失败", e);
|
||||
} finally {
|
||||
isRunning.set(false);
|
||||
}
|
||||
}).start();
|
||||
return "重消费任务已启动";
|
||||
}
|
||||
return "已有任务正在运行";
|
||||
}
|
||||
public void startTimeRangeConsumer(String startTime, String endTime, String namesrvAddr) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
doReconsume(startTime, endTime, namesrvAddr);
|
||||
} catch (Exception e) {
|
||||
log.error("重消费失败", e);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void doReconsume(String startTime, String endTime, String namesrvAddr) throws Exception {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
long startTimestamp = sdf.parse(startTime).getTime();
|
||||
long endTimestamp = sdf.parse(endTime).getTime();
|
||||
|
||||
String consumerGroup = "RECONSUME_GROUP_TEST" + Instant.now().getEpochSecond();
|
||||
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroup);
|
||||
consumer.setNamesrvAddr(namesrvAddr);
|
||||
|
||||
// ================== 第一阶段:开始消息缓存 ==================
|
||||
log.info("===== 第一阶段:开始消息缓存 =====");
|
||||
|
||||
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP);
|
||||
consumer.setConsumeTimestamp(sdf.format(new Date(startTimestamp)));
|
||||
|
||||
consumer.subscribe(MessageCodeEnum.TR_AGENT_UP.getCode(), "*");
|
||||
|
||||
consumer.registerMessageListener(new MessageListenerConcurrently() {
|
||||
@Override
|
||||
public ConsumeConcurrentlyStatus consumeMessage(
|
||||
List<MessageExt> list,
|
||||
ConsumeConcurrentlyContext context) {
|
||||
|
||||
if (!shouldContinue.get()) {
|
||||
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
for (MessageExt messageExt : list) {
|
||||
long bornTimestamp = messageExt.getBornTimestamp();
|
||||
int queueId = messageExt.getQueueId();
|
||||
|
||||
// 记录当前队列的offset
|
||||
queueCurrentOffsets.put(queueId, messageExt.getQueueOffset());
|
||||
|
||||
// 1. 时间太早,跳过
|
||||
if (bornTimestamp < startTimestamp) {
|
||||
skippedCount.incrementAndGet();
|
||||
if (skippedCount.get() % 1000 != 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 超过结束时间,标记队列完成
|
||||
if (bornTimestamp > endTimestamp) {
|
||||
log.info("队列 {} 已超过结束时间,标记为完成", queueId);
|
||||
completedQueues.add(queueId);
|
||||
|
||||
// 检查是否所有队列都完成
|
||||
if (completedQueues.size() >= 4) { // 假设有4个队列
|
||||
log.info("所有队列均已超过结束时间,第一阶段完成");
|
||||
shouldContinue.set(false);
|
||||
}
|
||||
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
|
||||
}
|
||||
|
||||
String body = new String(messageExt.getBody(), StandardCharsets.UTF_8);
|
||||
DeviceMessage message = JSON.parseObject(body, DeviceMessage.class);
|
||||
String dataType = message.getDataType();
|
||||
String clientId = message.getClientId();
|
||||
// 只处理"网络上报重试"类型的消息
|
||||
if(MsgEnum.网络上报重试.getValue().equals(dataType)){
|
||||
|
||||
receivedCount.incrementAndGet();
|
||||
|
||||
// 只缓存消息,不处理
|
||||
cacheMessageOnly(message, messageExt, bornTimestamp, sdf);
|
||||
|
||||
// 定期打印进度
|
||||
if (receivedCount.get() % 100 == 0) {
|
||||
printPhase1Progress();
|
||||
}
|
||||
}
|
||||
}
|
||||
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
|
||||
} catch (Exception e) {
|
||||
log.error("重消费消息异常", e);
|
||||
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 启动消费者
|
||||
consumer.start();
|
||||
log.info("时间范围重消费者启动成功,消费者组: {}", consumerGroup);
|
||||
|
||||
// 预热:查找大致的起始位置
|
||||
findApproximateStartOffset(consumer, startTimestamp);
|
||||
|
||||
// 监控第一阶段完成
|
||||
waitForPhase1Completion(consumer);
|
||||
|
||||
// 第一阶段完成,关闭消费者
|
||||
consumer.shutdown();
|
||||
log.info("===== 第一阶段完成,共缓存{}条消息,涉及{}个设备 =====",
|
||||
receivedCount.get(), messageCache.size());
|
||||
|
||||
// ================== 第二阶段:开始顺序处理 ==================
|
||||
log.info("===== 第二阶段:开始顺序处理 =====");
|
||||
startPhase2Processing();
|
||||
|
||||
// 等待第二阶段完成
|
||||
waitForPhase2Completion();
|
||||
|
||||
log.info("✓ 时间范围重消费完成,总计接收消息: {}, 处理消息: {}",
|
||||
receivedCount.get(), processedCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一阶段:只缓存消息,不处理
|
||||
*/
|
||||
private void cacheMessageOnly(DeviceMessage message, MessageExt messageExt,
|
||||
long bornTimestamp, SimpleDateFormat sdf) {
|
||||
try {
|
||||
// 解析data中的时间戳
|
||||
long timestamp = extractTimestampFromData(message.getData());
|
||||
String clientId = message.getClientId();
|
||||
|
||||
// 创建消息包装
|
||||
MessageWrapper wrapper = new MessageWrapper(
|
||||
messageExt.getMsgId(),
|
||||
message,
|
||||
messageExt,
|
||||
timestamp,
|
||||
bornTimestamp
|
||||
);
|
||||
|
||||
// 获取或创建该客户端的优先级队列
|
||||
PriorityBlockingQueue<MessageWrapper> queue = messageCache.computeIfAbsent(
|
||||
clientId,
|
||||
id -> new PriorityBlockingQueue<>()
|
||||
);
|
||||
|
||||
// 添加到队列(会自动按时间戳排序)
|
||||
queue.offer(wrapper);
|
||||
|
||||
if (log.isDebugEnabled() && receivedCount.get() % 20 == 0) {
|
||||
log.debug("消息已缓存: clientId={}, timestamp={}, bornTime={}, 队列大小={}",
|
||||
clientId,
|
||||
timestamp,
|
||||
sdf.format(new Date(bornTimestamp)),
|
||||
queue.size());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("缓存消息失败: msgId={}", messageExt.getMsgId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从data字段解析时间戳
|
||||
*/
|
||||
private long extractTimestampFromData(String data) {
|
||||
try {
|
||||
List<InitialBandwidthTraffic> interfaces = JsonDataParser.parseJsonData(data, InitialBandwidthTraffic.class);
|
||||
if(!interfaces.isEmpty()) {
|
||||
// 时间戳转换
|
||||
long timestamp = interfaces.get(0).getTimestamp();
|
||||
return timestamp;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析data时间戳失败,使用默认值", e);
|
||||
}
|
||||
return System.currentTimeMillis(); // 解析失败使用当前时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待第一阶段(消息缓存)完成
|
||||
*/
|
||||
private void waitForPhase1Completion(DefaultMQPushConsumer consumer) {
|
||||
log.info("等待第一阶段完成(消息缓存完成)...");
|
||||
|
||||
int checkCount = 0;
|
||||
long lastMessageCount = 0;
|
||||
int noProgressCount = 0;
|
||||
|
||||
while (shouldContinue.get()) {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
checkCount++;
|
||||
|
||||
long currentMessageCount = receivedCount.get();
|
||||
int totalCached = 0;
|
||||
for (PriorityBlockingQueue<MessageWrapper> queue : messageCache.values()) {
|
||||
totalCached += queue.size();
|
||||
}
|
||||
|
||||
// 每30秒打印一次进度
|
||||
if (checkCount % 6 == 0) {
|
||||
log.info("第一阶段进度: 已接收={}, 已缓存={}, 设备数={}, 完成队列={}/4",
|
||||
currentMessageCount, totalCached, messageCache.size(), completedQueues.size());
|
||||
}
|
||||
|
||||
// 检查是否有进度
|
||||
if (currentMessageCount == lastMessageCount) {
|
||||
noProgressCount++;
|
||||
} else {
|
||||
noProgressCount = 0;
|
||||
lastMessageCount = currentMessageCount;
|
||||
}
|
||||
|
||||
// 如果30秒内没有新消息,认为第一阶段完成
|
||||
if (noProgressCount >= 6 && completedQueues.size() >= 4) {
|
||||
log.info("30秒内无新消息且所有队列完成,第一阶段完成");
|
||||
shouldContinue.set(false);
|
||||
break;
|
||||
}
|
||||
|
||||
// 超时保护:最多等待1小时
|
||||
if (checkCount >= 720) {
|
||||
log.warn("第一阶段超时(1小时),强制结束");
|
||||
shouldContinue.set(false);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 第二阶段:开始顺序处理所有缓存的消息
|
||||
*/
|
||||
private void startPhase2Processing() {
|
||||
isProcessingPhase.set(true);
|
||||
|
||||
int totalMessages = messageCache.values().stream()
|
||||
.mapToInt(Queue::size)
|
||||
.sum();
|
||||
|
||||
log.info("开始顺序处理{}个设备的{}条消息...",
|
||||
messageCache.size(), totalMessages);
|
||||
|
||||
// 为每个设备启动顺序处理任务
|
||||
for (Map.Entry<String, PriorityBlockingQueue<MessageWrapper>> entry : messageCache.entrySet()) {
|
||||
String clientId = entry.getKey();
|
||||
PriorityBlockingQueue<MessageWrapper> queue = entry.getValue();
|
||||
|
||||
if (!queue.isEmpty()) {
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
activeSequentialTasks.incrementAndGet();
|
||||
try {
|
||||
processDeviceMessagesSequentially(clientId, queue);
|
||||
} finally {
|
||||
activeSequentialTasks.decrementAndGet();
|
||||
}
|
||||
}, sequentialProcessor);
|
||||
|
||||
processingFutures.put(clientId, future);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("已启动{}个设备的顺序处理任务", processingFutures.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按顺序处理单个设备的所有消息
|
||||
*/
|
||||
private void processDeviceMessagesSequentially(String clientId,
|
||||
PriorityBlockingQueue<MessageWrapper> queue) {
|
||||
String threadName = Thread.currentThread().getName();
|
||||
Thread.currentThread().setName("SeqProcessor-" + clientId.substring(0, Math.min(8, clientId.length())));
|
||||
|
||||
try {
|
||||
int queueSize = queue.size();
|
||||
log.info("开始顺序处理设备{}的{}条消息", clientId, queueSize);
|
||||
|
||||
int deviceProcessed = 0;
|
||||
MessageWrapper lastMessage = null;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
MessageWrapper wrapper = queue.poll();
|
||||
if (wrapper != null) {
|
||||
try {
|
||||
// 验证时间顺序(可选)
|
||||
if (lastMessage != null && wrapper.getTimestamp() < lastMessage.getTimestamp()) {
|
||||
log.warn("设备{}消息时间顺序异常: 前一条timestamp={}, 当前timestamp={}",
|
||||
clientId, lastMessage.getTimestamp(), wrapper.getTimestamp());
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
if (messageHandler != null) {
|
||||
messageHandler.handleMessage(wrapper.getMessage());
|
||||
}
|
||||
|
||||
deviceProcessed++;
|
||||
processedCount.incrementAndGet();
|
||||
lastMessage = wrapper;
|
||||
|
||||
// 每处理20条日志一次
|
||||
if (deviceProcessed % 20 == 0) {
|
||||
log.debug("设备{}已处理{}条消息,剩余{}条",
|
||||
clientId, deviceProcessed, queue.size());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理设备{}消息失败: msgId={}", clientId, wrapper.getMsgId(), e);
|
||||
// 根据业务决定是否重试或跳过
|
||||
// 这里选择跳过,记录错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long timeCost = System.currentTimeMillis() - startTime;
|
||||
log.info("设备{}处理完成: 共处理{}条消息,耗时{}ms",
|
||||
clientId, deviceProcessed, timeCost);
|
||||
|
||||
} finally {
|
||||
Thread.currentThread().setName(threadName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待第二阶段处理完成
|
||||
*/
|
||||
private void waitForPhase2Completion() {
|
||||
log.info("等待所有设备顺序处理完成...");
|
||||
|
||||
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
|
||||
processingFutures.values().toArray(new CompletableFuture[0])
|
||||
);
|
||||
|
||||
try {
|
||||
// 等待所有任务完成
|
||||
allFutures.get(2, TimeUnit.HOURS); // 最多等待2小时
|
||||
|
||||
log.info("✓ 所有设备顺序处理完成");
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
log.warn("第二阶段处理超时(2小时),强制结束");
|
||||
// 可以记录未完成的任务
|
||||
List<String> unfinishedDevices = new ArrayList<>();
|
||||
for (Map.Entry<String, CompletableFuture<Void>> entry : processingFutures.entrySet()) {
|
||||
if (!entry.getValue().isDone()) {
|
||||
unfinishedDevices.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
log.warn("以下设备未完成处理: {}", unfinishedDevices);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("等待第二阶段处理异常", e);
|
||||
} finally {
|
||||
shutdownResources();
|
||||
}
|
||||
|
||||
// 最后统计
|
||||
int remainingMessages = messageCache.values().stream()
|
||||
.mapToInt(Queue::size)
|
||||
.sum();
|
||||
|
||||
if (remainingMessages > 0) {
|
||||
log.warn("仍有{}条消息未处理", remainingMessages);
|
||||
} else {
|
||||
log.info("所有消息处理完成,清理缓存");
|
||||
messageCache.clear();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 处理完成后关闭资源 - 添加状态重置
|
||||
*/
|
||||
private void shutdownResources() {
|
||||
log.info("重消费完成,正在关闭资源...");
|
||||
|
||||
shouldContinue.set(false);
|
||||
isProcessingPhase.set(false);
|
||||
isRunning.set(false);
|
||||
|
||||
// 清理缓存
|
||||
messageCache.clear();
|
||||
processingFutures.clear();
|
||||
|
||||
// 关闭线程池
|
||||
sequentialProcessor.shutdown();
|
||||
try {
|
||||
if (!sequentialProcessor.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
sequentialProcessor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
sequentialProcessor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
// 必须重置计数器和集合
|
||||
receivedCount.set(0);
|
||||
processedCount.set(0);
|
||||
skippedCount.set(0);
|
||||
queueCurrentOffsets.clear();
|
||||
completedQueues.clear();
|
||||
|
||||
log.info("✓ 重消费者资源已完全释放");
|
||||
}
|
||||
/**
|
||||
* 查找大致的起始offset
|
||||
*/
|
||||
private void findApproximateStartOffset(DefaultMQPushConsumer consumer, long startTimestamp) throws Exception {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
log.info("查找起始offset,目标时间: {}", sdf.format(new Date(startTimestamp)));
|
||||
|
||||
try {
|
||||
// 获取主题队列
|
||||
Set<MessageQueue> queues = consumer.fetchSubscribeMessageQueues(MessageCodeEnum.TR_AGENT_UP.getCode());
|
||||
|
||||
if (queues == null || queues.isEmpty()) {
|
||||
log.info("无队列信息,跳过offset查找");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("找到{}个队列,开始设置起始offset...", queues.size());
|
||||
|
||||
for (MessageQueue queue : queues) {
|
||||
try {
|
||||
// 搜索指定时间戳的offset
|
||||
long offset = consumer.searchOffset(queue, startTimestamp);
|
||||
|
||||
if (offset >= 0) {
|
||||
// 获取队列的offset范围
|
||||
long minOffset = consumer.minOffset(queue);
|
||||
long maxOffset = consumer.maxOffset(queue);
|
||||
|
||||
// 确保offset在有效范围内
|
||||
if (offset < minOffset) offset = minOffset;
|
||||
if (offset > maxOffset) offset = maxOffset;
|
||||
|
||||
queueCurrentOffsets.put(queue.getQueueId(), offset);
|
||||
log.info("队列{}: offset={} (范围: {}-{})",
|
||||
queue.getQueueId(), offset, minOffset, maxOffset);
|
||||
} else {
|
||||
// 使用最小offset
|
||||
long minOffset = consumer.minOffset(queue);
|
||||
queueCurrentOffsets.put(queue.getQueueId(), minOffset);
|
||||
log.info("队列{}: 使用最小offset={}", queue.getQueueId(), minOffset);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("队列{}设置offset失败: {}", queue.getQueueId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("起始offset设置完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("查找起始offset失败,将使用默认时间戳方式: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印第一阶段进度
|
||||
*/
|
||||
private void printPhase1Progress() {
|
||||
int totalCached = 0;
|
||||
for (PriorityBlockingQueue<MessageWrapper> queue : messageCache.values()) {
|
||||
totalCached += queue.size();
|
||||
}
|
||||
|
||||
log.info("第一阶段进度: 已接收={}, 已缓存={}, 设备数={}, 完成队列={}/4",
|
||||
receivedCount.get(), totalCached, messageCache.size(), completedQueues.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印第二阶段进度
|
||||
*/
|
||||
private void printPhase2Progress() {
|
||||
int totalRemaining = 0;
|
||||
int activeTasks = 0;
|
||||
|
||||
for (Map.Entry<String, PriorityBlockingQueue<MessageWrapper>> entry : messageCache.entrySet()) {
|
||||
totalRemaining += entry.getValue().size();
|
||||
CompletableFuture<Void> future = processingFutures.get(entry.getKey());
|
||||
if (future != null && !future.isDone()) {
|
||||
activeTasks++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("第二阶段进度: 已处理={}, 剩余={}, 活跃任务={}",
|
||||
processedCount.get(), totalRemaining, activeTasks);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.tongran.rocketmq.controller;
|
||||
|
||||
import com.tongran.common.security.annotation.RequiresPermissions;
|
||||
import com.tongran.rocketmq.consumer.TimeRangeReConsumer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/historyTraffic")
|
||||
@RequiresPermissions("rocketmq:history")
|
||||
public class HistoryTrafficController {
|
||||
|
||||
@Autowired
|
||||
private TimeRangeReConsumer timeRangeReConsumer;
|
||||
/**
|
||||
* 处理历史流量数据
|
||||
* @param timeRange 时间范围,分割 2026-02-03 00:00:00,2026-02-03 12:00:00,172.16.15.52:9876
|
||||
*/
|
||||
@GetMapping("/processHistoryData")
|
||||
public void processHistoryData(String timeRange){
|
||||
String[] timeRangeArr = timeRange.split(",");
|
||||
String startTime = timeRangeArr[0];
|
||||
String endTime = timeRangeArr[1];
|
||||
String namesrvAddr = timeRangeArr[2];
|
||||
timeRangeReConsumer.startTimeRangeConsumer(startTime, endTime, namesrvAddr);
|
||||
}
|
||||
}
|
||||
@@ -115,8 +115,6 @@ public class MessageHandler {
|
||||
@Autowired
|
||||
private IRmAlarmLogService rmAlarmLogService;
|
||||
@Autowired
|
||||
private IRmAlarmPushConfigService rmAlarmPushConfigService;
|
||||
@Autowired
|
||||
private IRmFrpcConfigManageService rmFrpcConfigManageService;
|
||||
@Autowired
|
||||
private IRmPppoeConfigSubService rmPppoeConfigSubService;
|
||||
@@ -740,9 +738,9 @@ public class MessageHandler {
|
||||
// 尝试获取锁
|
||||
locked = lock.tryLock(0, 20, TimeUnit.SECONDS);
|
||||
if (locked) {
|
||||
log.info("设备{}获取锁成功,开始处理消息", clientId);
|
||||
// log.info("设备{}获取锁成功,开始处理消息", clientId);
|
||||
processNetRecoverMessageInternal(message);
|
||||
log.info("设备{}消息处理完成", clientId);
|
||||
// log.info("设备{}消息处理完成", clientId);
|
||||
} else {
|
||||
log.warn("设备{}获取锁失败,消息处理繁忙", clientId);
|
||||
throw new RuntimeException("设备处理繁忙,请重试");
|
||||
@@ -755,7 +753,7 @@ public class MessageHandler {
|
||||
if (locked && lock.isHeldByCurrentThread()) {
|
||||
try {
|
||||
lock.unlock();
|
||||
log.debug("设备{}锁已释放", clientId);
|
||||
// log.debug("设备{}锁已释放", clientId);
|
||||
} catch (IllegalMonitorStateException e) {
|
||||
log.warn("设备{}锁释放异常,可能已自动超时", clientId);
|
||||
}
|
||||
@@ -775,10 +773,6 @@ public class MessageHandler {
|
||||
boolean lasttrafficFlag = interfaces.get(0).isLastTrafficFlag();
|
||||
// 时间戳存储到redis
|
||||
storeTimestampToRedis(clientId, timestamp);
|
||||
if(lasttrafficFlag){
|
||||
// 把redis中存储的时间戳提取出来,删除redis中的时间戳
|
||||
processExitsTraffic(clientId);
|
||||
}
|
||||
long millis = timestamp * 1000;
|
||||
Date createTime = new Date(millis / 1000 * 1000); // 去除毫秒
|
||||
String timeStr = DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss",createTime);
|
||||
@@ -790,13 +784,6 @@ public class MessageHandler {
|
||||
countQuery.setStartTime(timeStr);
|
||||
int exitsCount = initialBandwidthTrafficService.countByClientIdAndTime(countQuery);
|
||||
|
||||
// 即使数据已存在,也需要处理临时表逻辑,确保后续计算正确
|
||||
if(exitsCount > 0){
|
||||
// 不直接返回,继续执行临时表处理逻辑
|
||||
// 但跳过最终的数据入库操作
|
||||
System.out.println("数据已存在,跳过入库操作,但继续处理临时表逻辑");
|
||||
}
|
||||
|
||||
// 创建比timestamp少5分钟的时间
|
||||
long fiveMinutesEarlier = millis - (5 * 60 * 1000); // 减去5分钟的毫秒数
|
||||
Date fiveMinutesEarlierDate = new Date(fiveMinutesEarlier / 1000 * 1000); // 同样去除毫秒
|
||||
@@ -943,6 +930,10 @@ public class MessageHandler {
|
||||
// 数据已存在时,只更新临时表,确保后续计算正确
|
||||
initialBandwidthTrafficTempService.batchInsertServerRecoverTemp(interfaces);
|
||||
}
|
||||
if(lasttrafficFlag){
|
||||
// 把redis中存储的时间戳提取出来,删除redis中的时间戳
|
||||
processExitsTraffic(clientId);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("NET流量data数据为空");
|
||||
}
|
||||
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
package com.tongran.rocketmq.handler;
|
||||
|
||||
import com.tongran.common.core.constant.SecurityConstants;
|
||||
import com.tongran.common.core.enums.MsgEnum;
|
||||
import com.tongran.common.core.utils.DateUtils;
|
||||
import com.tongran.rocketmq.domain.DeviceMessage;
|
||||
import com.tongran.rocketmq.domain.InitialBandwidthTraffic;
|
||||
import com.tongran.rocketmq.domain.InitialBandwidthTrafficTemp;
|
||||
import com.tongran.rocketmq.service.IInitialBandwidthTrafficService;
|
||||
import com.tongran.rocketmq.service.IInitialBandwidthTrafficTempService;
|
||||
import com.tongran.rocketmq.utils.DataProcessUtil;
|
||||
import com.tongran.rocketmq.utils.JsonDataParser;
|
||||
import com.tongran.system.api.RemoteRevenueConfigService;
|
||||
import com.tongran.system.api.domain.EpsInitialTrafficDataRemote;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 设备消息处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@EnableScheduling
|
||||
public class MessageHistoryDataHandler {
|
||||
|
||||
private final Map<String, Consumer<DeviceMessage>> messageHandlers = new HashMap<>();
|
||||
@Autowired
|
||||
private IInitialBandwidthTrafficService initialBandwidthTrafficService;
|
||||
@Autowired
|
||||
private RemoteRevenueConfigService remoteRevenueConfigService;
|
||||
@Autowired
|
||||
private DataProcessUtil dataProcessUtil;
|
||||
@Autowired
|
||||
private RedissonClient redissonClient;
|
||||
@Autowired
|
||||
private IInitialBandwidthTrafficTempService initialBandwidthTrafficTempService;
|
||||
/**
|
||||
* 注册消息处理器
|
||||
*/
|
||||
private void registerHandler(String dataType, Consumer<DeviceMessage> handler) {
|
||||
messageHandlers.put(dataType, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备消息(对外暴露的主方法)
|
||||
*/
|
||||
public void handleMessage(DeviceMessage message) {
|
||||
String dataType = message.getDataType();
|
||||
Consumer<DeviceMessage> handler = messageHandlers.get(dataType);
|
||||
|
||||
if (handler != null) {
|
||||
handler.accept(message);
|
||||
} else {
|
||||
log.warn("未知数据类型:{}", dataType);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化处理器映射
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
registerHandler(MsgEnum.网络上报重试.getValue(), this::handleNetRecoverMessage);
|
||||
}
|
||||
private void handleNetRecoverMessage(DeviceMessage message) {
|
||||
String clientId = message.getClientId();
|
||||
String lockKey = "traffic:recover:" + clientId;
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
boolean locked = false;
|
||||
|
||||
try {
|
||||
// 尝试获取锁
|
||||
locked = lock.tryLock(0, 20, TimeUnit.SECONDS);
|
||||
if (locked) {
|
||||
processNetRecoverMessageInternal(message);
|
||||
} else {
|
||||
log.warn("设备{}获取锁失败,消息处理繁忙", clientId);
|
||||
throw new RuntimeException("设备处理繁忙,请重试");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("处理消息时线程被中断", e);
|
||||
} finally {
|
||||
// 只有成功获取锁的线程才需要释放
|
||||
if (locked && lock.isHeldByCurrentThread()) {
|
||||
try {
|
||||
lock.unlock();
|
||||
} catch (IllegalMonitorStateException e) {
|
||||
log.warn("设备{}锁释放异常,可能已自动超时", clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 网络重试流量数据入库
|
||||
* @param message
|
||||
*/
|
||||
private void processNetRecoverMessageInternal(DeviceMessage message) {
|
||||
List<InitialBandwidthTraffic> interfaces = JsonDataParser.parseJsonData(message.getData(), InitialBandwidthTraffic.class);
|
||||
if(!interfaces.isEmpty()){
|
||||
String clientId = message.getClientId();
|
||||
// 时间戳转换
|
||||
long timestamp = interfaces.get(0).getTimestamp();
|
||||
long millis = timestamp * 1000;
|
||||
Date createTime = new Date(millis / 1000 * 1000); // 去除毫秒
|
||||
String timeStr = DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss",createTime);
|
||||
|
||||
// 判断数据库中是否已存在数据
|
||||
InitialBandwidthTraffic countQuery = new InitialBandwidthTraffic();
|
||||
countQuery.setClientId(clientId);
|
||||
countQuery.setCreateTime(createTime);
|
||||
countQuery.setStartTime(timeStr);
|
||||
int exitsCount = initialBandwidthTrafficService.countByClientIdAndTime(countQuery);
|
||||
|
||||
// 创建比timestamp少5分钟的时间
|
||||
long fiveMinutesEarlier = millis - (5 * 60 * 1000); // 减去5分钟的毫秒数
|
||||
Date fiveMinutesEarlierDate = new Date(fiveMinutesEarlier / 1000 * 1000); // 同样去除毫秒
|
||||
|
||||
// 查询临时表信息,计算实际流量值
|
||||
InitialBandwidthTrafficTemp temp = new InitialBandwidthTrafficTemp();
|
||||
temp.setCreateTime(fiveMinutesEarlierDate);
|
||||
temp.setClientId(clientId);
|
||||
List<InitialBandwidthTrafficTemp> tempList = initialBandwidthTrafficTempService.selectInitialBandwidthTrafficRecoverTempList(temp);
|
||||
|
||||
if(!tempList.isEmpty()){
|
||||
// 1. 构建快速查找的Map,使用MAC地址+网卡名称作为唯一键
|
||||
Map<String, InitialBandwidthTrafficTemp> tempMap = tempList.stream()
|
||||
.collect(Collectors.toMap(
|
||||
tempItem -> generateKey(tempItem.getMac(), tempItem.getName()),
|
||||
Function.identity(),
|
||||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
// 2. 预计算除数(避免重复创建对象)
|
||||
BigDecimal divisor = new BigDecimal(300);
|
||||
|
||||
interfaces.forEach(iface -> {
|
||||
iface.setClientId(clientId);
|
||||
iface.setCreateTime(createTime);
|
||||
|
||||
// 设置总流量(转换为比特)
|
||||
iface.setTotalOutSpeed(dataProcessUtil.bytesToBits(iface.getOutSpeed()));
|
||||
iface.setTotalInSpeed(dataProcessUtil.bytesToBits(iface.getInSpeed()));
|
||||
iface.setTotalIpv4OutSpeed(dataProcessUtil.bytesToBits(iface.getIpv4OutSpeed()));
|
||||
iface.setTotalIpv4InSpeed(dataProcessUtil.bytesToBits(iface.getIpv4InSpeed()));
|
||||
iface.setTotalIpv6OutSpeed(dataProcessUtil.bytesToBits(iface.getIpv6OutSpeed()));
|
||||
iface.setTotalIpv6InSpeed(dataProcessUtil.bytesToBits(iface.getIpv6InSpeed()));
|
||||
// 首次采集,速率设为null
|
||||
iface.setInSpeed(null);
|
||||
iface.setOutSpeed(null);
|
||||
iface.setIpv4InSpeed(null);
|
||||
iface.setIpv4OutSpeed(null);
|
||||
iface.setIpv6InSpeed(null);
|
||||
iface.setIpv6OutSpeed(null);
|
||||
|
||||
// 使用MAC地址+网卡名称作为查找键
|
||||
String key = generateKey(iface.getMac(), iface.getName());
|
||||
InitialBandwidthTrafficTemp tempInfo = tempMap.get(key);
|
||||
if (tempInfo != null) {
|
||||
// 计算总流入速率
|
||||
if (iface.getTotalInSpeed() != null && tempInfo.getTotalInSpeed() != null) {
|
||||
BigDecimal nowInSpeed = new BigDecimal(iface.getTotalInSpeed());
|
||||
BigDecimal tempInSpeed = new BigDecimal(tempInfo.getTotalInSpeed());
|
||||
BigDecimal inDiff = nowInSpeed.subtract(tempInSpeed);
|
||||
if (inDiff.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
iface.setInSpeed(inDiff.divide(divisor, 0, RoundingMode.HALF_UP).toString());
|
||||
}
|
||||
}
|
||||
// 计算总流出速率
|
||||
if (iface.getTotalOutSpeed() != null && tempInfo.getTotalOutSpeed() != null) {
|
||||
BigDecimal nowOutSpeed = new BigDecimal(iface.getTotalOutSpeed());
|
||||
BigDecimal tempOutSpeed = new BigDecimal(tempInfo.getTotalOutSpeed());
|
||||
BigDecimal outDiff = nowOutSpeed.subtract(tempOutSpeed);
|
||||
if (outDiff.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
iface.setOutSpeed(outDiff.divide(divisor, 0, RoundingMode.HALF_UP).toString());
|
||||
}
|
||||
}
|
||||
// 计算IPv4流入速率
|
||||
if (iface.getTotalIpv4InSpeed() != null && tempInfo.getTotalIpv4InSpeed() != null) {
|
||||
BigDecimal nowIpv4In = new BigDecimal(iface.getTotalIpv4InSpeed());
|
||||
BigDecimal tempIpv4In = new BigDecimal(tempInfo.getTotalIpv4InSpeed());
|
||||
BigDecimal ipv4InDiff = nowIpv4In.subtract(tempIpv4In);
|
||||
if (ipv4InDiff.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
iface.setIpv4InSpeed(ipv4InDiff.divide(divisor, 0, RoundingMode.HALF_UP).toString());
|
||||
}
|
||||
}
|
||||
// 计算IPv4流出速率
|
||||
if (iface.getTotalIpv4OutSpeed() != null && tempInfo.getTotalIpv4OutSpeed() != null) {
|
||||
BigDecimal nowIpv4Out = new BigDecimal(iface.getTotalIpv4OutSpeed());
|
||||
BigDecimal tempIpv4Out = new BigDecimal(tempInfo.getTotalIpv4OutSpeed());
|
||||
BigDecimal ipv4OutDiff = nowIpv4Out.subtract(tempIpv4Out);
|
||||
if (ipv4OutDiff.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
iface.setIpv4OutSpeed(ipv4OutDiff.divide(divisor, 0, RoundingMode.HALF_UP).toString());
|
||||
}
|
||||
}
|
||||
// 计算IPv6流入速率
|
||||
if (iface.getTotalIpv6InSpeed() != null && tempInfo.getTotalIpv6InSpeed() != null) {
|
||||
BigDecimal nowIpv6In = new BigDecimal(iface.getTotalIpv6InSpeed());
|
||||
BigDecimal tempIpv6In = new BigDecimal(tempInfo.getTotalIpv6InSpeed());
|
||||
BigDecimal ipv6InDiff = nowIpv6In.subtract(tempIpv6In);
|
||||
if (ipv6InDiff.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
iface.setIpv6InSpeed(ipv6InDiff.divide(divisor, 0, RoundingMode.HALF_UP).toString());
|
||||
}
|
||||
}
|
||||
// 计算IPv6流出速率
|
||||
if (iface.getTotalIpv6OutSpeed() != null && tempInfo.getTotalIpv6OutSpeed() != null) {
|
||||
BigDecimal nowIpv6Out = new BigDecimal(iface.getTotalIpv6OutSpeed());
|
||||
BigDecimal tempIpv6Out = new BigDecimal(tempInfo.getTotalIpv6OutSpeed());
|
||||
BigDecimal ipv6OutDiff = nowIpv6Out.subtract(tempIpv6Out);
|
||||
if (ipv6OutDiff.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
iface.setIpv6OutSpeed(ipv6OutDiff.divide(divisor, 0, RoundingMode.HALF_UP).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// 清空临时表对应server信息
|
||||
InitialBandwidthTrafficTemp delQuery = new InitialBandwidthTrafficTemp();
|
||||
delQuery.setClientId(clientId);
|
||||
delQuery.setCreateTime(fiveMinutesEarlierDate);
|
||||
initialBandwidthTrafficTempService.deleteTempMsgByClientIdAndTime(delQuery);
|
||||
} else {
|
||||
interfaces.forEach(iface -> {
|
||||
iface.setClientId(clientId);
|
||||
iface.setCreateTime(createTime);
|
||||
// 设置总流量(转换为比特)
|
||||
iface.setTotalOutSpeed(dataProcessUtil.bytesToBits(iface.getOutSpeed()));
|
||||
iface.setTotalInSpeed(dataProcessUtil.bytesToBits(iface.getInSpeed()));
|
||||
iface.setTotalIpv4OutSpeed(dataProcessUtil.bytesToBits(iface.getIpv4OutSpeed()));
|
||||
iface.setTotalIpv4InSpeed(dataProcessUtil.bytesToBits(iface.getIpv4InSpeed()));
|
||||
iface.setTotalIpv6OutSpeed(dataProcessUtil.bytesToBits(iface.getIpv6OutSpeed()));
|
||||
iface.setTotalIpv6InSpeed(dataProcessUtil.bytesToBits(iface.getIpv6InSpeed()));
|
||||
|
||||
// 首次采集,速率设为null
|
||||
iface.setInSpeed(null);
|
||||
iface.setOutSpeed(null);
|
||||
iface.setIpv4InSpeed(null);
|
||||
iface.setIpv4OutSpeed(null);
|
||||
iface.setIpv6InSpeed(null);
|
||||
iface.setIpv6OutSpeed(null);
|
||||
});
|
||||
}
|
||||
|
||||
// 只有在数据不存在时才执行入库操作
|
||||
if (exitsCount == 0) {
|
||||
InitialBandwidthTraffic data = new InitialBandwidthTraffic();
|
||||
// 批量入库集合
|
||||
data.setList(interfaces);
|
||||
// 临时表 用来计算流量速率
|
||||
initialBandwidthTrafficTempService.batchInsertServerRecoverTemp(interfaces);
|
||||
// 初始流量数据入库
|
||||
initialBandwidthTrafficService.batchInsertRecoverTraffic(data);
|
||||
EpsInitialTrafficDataRemote epsInitialTrafficDataRemote = new EpsInitialTrafficDataRemote();
|
||||
epsInitialTrafficDataRemote.setStartTime(timeStr);
|
||||
epsInitialTrafficDataRemote.setEndTime(timeStr);
|
||||
// 复制到业务初始库
|
||||
remoteRevenueConfigService.autoSaveServiceRecoverTrafficData(epsInitialTrafficDataRemote, SecurityConstants.INNER);
|
||||
} else {
|
||||
// 数据已存在时,只更新临时表,确保后续计算正确
|
||||
initialBandwidthTrafficTempService.batchInsertServerRecoverTemp(interfaces);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("NET流量data数据为空");
|
||||
}
|
||||
}
|
||||
|
||||
private String generateKey(String mac, String name) {
|
||||
if (mac == null) {
|
||||
mac = "";
|
||||
}
|
||||
if (name == null) {
|
||||
name = "";
|
||||
}
|
||||
return mac + "|" + name;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user