Browse Source

优化圣地蓝数据转发功能

master
bgy 1 month ago
parent
commit
1e7db35ba9
7 changed files with 1424 additions and 262 deletions
  1. +9
    -1
      pom.xml
  2. +409
    -0
      src/main/java/com/topsail/influxdb/config/InfluxDBConfig.java
  3. +227
    -14
      src/main/java/com/topsail/influxdb/rabbitmq/AmqpListener.java
  4. +221
    -12
      src/main/java/com/topsail/influxdb/rabbitmq/service/AmqpService.java
  5. +378
    -144
      src/main/java/com/topsail/influxdb/service/DeviceDataService.java
  6. +135
    -88
      src/main/java/com/topsail/influxdb/service/DeviceLogService.java
  7. +45
    -3
      src/main/resources/application.properties

+ 9
- 1
pom.xml View File

@ -77,7 +77,7 @@
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <artifactId>fastjson</artifactId>
<version>1.2.15</version>
<version>1.2.83</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.code.gson</groupId> <groupId>com.google.code.gson</groupId>
@ -134,6 +134,14 @@
</dependency> </dependency>
</dependencies> </dependencies>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins> </plugins>
<resources> <resources>
<resource> <resource>


+ 409
- 0
src/main/java/com/topsail/influxdb/config/InfluxDBConfig.java View File

@ -0,0 +1,409 @@
package com.topsail.influxdb.config;
import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.InfluxDBClientOptions;
import com.influxdb.client.WriteApi;
import com.influxdb.client.WriteOptions;
import com.influxdb.client.write.events.WriteErrorEvent;
import com.influxdb.client.write.events.WriteSuccessEvent;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* InfluxDB配置类
* 负责管理InfluxDB客户端和WriteApi的生命周期
*/
@Configuration
public class InfluxDBConfig {
private static final Logger LOG = LoggerFactory.getLogger(InfluxDBConfig.class);
@Value("${shengdilan.influxdb.url}")
private String url;
@Value("${shengdilan.influxdb.token}")
private String token;
@Value("${shengdilan.influxdb.org}")
private String org;
private InfluxDBClient influxDBClient;
/** 标记InfluxDB连接是否可用 */
private final AtomicBoolean connectionHealthy = new AtomicBoolean(true);
/** 连续写入失败计数 */
private final AtomicLong consecutiveFailures = new AtomicLong(0);
/** 连接不可用标志 - 当连续失败超过阈值时置为true */
private static final long FAILURE_THRESHOLD = 5;
/** 定时健康检查调度器 */
private ScheduledExecutorService healthCheckScheduler;
/** 健康检查最小间隔(秒) */
private static final int HEALTH_CHECK_MIN_INTERVAL_SECONDS = 3;
/** 健康检查最大间隔(秒) */
private static final int HEALTH_CHECK_MAX_INTERVAL_SECONDS = 60;
/** 当前健康检查间隔(秒),用于指数退避 */
private final AtomicLong currentHealthCheckInterval = new AtomicLong(HEALTH_CHECK_MIN_INTERVAL_SECONDS);
/** 健康检查连续失败计数 */
private final AtomicLong healthCheckFailCount = new AtomicLong(0);
/** 健康检查连续失败多少次后重建客户端 */
private static final long HEALTH_CHECK_RECREATE_THRESHOLD = 3;
/** 当前使用的WriteApi引用(重建客户端时同步更新) */
private volatile WriteApi currentWriteApi;
/** 连接恢复回调监听器列表(线程安全) */
private final List<Runnable> recoveryCallbacks = new CopyOnWriteArrayList<>();
/**
* 创建InfluxDB客户端Bean
* 配置优化的超时参数 - 针对网络不稳定场景优化
* @return InfluxDBClient实例
*/
@Bean
public InfluxDBClient influxDBClient() {
// 配置 OkHttpClient 的超时参数
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 连接超时10秒缩短以快速检测不可达
.readTimeout(30, TimeUnit.SECONDS) // 读取超时30秒
.writeTimeout(30, TimeUnit.SECONDS); // 写入超时30秒
// 使用自定义的 OkHttpClient Builder 创建 InfluxDB 客户端
InfluxDBClientOptions options = InfluxDBClientOptions.builder()
.url(url)
.authenticateToken(token.toCharArray())
.org(org)
.okHttpClient(okHttpClientBuilder)
.build();
this.influxDBClient = InfluxDBClientFactory.create(options);
LOG.info("InfluxDB客户端已创建,连接地址: {}", url);
return this.influxDBClient;
}
/**
* 创建WriteApi Bean
* 配置优化的批量写入选项 - 针对高吞吐量场景优化
* 添加写入事件监听器捕获异步写入错误并将失败数据备份到MQ
* @param client InfluxDB客户端
* @return WriteApi实例
*/
@Bean
public WriteApi writeApi(InfluxDBClient client) {
WriteOptions writeOptions = WriteOptions.builder()
.batchSize(2) //2 批量大小增加到5000提高批量写入效率
.flushInterval(2000) // 2000刷新间隔2秒平衡实时性和性能
.bufferLimit(10000) // 100000缓冲区限制增加到10万避免背压警告
.retryInterval(3000) // 3000重试间隔3秒
.build();
WriteApi writeApi = client.getWriteApi(writeOptions);
this.currentWriteApi = writeApi;
// 注册写入成功事件监听器 - 写入成功时检测状态转换并触发恢复回调
writeApi.listenEvents(WriteSuccessEvent.class, event -> {
// 检测从不可用到可用的状态转换仅在转换时触发恢复回调
boolean wasUnhealthy = !connectionHealthy.getAndSet(true);
if (wasUnhealthy) {
consecutiveFailures.set(0);
healthCheckFailCount.set(0);
currentHealthCheckInterval.set(HEALTH_CHECK_MIN_INTERVAL_SECONDS);
LOG.info("InfluxDB连接已通过异步写入成功恢复正常,触发恢复回调");
fireRecoveryCallbacks();
} else {
consecutiveFailures.set(0);
}
});
// 注册写入错误事件监听器 - 捕获异步写入失败
writeApi.listenEvents(WriteErrorEvent.class, event -> {
long failures = consecutiveFailures.incrementAndGet();
Throwable throwable = event.getThrowable();
String errorMsg = throwable != null ? throwable.getMessage() : "未知错误";
if (failures >= FAILURE_THRESHOLD) {
connectionHealthy.set(false);
LOG.error("InfluxDB写入连续失败{}次,标记连接为不可用状态。错误: {}", failures, errorMsg);
} else {
LOG.warn("InfluxDB异步写入失败(第{}次): {}", failures, errorMsg);
}
});
LOG.info("WriteApi已创建,已注册写入事件监听器");
// 启动定时健康检查
startHealthCheck();
return writeApi;
}
/**
* 注册连接恢复回调监听器
* 当InfluxDB连接从不可用恢复为可用时会依次调用所有已注册的回调
* @param callback 恢复回调
*/
public void addRecoveryCallback(Runnable callback) {
if (callback != null) {
recoveryCallbacks.add(callback);
LOG.info("已注册InfluxDB连接恢复回调,当前回调数量: {}", recoveryCallbacks.size());
}
}
/**
* 触发所有连接恢复回调
*/
private void fireRecoveryCallbacks() {
if (recoveryCallbacks.isEmpty()) {
return;
}
LOG.info("开始触发InfluxDB连接恢复回调,共{}个", recoveryCallbacks.size());
for (Runnable callback : recoveryCallbacks) {
try {
callback.run();
} catch (Exception e) {
LOG.error("执行连接恢复回调时发生异常: {}", e.getMessage(), e);
}
}
}
/**
* 启动定时健康检查带指数退避策略
* 当连接不可用时从3秒开始每次失败后间隔翻倍最大60秒
* 连接恢复后重置间隔
* 健康检查连续失败超过阈值时自动重建InfluxDB客户端连接
*/
private void startHealthCheck() {
healthCheckScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "influxdb-health-check");
t.setDaemon(true);
return t;
});
// 使用递归调度实现动态间隔
scheduleNextHealthCheck();
LOG.info("InfluxDB定时健康检查已启动,初始间隔: {}秒,最大间隔: {}秒,重建阈值: {}次",
HEALTH_CHECK_MIN_INTERVAL_SECONDS, HEALTH_CHECK_MAX_INTERVAL_SECONDS, HEALTH_CHECK_RECREATE_THRESHOLD);
}
/**
* 调度下一次健康检查实现指数退避 + 客户端自动重建
*/
private void scheduleNextHealthCheck() {
long interval = currentHealthCheckInterval.get();
healthCheckScheduler.schedule(() -> {
// 仅在连接不可用时才进行健康检查
if (!connectionHealthy.get()) {
try {
boolean pingResult = influxDBClient.health().getStatus().equals("pass");
if (pingResult) {
long failCount = healthCheckFailCount.get();
LOG.info("InfluxDB健康检查通过,恢复连接状态(上次检查间隔: {}秒,累计失败: {}次)", interval, failCount);
healthCheckFailCount.set(0);
connectionHealthy.set(true);
consecutiveFailures.set(0);
// 重置退避间隔
currentHealthCheckInterval.set(HEALTH_CHECK_MIN_INTERVAL_SECONDS);
// 触发恢复回调
fireRecoveryCallbacks();
} else {
long failCount = healthCheckFailCount.incrementAndGet();
LOG.warn("InfluxDB健康检查未通过(连续失败{}次),下次检查间隔将增加", failCount);
checkAndRecreateClient(failCount);
increaseHealthCheckInterval();
}
} catch (Exception e) {
long failCount = healthCheckFailCount.incrementAndGet();
LOG.warn("InfluxDB健康检查异常(连续失败{}次): {},下次检查间隔将增加", failCount, e.getMessage());
checkAndRecreateClient(failCount);
increaseHealthCheckInterval();
}
} else {
// 连接健康重置间隔和失败计数
healthCheckFailCount.set(0);
currentHealthCheckInterval.set(HEALTH_CHECK_MIN_INTERVAL_SECONDS);
}
// 调度下一次检查
scheduleNextHealthCheck();
}, interval, TimeUnit.SECONDS);
}
/**
* 检查健康检查失败次数是否达到阈值达到则重建InfluxDB客户端和WriteApi
* @param failCount 当前连续失败次数
*/
private synchronized void checkAndRecreateClient(long failCount) {
if (failCount >= HEALTH_CHECK_RECREATE_THRESHOLD && failCount % HEALTH_CHECK_RECREATE_THRESHOLD == 0) {
LOG.warn("健康检查连续失败{}次达到阈值,开始重建InfluxDB客户端连接...", failCount);
try {
recreateInfluxDBClient();
LOG.info("InfluxDB客户端重建完成");
} catch (Exception e) {
LOG.error("InfluxDB客户端重建失败: {}", e.getMessage(), e);
}
}
}
/**
* 重建InfluxDB客户端和WriteApi
* 关闭旧的连接池和WriteApi创建全新的实例解决连接池中的失效连接导致健康检查持续失败的问题
*/
private synchronized void recreateInfluxDBClient() {
try {
// 1. 关闭旧的WriteApi
if (currentWriteApi != null) {
try {
currentWriteApi.close();
LOG.info("旧WriteApi已关闭");
} catch (Exception e) {
LOG.warn("关闭旧WriteApi时发生异常: {}", e.getMessage());
}
}
// 2. 关闭旧的InfluxDBClient
if (influxDBClient != null) {
try {
influxDBClient.close();
LOG.info("旧InfluxDB客户端已关闭");
} catch (Exception e) {
LOG.warn("关闭旧InfluxDB客户端时发生异常: {}", e.getMessage());
}
}
// 3. 创建全新的InfluxDBClient
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS);
InfluxDBClientOptions options = InfluxDBClientOptions.builder()
.url(url)
.authenticateToken(token.toCharArray())
.org(org)
.okHttpClient(okHttpClientBuilder)
.build();
this.influxDBClient = InfluxDBClientFactory.create(options);
LOG.info("新InfluxDB客户端已创建,连接地址: {}", url);
// 4. 创建全新的WriteApi并注册事件监听器
WriteOptions writeOptions = WriteOptions.builder()
.batchSize(2) // 批量大小与Bean配置保持一致
.flushInterval(2000)
.bufferLimit(10000)
.retryInterval(3000)
.build();
WriteApi newWriteApi = influxDBClient.getWriteApi(writeOptions);
this.currentWriteApi = newWriteApi;
// 重新注册写入成功事件监听器
newWriteApi.listenEvents(WriteSuccessEvent.class, event -> {
boolean wasUnhealthy = !connectionHealthy.getAndSet(true);
if (wasUnhealthy) {
consecutiveFailures.set(0);
healthCheckFailCount.set(0);
currentHealthCheckInterval.set(HEALTH_CHECK_MIN_INTERVAL_SECONDS);
LOG.info("InfluxDB连接已通过异步写入成功恢复正常(重建后),触发恢复回调");
fireRecoveryCallbacks();
} else {
consecutiveFailures.set(0);
}
});
// 重新注册写入错误事件监听器
newWriteApi.listenEvents(WriteErrorEvent.class, event -> {
long failures = consecutiveFailures.incrementAndGet();
Throwable throwable = event.getThrowable();
String errorMsg = throwable != null ? throwable.getMessage() : "未知错误";
if (failures >= FAILURE_THRESHOLD) {
connectionHealthy.set(false);
LOG.error("InfluxDB写入连续失败{}次(重建后),标记连接为不可用状态。错误: {}", failures, errorMsg);
} else {
LOG.warn("InfluxDB异步写入失败(第{}次,重建后): {}", failures, errorMsg);
}
});
LOG.info("新WriteApi已创建并注册事件监听器");
} catch (Exception e) {
LOG.error("重建InfluxDB客户端过程中发生异常: {}", e.getMessage(), e);
}
}
/**
* 增加健康检查间隔指数退避上限为MAX
*/
private void increaseHealthCheckInterval() {
long current = currentHealthCheckInterval.get();
long next = Math.min(current * 2, HEALTH_CHECK_MAX_INTERVAL_SECONDS);
currentHealthCheckInterval.set(next);
LOG.debug("健康检查间隔从{}秒增加到{}秒", current, next);
}
/**
* 检查InfluxDB连接是否健康
* @return true表示连接正常false表示连接不可用
*/
public boolean isConnectionHealthy() {
return connectionHealthy.get();
}
/**
* 立即标记连接为不健康状态
* 当同步写入遇到超时/连接异常时调用避免其他线程继续尝试无效写入
*/
public void markConnectionUnhealthy() {
if (connectionHealthy.getAndSet(false)) {
LOG.warn("InfluxDB连接已被同步写入异常标记为不可用状态,后续数据将直接发送到MQ备份");
}
}
/**
* 获取当前可用的WriteApi实例
* 当客户端被重建后返回的是新的WriteApi实例
* @return 当前WriteApi实例
*/
public WriteApi getCurrentWriteApi() {
return currentWriteApi;
}
/**
* 获取组织名称
* @return 组织名称
*/
public String getOrg() {
return org;
}
/**
* 获取InfluxDB连接URL
* @return 连接URL
*/
public String getUrl() {
return url;
}
/**
* 应用关闭时清理资源
*/
@PreDestroy
public void destroy() {
// 关闭健康检查调度器
if (healthCheckScheduler != null) {
healthCheckScheduler.shutdown();
}
if (influxDBClient != null) {
try {
influxDBClient.close();
} catch (Exception e) {
// 记录日志但不抛出异常避免影响关闭流程
LOG.warn("关闭InfluxDB客户端时发生异常: {}", e.getMessage());
}
}
}
}

+ 227
- 14
src/main/java/com/topsail/influxdb/rabbitmq/AmqpListener.java View File

@ -1,6 +1,7 @@
package com.topsail.influxdb.rabbitmq; package com.topsail.influxdb.rabbitmq;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.rabbitmq.client.Channel; import com.rabbitmq.client.Channel;
import com.topsail.influxdb.entity.DeviceLogData; import com.topsail.influxdb.entity.DeviceLogData;
import com.topsail.influxdb.pojo.History; import com.topsail.influxdb.pojo.History;
@ -17,6 +18,9 @@ import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.Set;
@Component @Component
public class AmqpListener { public class AmqpListener {
public static final Logger LOG = LoggerFactory.getLogger(AmqpListener.class); public static final Logger LOG = LoggerFactory.getLogger(AmqpListener.class);
@ -36,24 +40,112 @@ public class AmqpListener {
* @param channel * @param channel
* @throws Exception * @throws Exception
*/ */
@RabbitListener(queues = "shengdilandevicedataall")
// @RabbitListener(queues = "shengdilandevicedataall")
public void deviceDataMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception { public void deviceDataMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception {
if (StringUtils.isEmpty(message)) { if (StringUtils.isEmpty(message)) {
channel.basicAck(deliveryTag, false); channel.basicAck(deliveryTag, false);
return; return;
} }
try { try {
History history = JSON.parseObject(message, History.class); History history = JSON.parseObject(message, History.class);
deviceDataService.saveDeviceDataToInfluxdb(history); deviceDataService.saveDeviceDataToInfluxdb(history);
LOG.info("saveDeviceDataToInfluxdb OK:" + history.getImei());
// 成功写入后确认消息
channel.basicAck(deliveryTag, false);
} catch (JSONException je) {
// 毒丸保护JSON解析异常不会通过重试恢复直接丢弃消息并记录日志
LOG.error("JSON解析异常(不可恢复),消息将被丢弃: {}", je.getMessage(), je);
LOG.error("被丢弃的消息内容: {}", message);
channel.basicAck(deliveryTag, false); channel.basicAck(deliveryTag, false);
} catch (Exception e) { } catch (Exception e) {
channel.basicNack(deliveryTag, false, true);
LOG.info("saveDeviceDataToInfluxdb Error:(" + e + ")-" + message);
LOG.error("处理设备数据时发生异常: {}", e.getMessage(), e);
// 关键保护无论什么异常都确保消息不丢失
boolean messageHandled = false;
try {
// 检查是否为 InfluxDB 超时或连接异常
boolean isInfluxTimeout = isInfluxDBTimeoutException(e);
if (isInfluxTimeout) {
LOG.warn("检测到 InfluxDB 超时/连接异常,将消息发送到备份队列: {}", e.getMessage());
amqpService.SendMessage("shengdilandevicedataback", message);
LOG.info("消息已发送到备份队列,确认原消息");
messageHandled = true;
} else {
LOG.error("非超时异常,消息重新入队等待重试: {}", e.getMessage());
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return; // 避免重复确认
}
} catch (Exception sendError) {
// 最关键的保护如果发送备份队列也失败不要确认原消息
LOG.error("发送备份队列失败,原消息将重新入队: {}", sendError.getMessage(), sendError);
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return;
}
// 只有成功处理后才确认原消息
if (messageHandled) {
channel.basicAck(deliveryTag, false);
}
} }
} }
// @RabbitListener(queues = "shengdilandevicedataback")
public void deviceDataMqListenerForBackup(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception {
if (StringUtils.isEmpty(message)) {
channel.basicAck(deliveryTag, false);
return;
}
try {
History history = JSON.parseObject(message, History.class);
deviceDataService.saveDeviceDataToInfluxdb(history);
// 成功写入后确认消息
channel.basicAck(deliveryTag, false);
} catch (JSONException je) {
// 毒丸保护JSON解析异常不会通过重试恢复直接丢弃消息并记录日志
LOG.error("JSON解析异常(不可恢复),消息将被丢弃: {}", je.getMessage(), je);
LOG.error("被丢弃的消息内容: {}", message);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
LOG.error("处理设备数据时发生异常: {}", e.getMessage(), e);
// 关键保护无论什么异常都确保消息不丢失
boolean messageHandled = false;
try {
// 检查是否为 InfluxDB 超时或连接异常
boolean isInfluxTimeout = isInfluxDBTimeoutException(e);
if (isInfluxTimeout) {
LOG.warn("检测到 InfluxDB 超时/连接异常,将消息发送到备份队列: {}", e.getMessage());
amqpService.SendMessage("shengdilandevicedataback", message);
LOG.info("消息已发送到备份队列,确认原消息");
messageHandled = true;
} else {
LOG.error("非超时异常,消息重新入队等待重试: {}", e.getMessage());
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return; // 避免重复确认
}
} catch (Exception sendError) {
// 最关键的保护如果发送备份队列也失败不要确认原消息
LOG.error("发送备份队列失败,原消息将重新入队: {}", sendError.getMessage(), sendError);
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return;
}
// 只有成功处理后才确认原消息
if (messageHandled) {
channel.basicAck(deliveryTag, false);
}
}
}
/** /**
* 监听设备下发命令日志 * 监听设备下发命令日志
* *
@ -62,23 +154,56 @@ public class AmqpListener {
* @param channel * @param channel
* @throws Exception * @throws Exception
*/ */
@RabbitListener(queues = "shengdilandevicelogall")
// @RabbitListener(queues = "shengdilandevicelogall")
public void deviceLogMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception { public void deviceLogMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception {
if (StringUtils.isEmpty(message)) { if (StringUtils.isEmpty(message)) {
channel.basicAck(deliveryTag, false); channel.basicAck(deliveryTag, false);
return; return;
} }
try { try {
DeviceLogData deviceLogData = JSON.parseObject(message, DeviceLogData.class); DeviceLogData deviceLogData = JSON.parseObject(message, DeviceLogData.class);
deviceLogService.saveDeviceLogToInfluxdb(deviceLogData); deviceLogService.saveDeviceLogToInfluxdb(deviceLogData);
LOG.info("saveDeviceLogToInfluxdb OK:" + deviceLogData.getImei());
// 成功写入后确认消息
channel.basicAck(deliveryTag, false);
} catch (JSONException je) {
// 毒丸保护JSON解析异常不会通过重试恢复直接丢弃消息并记录日志
LOG.error("JSON解析异常(不可恢复),消息将被丢弃: {}", je.getMessage(), je);
LOG.error("被丢弃的消息内容: {}", message);
channel.basicAck(deliveryTag, false); channel.basicAck(deliveryTag, false);
} catch (Exception e) { } catch (Exception e) {
channel.basicNack(deliveryTag, false, true);
LOG.info("saveDeviceLogToInfluxdb Error:(" + e + ")-" + message);
LOG.error("处理设备日志时发生异常: {}", e.getMessage(), e);
boolean messageHandled = false;
try {
boolean isInfluxTimeout = isInfluxDBTimeoutException(e);
if (isInfluxTimeout) {
LOG.warn("检测到 InfluxDB 超时/连接异常,将消息发送到备份队列: {}", e.getMessage());
amqpService.SendMessage("shengdilandevicelogback", message);
LOG.info("消息已发送到备份队列,确认原消息");
messageHandled = true;
} else {
LOG.error("非超时异常,消息重新入队等待重试: {}", e.getMessage());
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return;
}
} catch (Exception sendError) {
LOG.error("发送备份队列失败,原消息将重新入队: {}", sendError.getMessage(), sendError);
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return;
}
if (messageHandled) {
channel.basicAck(deliveryTag, false);
}
} }
} }
/** /**
* 更新设备下发命令日志 * 更新设备下发命令日志
* *
@ -87,21 +212,109 @@ public class AmqpListener {
* @param channel * @param channel
* @throws Exception * @throws Exception
*/ */
@RabbitListener(queues = "shengdilandevicelogupdate") @RabbitListener(queues = "shengdilandevicelogupdate")
public void updateDeviceLogMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception { public void updateDeviceLogMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception {
if (StringUtils.isEmpty(message)) { if (StringUtils.isEmpty(message)) {
channel.basicAck(deliveryTag, false); channel.basicAck(deliveryTag, false);
return; return;
} }
try { try {
DeviceLogData deviceLogData = JSON.parseObject(message, DeviceLogData.class); DeviceLogData deviceLogData = JSON.parseObject(message, DeviceLogData.class);
deviceLogService.updateDeviceLogMqListener(deviceLogData); deviceLogService.updateDeviceLogMqListener(deviceLogData);
LOG.info("saveDeviceLogToInfluxdb OK:" + deviceLogData.getImei());
// 成功写入后确认消息
channel.basicAck(deliveryTag, false);
} catch (JSONException je) {
// 毒丸保护JSON解析异常不会通过重试恢复直接丢弃消息并记录日志
LOG.error("JSON解析异常(不可恢复),消息将被丢弃: {}", je.getMessage(), je);
LOG.error("被丢弃的消息内容: {}", message);
channel.basicAck(deliveryTag, false); channel.basicAck(deliveryTag, false);
} catch (Exception e) { } catch (Exception e) {
channel.basicNack(deliveryTag, false, true);
LOG.info("saveDeviceLogToInfluxdb Error:(" + e + ")-" + message);
LOG.error("更新设备日志时发生异常: {}", e.getMessage(), e);
boolean messageHandled = false;
try {
boolean isInfluxTimeout = isInfluxDBTimeoutException(e);
if (isInfluxTimeout) {
LOG.warn("检测到 InfluxDB 超时/连接异常,将消息发送到备份队列: {}", e.getMessage());
amqpService.SendMessage("shengdilandevicelogback", message);
LOG.info("消息已发送到备份队列,确认原消息");
messageHandled = true;
} else {
LOG.error("非超时异常,消息重新入队等待重试: {}", e.getMessage());
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return;
}
} catch (Exception sendError) {
LOG.error("发送备份队列失败,原消息将重新入队: {}", sendError.getMessage(), sendError);
channel.basicNack(deliveryTag, false, true);
messageHandled = true;
return;
}
if (messageHandled) {
channel.basicAck(deliveryTag, false);
}
}
}
/**
* 检查异常是否为 InfluxDB 超时连接失败或 shard 损坏异常
* @param e 异常对象
* @return 如果是超时连接失败或 shard 损坏异常返回 true否则返回 false
*/
private boolean isInfluxDBTimeoutException(Exception e) {
String message = e.getMessage();
// 检查 shard 损坏相关的错误
if (message != null && (message.contains("not attempting to open shard") ||
message.contains("short buffer") ||
message.contains("InternalServerErrorException"))) {
return true;
}
// 检查连接失败相关的错误
if (message != null && (message.contains("Failed to connect") ||
message.contains("ConnectException") ||
message.contains("Connection refused") ||
message.contains("Connection timed out"))) {
return true;
}
// 检查直接异常类型
if (e instanceof com.influxdb.exceptions.InfluxException) {
if (message != null && (message.contains("Read timed out") ||
message.contains("SocketTimeoutException") ||
message.contains("timeout") ||
message.contains("connect timed out"))) {
return true;
}
}
// 检查根本原因使用HashSet防止循环引用导致死循环
Throwable cause = e.getCause();
Set<Throwable> visitedCauses = new HashSet<>();
while (cause != null && visitedCauses.add(cause)) {
if (cause instanceof java.net.SocketTimeoutException ||
cause instanceof java.net.ConnectException) {
return true;
}
if (cause.getMessage() != null &&
(cause.getMessage().contains("Read timed out") ||
cause.getMessage().contains("SocketTimeoutException") ||
cause.getMessage().contains("connect timed out") ||
cause.getMessage().contains("Failed to connect") ||
cause.getMessage().contains("ConnectException") ||
cause.getMessage().contains("Connection timed out"))) {
return true;
}
cause = cause.getCause();
} }
return false;
} }
} }

+ 221
- 12
src/main/java/com/topsail/influxdb/rabbitmq/service/AmqpService.java View File

@ -1,46 +1,255 @@
package com.topsail.influxdb.rabbitmq.service; package com.topsail.influxdb.rabbitmq.service;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message; import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.TimeoutException;
@Service @Service
public class AmqpService { public class AmqpService {
public static final Logger LOG = LoggerFactory.getLogger(AmqpService.class); public static final Logger LOG = LoggerFactory.getLogger(AmqpService.class);
private final AmqpAdmin amqpAdmin; private final AmqpAdmin amqpAdmin;
private final AmqpTemplate amqpTemplate; private final AmqpTemplate amqpTemplate;
private final ConnectionFactory connectionFactory;
// 从配置文件读取 RabbitMQ Management API 配置
@Value("${rabbitmq.management.url:http://localhost:15672}")
private String managementUrl;
@Value("${rabbitmq.management.username:guest}")
private String managementUsername;
@Value("${rabbitmq.management.password:guest}")
private String managementPassword;
@Value("${rabbitmq.management.vhost:/}")
private String managementVhost;
// 缓存认证头避免重复计算使用volatile保证可见性
private volatile String cachedAuthHeader = null;
@Autowired @Autowired
public AmqpService(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) {
public AmqpService(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate, ConnectionFactory connectionFactory) {
this.amqpAdmin = amqpAdmin; this.amqpAdmin = amqpAdmin;
this.amqpTemplate = amqpTemplate; this.amqpTemplate = amqpTemplate;
this.connectionFactory = connectionFactory;
} }
public void SendMessage(String queue,String content){
//LOG.info("Send ampg" + content);
this.amqpTemplate.convertAndSend(queue,content);
public void SendMessage(String queue, String content) {
this.amqpTemplate.convertAndSend(queue, content);
} }
public void SendExchange(String exchange,String content){
this.amqpTemplate.convertAndSend(exchange,"",content);
public void SendExchange(String exchange, String content) {
this.amqpTemplate.convertAndSend(exchange, "", content);
} }
public boolean IsQueuesEmpty(String queue){
try{
public boolean IsQueuesEmpty(String queue) {
try {
Message msg = this.amqpTemplate.receive(queue); Message msg = this.amqpTemplate.receive(queue);
if(msg!=null){
this.amqpTemplate.send(queue,msg);
if (msg != null) {
this.amqpTemplate.send(queue, msg);
return false; return false;
}else{
} else {
return true; return true;
} }
}catch (Exception e){
} catch (Exception e) {
return true; return true;
} }
} }
/**
* 获取指定队列的消息数量兼容 Spring Boot 2.1.8
* @param queueName 队列名称
* @return 队列中待消费的消息数量如果获取失败或队列不存在则返回-1
*/
public long getQueueMessageCount(String queueName) {
if (queueName == null || queueName.trim().isEmpty()) {
LOG.warn("队列名称不能为空");
return -1;
}
org.springframework.amqp.rabbit.connection.Connection connection = null;
Channel channel = null;
try {
connection = connectionFactory.createConnection();
channel = connection.createChannel(false);
// queueDeclarePassive 不会创建队列只是获取队列信息如果队列不存在会抛出异常
com.rabbitmq.client.AMQP.Queue.DeclareOk declareOk = channel.queueDeclarePassive(queueName);
long messageCount = declareOk.getMessageCount();
LOG.debug("队列 [{}] 当前消息数量: {}", queueName, messageCount);
return messageCount;
} catch (IOException e) {
String errorMsg = e.getMessage();
if (errorMsg != null && (errorMsg.contains("NOT_FOUND") || errorMsg.contains("404"))) {
LOG.warn("队列 [{}] 不存在", queueName);
} else {
LOG.error("获取队列 [{}] 消息数量时发生IO异常: {}", queueName, errorMsg);
}
return -1;
} catch (Exception e) {
LOG.error("获取队列 [{}] 消息数量时发生异常", queueName, e);
return -1;
} finally {
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException | TimeoutException e) {
LOG.warn("关闭 Channel 时发生异常", e);
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception e) {
LOG.warn("关闭 Connection 时发生异常", e);
}
}
}
}
/**
* 获取RabbitMQ中所有的队列名称通过 Management HTTP API
* @return 队列名称列表如果获取失败则返回空列表
*/
public List<String> getAllQueueNames() {
List<String> queueNames = new ArrayList<>();
String baseUrl = managementUrl.endsWith("/") ? managementUrl.substring(0, managementUrl.length() - 1) : managementUrl;
String apiUrl = baseUrl + "/api/queues";
if (managementVhost != null && !managementVhost.trim().isEmpty() && !"/".equals(managementVhost)) {
try {
String encodedVhost = java.net.URLEncoder.encode(managementVhost, StandardCharsets.UTF_8.name());
apiUrl = baseUrl + "/api/queues/" + encodedVhost;
} catch (Exception e) {
LOG.warn("虚拟主机名称编码失败,使用默认队列列表", e);
}
}
HttpURLConnection connection = null;
InputStream inputStream = null;
BufferedReader reader = null;
try {
LOG.debug("正在连接 RabbitMQ Management API: {}", apiUrl);
URL url = new URL(apiUrl);
connection = (HttpURLConnection) url.openConnection();
String authHeader = getAuthHeader();
connection.setRequestProperty("Authorization", authHeader);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(10000);
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
inputStream = connection.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
StringBuilder response = new StringBuilder();
char[] buffer = new char[4096];
int bytesRead;
while ((bytesRead = reader.read(buffer)) != -1) {
response.append(buffer, 0, bytesRead);
}
String jsonResponse = response.toString().trim();
if (!jsonResponse.isEmpty()) {
try {
JSONArray queues = JSONArray.parseArray(jsonResponse);
if (queues != null && !queues.isEmpty()) {
for (int i = 0; i < queues.size(); i++) {
JSONObject queue = queues.getJSONObject(i);
String queueName = queue.getString("name");
if (queueName != null && !queueName.trim().isEmpty()) {
queueNames.add(queueName);
}
}
LOG.info("成功获取 {} 个队列名称", queueNames.size());
} else {
LOG.info("RabbitMQ 中暂无队列");
}
} catch (Exception jsonEx) {
LOG.error("解析 RabbitMQ Management API 响应失败", jsonEx);
}
} else {
LOG.warn("RabbitMQ Management API 返回空响应");
}
} else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
LOG.error("RabbitMQ Management API 认证失败,请检查用户名和密码配置");
} else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
LOG.error("RabbitMQ Management API 未找到,请确认 rabbitmq_management 插件已启用");
} else {
LOG.warn("RabbitMQ Management API 响应码: {}, 消息: {}", responseCode, connection.getResponseMessage());
}
} catch (IOException e) {
LOG.error("获取队列列表时发生IO异常,请确认 RabbitMQ Management 插件已启用且服务正常运行", e);
} catch (Exception e) {
LOG.error("获取队列列表时发生未知异常", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
LOG.warn("关闭 BufferedReader 时发生异常", e);
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LOG.warn("关闭 InputStream 时发生异常", e);
}
}
if (connection != null) {
connection.disconnect();
}
}
return queueNames;
}
/**
* 获取或生成 Basic Auth 认证头
* @return Authorization header
*/
private String getAuthHeader() {
if (cachedAuthHeader == null) {
synchronized (this) {
if (cachedAuthHeader == null) {
String auth = managementUsername + ":" + managementPassword;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
cachedAuthHeader = "Basic " + encodedAuth;
}
}
}
return cachedAuthHeader;
}
} }

+ 378
- 144
src/main/java/com/topsail/influxdb/service/DeviceDataService.java View File

@ -5,10 +5,10 @@ import com.alibaba.fastjson.JSONObject;
import com.influxdb.client.DeleteApi; import com.influxdb.client.DeleteApi;
import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory; import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.WriteApi;
import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.domain.WritePrecision;
import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxRecord;
import com.influxdb.query.FluxTable; import com.influxdb.query.FluxTable;
import com.topsail.influxdb.config.InfluxDBConfig;
import com.topsail.influxdb.entity.*; import com.topsail.influxdb.entity.*;
import com.topsail.influxdb.mapper.DeviceInfoMapper; import com.topsail.influxdb.mapper.DeviceInfoMapper;
import com.topsail.influxdb.pojo.History; import com.topsail.influxdb.pojo.History;
@ -33,7 +33,8 @@ import java.util.*;
@Service @Service
public class DeviceDataService { public class DeviceDataService {
public static final Logger LOG = LoggerFactory.getLogger(DeviceDataService.class); public static final Logger LOG = LoggerFactory.getLogger(DeviceDataService.class);
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 使用 ThreadLocal 解决 SimpleDateFormat 线程安全问题
private static final ThreadLocal<SimpleDateFormat> dateFormatLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// InfluxDB基础配置 // InfluxDB基础配置
private static final String DEVICEDATA_BUCKET_NAME = "iot"; private static final String DEVICEDATA_BUCKET_NAME = "iot";
@Value("${shengdilan.influxdb.token}") @Value("${shengdilan.influxdb.token}")
@ -54,24 +55,54 @@ public class DeviceDataService {
DeviceInfoMapper deviceInfoMapper; DeviceInfoMapper deviceInfoMapper;
@Autowired @Autowired
AmqpService amqpService; AmqpService amqpService;
private InfluxDBClient influxDBClient;
@Autowired
InfluxDBClient influxDBClient;
@Autowired
InfluxDBConfig influxDBConfig;
// 旧InfluxDB客户端用于数据迁移
private InfluxDBClient oldInfluxDBClient; private InfluxDBClient oldInfluxDBClient;
/** 备份队列名称 */
private static final String BACKUP_QUEUE_NAME = "shengdilandevicedataback";
@PostConstruct @PostConstruct
public void init() { public void init() {
this.influxDBClient = InfluxDBClientFactory.create(url, token.toCharArray(), org);
this.oldInfluxDBClient = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg); this.oldInfluxDBClient = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg);
// 注册连接恢复回调当InfluxDB恢复时报告备份队列积压情况
influxDBConfig.addRecoveryCallback(this::onConnectionRecovered);
LOG.info("DeviceDataService初始化完成,已注册InfluxDB连接恢复回调");
} }
@PreDestroy @PreDestroy
public void destroy() { public void destroy() {
if (influxDBClient != null) {
influxDBClient.close();
}
if (oldInfluxDBClient != null) { if (oldInfluxDBClient != null) {
oldInfluxDBClient.close();
try {
oldInfluxDBClient.close();
} catch (Exception e) {
LOG.warn("关闭旧InfluxDB客户端时发生异常: {}", e.getMessage());
}
} }
} }
/**
* InfluxDB连接恢复后的回调处理
* 报告备份队列积压情况@RabbitListener会自动重试处理备份队列中的消息
*/
private void onConnectionRecovered() {
try {
long backupQueueSize = amqpService.getQueueMessageCount(BACKUP_QUEUE_NAME);
if (backupQueueSize > 0) {
LOG.info("InfluxDB连接已恢复,备份队列 [{}] 当前积压{}条消息,@RabbitListener将自动重试处理",
BACKUP_QUEUE_NAME, backupQueueSize);
} else {
LOG.info("InfluxDB连接已恢复,备份队列无积压消息");
}
} catch (Exception e) {
LOG.warn("InfluxDB连接已恢复,但检查备份队列积压情况时发生异常: {}", e.getMessage());
}
}
/** /**
* 根据设备号查询设备历史数据 * 根据设备号查询设备历史数据
* *
@ -85,60 +116,61 @@ public class DeviceDataService {
*/ */
public List<DeviceHistoryVo> getDeviceHistoryData(String uid, Integer pageNo, Integer pageSize, String startTime, String endTime, String imei) { public List<DeviceHistoryVo> getDeviceHistoryData(String uid, Integer pageNo, Integer pageSize, String startTime, String endTime, String imei) {
InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org); InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org);
StringBuffer query = new StringBuffer();
query.append("from(bucket: \"iot\") ");
if (startTime != null && endTime != null) {
SimpleDateFormat oldFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat newFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
newFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//时区转换
String start = null;
try {
start = newFormat.format(oldFormat.parse(startTime));
String stop = newFormat.format(oldFormat.parse(endTime));
query.append(String.format(" |> range(start:%s, stop:%s)", start, stop));
} catch (ParseException e) {
e.printStackTrace();
try {
StringBuffer query = new StringBuffer();
query.append("from(bucket: \"iot\") ");
if (startTime != null && endTime != null) {
SimpleDateFormat oldFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat newFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
newFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//时区转换
String start = null;
try {
start = newFormat.format(oldFormat.parse(startTime));
String stop = newFormat.format(oldFormat.parse(endTime));
query.append(String.format(" |> range(start:%s, stop:%s)", start, stop));
} catch (ParseException e) {
e.printStackTrace();
}
} else {
query.append("|> range(start: -1d)");
} }
} else {
query.append("|> range(start: -1d)");
}
query.append(String.format(" |> filter(fn: (r) => r[\"_measurement\"] == \"history\") |> filter(fn: (r) => r[\"imei\"] == \"%s\") |> filter(fn: (r) => r[\"_field\"] == \"jsondata\" ) |> sort(columns:[\"_time\"], desc:true) ", imei));
if (pageNo != null && pageSize != null) {
query.append(String.format(" |> limit(n: %s,offset:%s)", pageSize, pageNo - 1));
}
query.append(" |> yield(name: \"last\")");
System.out.println("查询语句==========:" + query);
List<FluxTable> tables = client.getQueryApi().query(query.toString());
// List<DeviceHistoryData> returnList = new ArrayList<>();
List<DeviceHistoryVo> resultSet = new ArrayList<>();
for (FluxTable fluxTable : tables) {
List<FluxRecord> records = fluxTable.getRecords();
for (FluxRecord fluxRecord : records) {
String value = (String) fluxRecord.getValueByKey("_value");
DeviceHistoryData data = JSONObject.parseObject(value, DeviceHistoryData.class);
Integer singalstrength = (Integer) JSONObject.parseObject(value).get("singalstrength");
data.setSignalStrength(singalstrength);
data.setTime(dateFormat.format((Long.parseLong(data.getTime()))));
DeviceHistoryVo dataVo = DeviceHistoryVo.builder()
.deviceType(String.valueOf(data.getDeviceType()))
.imei(data.getImei())
.batteryLevel(data.getBatteryLevel())
.singalStrength(data.getSignalStrength())
.sampleData(data.getSampleData())
.passNum(data.getPassNum())
.alarmType(data.getAlarmType())
.unit(data.getUnit())
.sendTime(data.getTime())
.dataBody(data.getDataBody())
.value(data.getValue()).build();
dataVo = analysisSampleData(dataVo, data.getValue(), data.getUnit());
resultSet.add(dataVo);
// returnList.add(data);
query.append(String.format(" |> filter(fn: (r) => r[\"_measurement\"] == \"history\") |> filter(fn: (r) => r[\"imei\"] == \"%s\") |> filter(fn: (r) => r[\"_field\"] == \"jsondata\" ) |> sort(columns:[\"_time\"], desc:true) ", imei));
if (pageNo != null && pageSize != null) {
query.append(String.format(" |> limit(n: %s,offset:%s)", pageSize, pageNo - 1));
}
query.append(" |> yield(name: \"last\")");
System.out.println("查询语句==========:" + query);
List<FluxTable> tables = client.getQueryApi().query(query.toString());
List<DeviceHistoryVo> resultSet = new ArrayList<>();
for (FluxTable fluxTable : tables) {
List<FluxRecord> records = fluxTable.getRecords();
for (FluxRecord fluxRecord : records) {
String value = (String) fluxRecord.getValueByKey("_value");
DeviceHistoryData data = JSONObject.parseObject(value, DeviceHistoryData.class);
Integer singalstrength = (Integer) JSONObject.parseObject(value).get("singalstrength");
data.setSignalStrength(singalstrength);
data.setTime(dateFormatLocal.get().format((Long.parseLong(data.getTime()))));
DeviceHistoryVo dataVo = DeviceHistoryVo.builder()
.deviceType(String.valueOf(data.getDeviceType()))
.imei(data.getImei())
.batteryLevel(data.getBatteryLevel())
.singalStrength(data.getSignalStrength())
.sampleData(data.getSampleData())
.passNum(data.getPassNum())
.alarmType(data.getAlarmType())
.unit(data.getUnit())
.sendTime(data.getTime())
.dataBody(data.getDataBody())
.value(data.getValue()).build();
dataVo = analysisSampleData(dataVo, data.getValue(), data.getUnit());
resultSet.add(dataVo);
}
} }
Collections.reverse(resultSet);
return resultSet;
} finally {
client.close();
} }
client.close();
Collections.reverse(resultSet);
return resultSet;
} }
/** /**
@ -184,17 +216,13 @@ public class DeviceDataService {
*/ */
public List<DeviceDataInfluxData> getOldInfluxdbData(String imei) { public List<DeviceDataInfluxData> getOldInfluxdbData(String imei) {
DeviceBelongInfo deviceBelongInfo = deviceInfoMapper.queryDeviceBelongInfo(imei); DeviceBelongInfo deviceBelongInfo = deviceInfoMapper.queryDeviceBelongInfo(imei);
// InfluxDBClient client = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg);
StringBuffer query = new StringBuffer(); StringBuffer query = new StringBuffer();
query.append("from(bucket: \"iot\") "); query.append("from(bucket: \"iot\") ");
query.append("|> range(start: -1y)"); query.append("|> range(start: -1y)");
query.append(String.format(" |> filter(fn: (r) => r[\"_measurement\"] == \"history\") |> filter(fn: (r) => r[\"imei\"] == \"%s\") |> filter(fn: (r) => r[\"_field\"] == \"jsondata\" ) |> sort(columns:[\"_time\"], desc:true) ", imei)); query.append(String.format(" |> filter(fn: (r) => r[\"_measurement\"] == \"history\") |> filter(fn: (r) => r[\"imei\"] == \"%s\") |> filter(fn: (r) => r[\"_field\"] == \"jsondata\" ) |> sort(columns:[\"_time\"], desc:true) ", imei));
// if (pageNo != null && pageSize != null) {
// query.append(String.format(" |> limit(n: %s,offset:%s)", pageSize, pageNo - 1));
// }
query.append(" |> yield(name: \"last\")"); query.append(" |> yield(name: \"last\")");
System.out.println("查询语句==========:" + query); System.out.println("查询语句==========:" + query);
if(oldInfluxDBClient==null){
if (oldInfluxDBClient == null) {
oldInfluxDBClient = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg); oldInfluxDBClient = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg);
} }
List<FluxTable> tables = oldInfluxDBClient.getQueryApi().query(query.toString()); List<FluxTable> tables = oldInfluxDBClient.getQueryApi().query(query.toString());
@ -204,10 +232,25 @@ public class DeviceDataService {
List<FluxRecord> records = fluxTable.getRecords(); List<FluxRecord> records = fluxTable.getRecords();
for (FluxRecord fluxRecord : records) { for (FluxRecord fluxRecord : records) {
String value = (String) fluxRecord.getValueByKey("_value"); String value = (String) fluxRecord.getValueByKey("_value");
History history = JSONObject.parseObject(value, History.class);
History history = null;
try {
history = JSONObject.parseObject(value, History.class);
} catch (Exception e) {
LOG.warn("直接解析历史数据JSON失败,尝试清理后解析。IMEI: {}, 错误: {}", imei, e.getMessage());
try {
String cleanedValue = clearDataBodyValue(value);
history = JSONObject.parseObject(cleanedValue, History.class);
String databody = extractAndClearDataBody(value);
history.setDatabody(databody);
LOG.info("成功清理并解析历史数据JSON。IMEI: {}", imei);
} catch (Exception cleanEx) {
LOG.error("清理后仍无法解析历史数据JSON,跳过该条记录。IMEI: {}, 原始数据前200字符: {}",
imei, value != null && value.length() > 200 ? value.substring(0, 200) + "..." : value);
continue;
}
}
if (history != null) { if (history != null) {
Date createtime = history.getSenddate(); Date createtime = history.getSenddate();
//将时间转换成Instant
Instant time = null; Instant time = null;
if (createtime != null) { if (createtime != null) {
time = createtime.toInstant(); time = createtime.toInstant();
@ -221,7 +264,6 @@ public class DeviceDataService {
} }
} }
} }
// client.close();
Collections.reverse(rerurnList); Collections.reverse(rerurnList);
return rerurnList; return rerurnList;
} }
@ -232,14 +274,21 @@ public class DeviceDataService {
* @param history * @param history
*/ */
public void saveDeviceDataToInfluxdb(History history) { public void saveDeviceDataToInfluxdb(History history) {
// 先进行null检查避免空指针异常
if (history == null || (StringUtils.isEmpty(history.getImei()))) {
return;
}
if (history.getSenddate() == null) { if (history.getSenddate() == null) {
return; return;
} }
if (history == null || (StringUtils.isEmpty(history.getImei()))) {
// 检查InfluxDB连接是否可用不可用时直接将数据发送到MQ备份
if (!influxDBConfig.isConnectionHealthy()) {
LOG.warn("InfluxDB连接不可用,数据直接发送到MQ备份。设备: {}", history.getImei());
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
return; return;
} }
//查询设备的所属信息 //查询设备的所属信息
if (history != null && (history.getDeviceBelongInfo() == null || history.getHouseId() == null)) {
if (history.getDeviceBelongInfo() == null || history.getHouseId() == null) {
DeviceBelongInfo deviceBelongInfo = getDeviceBelongInfo(history.getImei().trim()); DeviceBelongInfo deviceBelongInfo = getDeviceBelongInfo(history.getImei().trim());
if (deviceBelongInfo == null) { if (deviceBelongInfo == null) {
return; return;
@ -248,21 +297,72 @@ public class DeviceDataService {
history.setHouseId(deviceBelongInfo.getHouseId()); history.setHouseId(deviceBelongInfo.getHouseId());
} }
Date createtime = history.getSenddate(); Date createtime = history.getSenddate();
//将时间转换成Instant
Instant time = null; Instant time = null;
if (createtime != null) { if (createtime != null) {
time = createtime.toInstant(); time = createtime.toInstant();
} }
// 复用已创建的客户端实例
DeviceDataInfluxData deviceDataInfluxData = rebuildDeviceDataInfluxData(history, time); DeviceDataInfluxData deviceDataInfluxData = rebuildDeviceDataInfluxData(history, time);
try (WriteApi writeApi = influxDBClient.getWriteApi()) {
writeApi.writeMeasurement(DEVICEDATA_BUCKET_NAME, org, WritePrecision.NS, deviceDataInfluxData);
} catch (Exception e) {
e.printStackTrace();
LOG.error("设备数据写入influxdb失败:{}", history.getImei());
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
int maxRetries = 3;
boolean success = false;
for (int retry = 0; retry < maxRetries && !success; retry++) {
try {
influxDBConfig.getCurrentWriteApi().writeMeasurement(DEVICEDATA_BUCKET_NAME, org, WritePrecision.NS, deviceDataInfluxData);
success = true;
LOG.info("设备数据写入influxdb成功:{}", history.getImei());
} catch (Exception e) {
LOG.error("设备数据写入influxdb失败(重试 {}/{}):{},错误:{}",
retry + 1, maxRetries, history.getImei(), e.getMessage());
boolean isTimeout = isInfluxDBTimeoutException(e);
// 如果是服务器内部错误如shard损坏不重试直接发送到MQ
if (e.getMessage() != null && (e.getMessage().contains("InternalServerErrorException") ||
e.getMessage().contains("not attempting to open shard") ||
e.getMessage().contains("short buffer"))) {
LOG.error("InfluxDB服务器内部错误,可能是shard损坏,数据发送到MQ备份。错误详情: {}", e.getMessage());
// 标记连接不健康避免其他线程继续尝试无效写入
influxDBConfig.markConnectionUnhealthy();
try {
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
} catch (Exception sendEx) {
LOG.error("发送备份队列失败,异常将抛回给MQ监听器,消息将重新入队: {}", sendEx.getMessage());
throw new RuntimeException("发送备份队列失败", sendEx);
}
return;
}
// 如果是超时异常立即标记连接不健康发送到备份队列
if (isTimeout) {
LOG.warn("检测到 InfluxDB 超时异常,标记连接不健康,将数据发送到备份队列: {}", e.getMessage());
influxDBConfig.markConnectionUnhealthy();
try {
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
} catch (Exception sendEx) {
LOG.error("发送备份队列失败,异常将抛回给MQ监听器,消息将重新入队: {}", sendEx.getMessage());
throw new RuntimeException("发送备份队列失败", sendEx);
}
return;
}
// 最后一次重试失败发送到MQ
if (retry == maxRetries - 1) {
LOG.error("设备数据写入influxdb最终失败:{}", history.getImei());
try {
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
} catch (Exception sendEx) {
LOG.error("发送备份队列失败,异常将抛回给MQ监听器,消息将重新入队: {}", sendEx.getMessage());
throw new RuntimeException("发送备份队列失败", sendEx);
}
} else {
try {
Thread.sleep(2000 * (retry + 1));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
} }
LOG.info("设备数据写入influxdb成功:{}", history.getImei());
} }
/** /**
@ -281,23 +381,22 @@ public class DeviceDataService {
deviceDataInfluxData.value3 = 0; deviceDataInfluxData.value3 = 0;
deviceDataInfluxData.value4 = 0; deviceDataInfluxData.value4 = 0;
try { try {
if (values.length > 0) {
deviceDataInfluxData.value1 = Double.parseDouble(values[0]);
if (values.length > 0 && values[0] != null && !"null".equals(values[0])) {
deviceDataInfluxData.value1 = parseDoubleValue(values[0]);
} }
if (values.length > 1) {
deviceDataInfluxData.value2 = Double.parseDouble(values[1]);
if (values.length > 1 && values[1] != null && !"null".equals(values[1])) {
deviceDataInfluxData.value2 = parseDoubleValue(values[1]);
} }
if (values.length > 2) {
deviceDataInfluxData.value3 = Double.parseDouble(values[2]);
if (values.length > 2 && values[2] != null && !"null".equals(values[2])) {
deviceDataInfluxData.value3 = parseDoubleValue(values[2]);
} }
if (values.length > 3) {
deviceDataInfluxData.value4 = Double.parseDouble(values[4]);
if (values.length > 3 && values[3] != null && !"null".equals(values[3])) {
deviceDataInfluxData.value4 = parseDoubleValue(values[3]);
} }
} catch (Exception e) { } catch (Exception e) {
LOG.info(e.getMessage());
LOG.warn("解析设备数据值失败,imei: {}, value: {}, 错误: {}", history.getImei(), value, e.getMessage());
} }
} }
//从内部移动处出来value有没有数值都进行插入
deviceDataInfluxData.battery = history.getBatterylevel(); deviceDataInfluxData.battery = history.getBatterylevel();
deviceDataInfluxData.sigal = history.getSingalstrength(); deviceDataInfluxData.sigal = history.getSingalstrength();
deviceDataInfluxData.jsondata = JSON.toJSONString(history); deviceDataInfluxData.jsondata = JSON.toJSONString(history);
@ -308,6 +407,32 @@ public class DeviceDataService {
return deviceDataInfluxData; return deviceDataInfluxData;
} }
/**
* 解析数值字符串
* 支持格式"0.2496Mpa", "25℃", "80%RH", "118.0", "-50.0", "null"
* - 包含单位(Mpa%RH)的字符串提取数值部分
* - 纯数字字符串直接解析
*
* @param value 待解析的字符串
* @return 解析后的double值解析失败返回0
*/
private double parseDoubleValue(String value) {
if (value == null || value.trim().isEmpty() || "null".equals(value)) {
return 0;
}
try {
// 去除所有非数字小数点负号的字符如单位Mpa, , %RH等
String numericStr = value.replaceAll("[^\\d.\\-]", "").trim();
if (numericStr.isEmpty()) {
return 0;
}
return Double.parseDouble(numericStr);
} catch (NumberFormatException e) {
LOG.debug("无法解析数值: {}", value);
return 0;
}
}
/** /**
* 查询设备的所属信息 * 查询设备的所属信息
*/ */
@ -320,16 +445,10 @@ public class DeviceDataService {
*/ */
public void deleteDeviceData() { public void deleteDeviceData() {
InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray()); InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray());
StringBuffer query = new StringBuffer();
query.append("from(bucket: \"iot\") ");
query.append(String.format(" |> filter(fn: (r) => r[\"_measurement\"] == \"history\")"));
query.append("|> range(start: -36d)");
System.out.println("查询语句==========:" + query);
DeleteApi deleteApi = client.getDeleteApi();
OffsetDateTime start = OffsetDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
OffsetDateTime stop = OffsetDateTime.of(2026, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
try { try {
// Delete data with specific time range
DeleteApi deleteApi = client.getDeleteApi();
OffsetDateTime start = OffsetDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
OffsetDateTime stop = OffsetDateTime.of(2026, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
deleteApi.delete(start, stop, "", DEVICEDATA_BUCKET_NAME, org); deleteApi.delete(start, stop, "", DEVICEDATA_BUCKET_NAME, org);
System.out.println("Data deleted successfully"); System.out.println("Data deleted successfully");
} catch (Exception e) { } catch (Exception e) {
@ -345,45 +464,69 @@ public class DeviceDataService {
* @param imei * @param imei
*/ */
public void transferDeviceData(String imei) { public void transferDeviceData(String imei) {
//1.查询所有的设备编号
List<SyncDataFlag> syncDataFlags = deviceInfoMapper.querySyncDeviceDataFlagInfo(imei); List<SyncDataFlag> syncDataFlags = deviceInfoMapper.querySyncDeviceDataFlagInfo(imei);
//2.根据设备编号查询历史Influxdb所有的设备数据
Boolean flag = true; Boolean flag = true;
if (syncDataFlags != null && syncDataFlags.size() > 0) { if (syncDataFlags != null && syncDataFlags.size() > 0) {
for (int i = 0; i < syncDataFlags.size(); i++) { for (int i = 0; i < syncDataFlags.size(); i++) {
if (!flag) { if (!flag) {
syncDataFlags = deviceInfoMapper.querySyncDeviceDataFlagInfo(null); syncDataFlags = deviceInfoMapper.querySyncDeviceDataFlagInfo(null);
if (syncDataFlags == null || syncDataFlags.isEmpty()) {
LOG.warn("重新查询同步标志列表为空,终止同步");
break;
}
} }
int index = new Random().nextInt(syncDataFlags.size()); int index = new Random().nextInt(syncDataFlags.size());
SyncDataFlag syncDataFlag = syncDataFlags.get(index); SyncDataFlag syncDataFlag = syncDataFlags.get(index);
Boolean syncDeviceData = false; Boolean syncDeviceData = false;
Long dataCount = 0L;
List<DeviceDataInfluxData> influxdbDataList = getOldInfluxdbData(syncDataFlag.getImei().toLowerCase(Locale.ROOT)); List<DeviceDataInfluxData> influxdbDataList = getOldInfluxdbData(syncDataFlag.getImei().toLowerCase(Locale.ROOT));
if (influxdbDataList != null && influxdbDataList.size() > 0) { if (influxdbDataList != null && influxdbDataList.size() > 0) {
for (DeviceDataInfluxData influxData : influxdbDataList) { for (DeviceDataInfluxData influxData : influxdbDataList) {
// InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray());
//3.将设备数据保存到influxdb中
if(influxDBClient== null){
influxDBClient = InfluxDBClientFactory.create(url, token.toCharArray());
}
try (WriteApi writeApi = influxDBClient.getWriteApi()) {
writeApi.writeMeasurement(DEVICEDATA_BUCKET_NAME, org, WritePrecision.NS, influxData);
syncDeviceData = true;
} catch (Exception e) {
LOG.error("保存设备数据到influxdb失败:{}", e.getMessage());
syncDeviceData = false;
if (influxData != null && influxData.jsondata != null && !influxData.jsondata.equals("")) {
String message = influxData.jsondata;
History history = JSON.parseObject(message, History.class);
if (history != null && history.getImei() != null && !history.getImei().equals("")) {
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
boolean writeSuccess = false;
int maxRetries = 3;
for (int retry = 0; retry < maxRetries && !writeSuccess; retry++) {
try {
influxDBConfig.getCurrentWriteApi().writeMeasurement(DEVICEDATA_BUCKET_NAME, org, WritePrecision.NS, influxData);
syncDeviceData = true;
dataCount = dataCount + 1;
writeSuccess = true;
LOG.info("设备数据保存到influxdb成功:{}", influxData.imei);
} catch (Exception e) {
LOG.error("保存设备数据到influxdb失败(重试 {}/{}):{},错误:{}",
retry + 1, maxRetries, influxData.imei, e.getMessage());
if (e.getMessage() != null && (e.getMessage().contains("InternalServerErrorException") ||
e.getMessage().contains("not attempting to open shard") ||
e.getMessage().contains("short buffer"))) {
LOG.error("InfluxDB服务器内部错误,可能是shard损坏,数据发送到MQ备份。错误详情: {}", e.getMessage());
if (influxData != null && influxData.jsondata != null && !influxData.jsondata.equals("")) {
History history = JSON.parseObject(influxData.jsondata, History.class);
if (history != null && history.getImei() != null && !history.getImei().equals("")) {
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
}
}
break;
} }
}
if (retry == maxRetries - 1) {
if (influxData != null && influxData.jsondata != null && !influxData.jsondata.equals("")) {
History history = JSON.parseObject(influxData.jsondata, History.class);
if (history != null && history.getImei() != null && !history.getImei().equals("")) {
amqpService.SendMessage("shengdilandevicedataback", JSON.toJSONString(history));
}
}
} else {
try {
Thread.sleep(2000 * (retry + 1));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
} }
} }
} }
if (syncDeviceData) { if (syncDeviceData) {
//4.更新同步设备数据状态
deviceInfoMapper.updateSyncDeviceDataFlagInfo(syncDataFlag.getId(), 1); deviceInfoMapper.updateSyncDeviceDataFlagInfo(syncDataFlag.getId(), 1);
} }
flag = false; flag = false;
@ -396,35 +539,126 @@ public class DeviceDataService {
*/ */
public Long getDeviceDataCount(String imei) { public Long getDeviceDataCount(String imei) {
InfluxDBClient client = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg); InfluxDBClient client = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg);
// 1. 拼接公共过滤条件片段总条数和分页查询共用
StringBuilder filterFragment = new StringBuilder();
filterFragment.append("from(bucket: \"iot\") ");
filterFragment.append("|> range(start: -400d)");
// 2. 拼接固定过滤条件_measurement和_field必选
filterFragment.append(" |> filter(fn: (r) => r[\"_measurement\"] == \"history\")");
filterFragment.append(" |> filter(fn: (r) => r[\"_field\"] == \"jsondata\")");
// 3. 动态拼接imei过滤条件仅当imei非空且非空白字符串时拼接
if (imei != null && !imei.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"imei\"] == \"%s\")", imei.trim()));
}
// 5. 拼接固定排序条件按时间倒序
// 2. 构建总条数查询子句yield命名为total
StringBuilder totalQuery = new StringBuilder();
totalQuery.append(filterFragment); // 复用过滤条件
totalQuery.append(" |> count(column: \"_value\")"); // 统计总条数
totalQuery.append(" |> yield(name: \"total\")\n"); // 标记结果集为total
System.out.println("查询数量语句:" + totalQuery.toString());
//查询总条数
long totalCount = 0;
List<FluxTable> totalTables = client.getQueryApi().query(totalQuery.toString());
for (FluxTable table : totalTables) {
for (FluxRecord record : table.getRecords()) {
Object total = record.getValue();
if (total != null) {
totalCount = totalCount + ((Number) total).longValue();
try {
StringBuilder filterFragment = new StringBuilder();
filterFragment.append("from(bucket: \"iot\") ");
filterFragment.append("|> range(start: -400d)");
filterFragment.append(" |> filter(fn: (r) => r[\"_measurement\"] == \"history\")");
filterFragment.append(" |> filter(fn: (r) => r[\"_field\"] == \"jsondata\")");
if (imei != null && !imei.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"imei\"] == \"%s\")", imei.trim()));
}
StringBuilder totalQuery = new StringBuilder();
totalQuery.append(filterFragment);
totalQuery.append(" |> count(column: \"_value\")");
totalQuery.append(" |> yield(name: \"total\")\n");
System.out.println("查询数量语句:" + totalQuery.toString());
long totalCount = 0;
List<FluxTable> totalTables = client.getQueryApi().query(totalQuery.toString());
for (FluxTable table : totalTables) {
for (FluxRecord record : table.getRecords()) {
Object total = record.getValue();
if (total != null) {
totalCount = totalCount + ((Number) total).longValue();
}
} }
} }
return totalCount;
} finally {
client.close();
}
}
/**
* 检查异常是否为 InfluxDB 超时连接失败或 shard 损坏异常
* @param e 异常对象
* @return 如果是超时连接失败或 shard 损坏异常返回 true否则返回 false
*/
private boolean isInfluxDBTimeoutException(Exception e) {
String message = e.getMessage();
// 检查 shard 损坏相关的错误
if (message != null && (message.contains("not attempting to open shard") ||
message.contains("short buffer") ||
message.contains("InternalServerErrorException"))) {
return true;
}
// 检查连接失败相关的错误
if (message != null && (message.contains("Failed to connect") ||
message.contains("ConnectException") ||
message.contains("Connection refused") ||
message.contains("Connection timed out"))) {
return true;
}
// 检查直接异常类型
if (e instanceof com.influxdb.exceptions.InfluxException) {
if (message != null && (message.contains("Read timed out") ||
message.contains("SocketTimeoutException") ||
message.contains("timeout") ||
message.contains("connect timed out"))) {
return true;
}
}
// 检查根本原因使用HashSet防止循环引用导致死循环
Throwable cause = e.getCause();
Set<Throwable> visitedCauses = new HashSet<>();
while (cause != null && visitedCauses.add(cause)) {
if (cause instanceof java.net.SocketTimeoutException ||
cause instanceof java.net.ConnectException) {
return true;
}
if (cause.getMessage() != null &&
(cause.getMessage().contains("Read timed out") ||
cause.getMessage().contains("SocketTimeoutException") ||
cause.getMessage().contains("connect timed out") ||
cause.getMessage().contains("Failed to connect") ||
cause.getMessage().contains("ConnectException") ||
cause.getMessage().contains("Connection timed out"))) {
return true;
}
cause = cause.getCause();
}
return false;
}
/**
* 清理databody字段值置为null以尝试修复JSON解析问题
*/
private String clearDataBodyValue(String value) {
if (value == null || value.isEmpty()) {
return value;
}
try {
// 使用正则替换databody字段值为null
return value.replaceAll("\"databody\"\\s*:\\s*\"[^\"]*\"", "\"databody\":null");
} catch (Exception e) {
LOG.warn("清理databody字段失败: {}", e.getMessage());
return value;
}
}
/**
* 提取并清理databody字段内容
* 使用正则从原始字符串中提取databody值避免JSON解析失败的问题
*/
private String extractAndClearDataBody(String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
// 使用正则从原始字符串中提取databody字段值
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\"databody\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
java.util.regex.Matcher matcher = pattern.matcher(value);
if (matcher.find()) {
return matcher.group(1);
}
} catch (Exception e) {
LOG.warn("提取databody字段失败: {}", e.getMessage());
} }
return totalCount;
return null;
} }
} }

+ 135
- 88
src/main/java/com/topsail/influxdb/service/DeviceLogService.java View File

@ -5,10 +5,10 @@ import com.alibaba.fastjson.JSONObject;
import com.influxdb.client.DeleteApi; import com.influxdb.client.DeleteApi;
import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory; import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.WriteApi;
import com.influxdb.client.domain.WritePrecision; import com.influxdb.client.domain.WritePrecision;
import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxRecord;
import com.influxdb.query.FluxTable; import com.influxdb.query.FluxTable;
import com.topsail.influxdb.config.InfluxDBConfig;
import com.topsail.influxdb.entity.*; import com.topsail.influxdb.entity.*;
import com.topsail.influxdb.mapper.DeviceInfoMapper; import com.topsail.influxdb.mapper.DeviceInfoMapper;
import com.topsail.influxdb.rabbitmq.service.AmqpService; import com.topsail.influxdb.rabbitmq.service.AmqpService;
@ -34,7 +34,7 @@ public class DeviceLogService {
public static final Logger LOG = LoggerFactory.getLogger(DeviceLogService.class); public static final Logger LOG = LoggerFactory.getLogger(DeviceLogService.class);
private static volatile Map<Integer, String> supplierInfoMap = new HashMap<>(); private static volatile Map<Integer, String> supplierInfoMap = new HashMap<>();
// 初始化时加载圣地蓝项目信息例如在构造函数或@PostConstruct方法中
// 初始化时加载圣地蓝项目信息
@PostConstruct @PostConstruct
public void initShendianlanProjects() { public void initShendianlanProjects() {
if (supplierInfoMap.isEmpty()) { if (supplierInfoMap.isEmpty()) {
@ -42,7 +42,6 @@ public class DeviceLogService {
if (supplierInfoMap.isEmpty()) { if (supplierInfoMap.isEmpty()) {
List<SupplierVO> supplierList = deviceInfoMapper.getSupplierList(); List<SupplierVO> supplierList = deviceInfoMapper.getSupplierList();
if (supplierList != null && !supplierList.isEmpty()) { if (supplierList != null && !supplierList.isEmpty()) {
//查询出属于圣地蓝项目的IMEI号
for (SupplierVO supplierVO : supplierList) { for (SupplierVO supplierVO : supplierList) {
Integer id = supplierVO.getId(); Integer id = supplierVO.getId();
String name = supplierVO.getSupplierName(); String name = supplierVO.getSupplierName();
@ -70,6 +69,8 @@ public class DeviceLogService {
DeviceInfoMapper deviceInfoMapper; DeviceInfoMapper deviceInfoMapper;
@Autowired @Autowired
AmqpService amqpService; AmqpService amqpService;
@Autowired
InfluxDBConfig influxDBConfig;
/** /**
* 存储设备数据到influxdb * 存储设备数据到influxdb
@ -84,13 +85,18 @@ public class DeviceLogService {
if (createtime == null) { if (createtime == null) {
createtime = new Date(); createtime = new Date();
} }
//将时间转换成Instant
Instant time = createtime.toInstant(); Instant time = createtime.toInstant();
InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray());
// 检查InfluxDB连接是否可用不可用时直接将数据发送到MQ备份
if (!influxDBConfig.isConnectionHealthy()) {
LOG.warn("InfluxDB连接不可用,日志数据直接发送到MQ备份。设备: {}", deviceLogData.getImei());
amqpService.SendMessage("shengdilandevicelogback", JSON.toJSONString(deviceLogData));
return;
}
DeviceLogInfluxData deviceLogInfluxData = new DeviceLogInfluxData(); DeviceLogInfluxData deviceLogInfluxData = new DeviceLogInfluxData();
deviceLogInfluxData.id = deviceLogData.getId(); deviceLogInfluxData.id = deviceLogData.getId();
deviceLogInfluxData.imei = deviceLogData.getImei(); deviceLogInfluxData.imei = deviceLogData.getImei();
//从内部移动处出来value有没有数值都进行插入
deviceLogInfluxData.result = deviceLogData.getResult(); deviceLogInfluxData.result = deviceLogData.getResult();
deviceLogInfluxData.statusIssue = deviceLogData.getStatusIssue(); deviceLogInfluxData.statusIssue = deviceLogData.getStatusIssue();
deviceLogInfluxData.supplierId = deviceLogData.getSupplierId(); deviceLogInfluxData.supplierId = deviceLogData.getSupplierId();
@ -103,13 +109,47 @@ public class DeviceLogService {
deviceLogInfluxData.deviceBelongInfo = deviceLogData.getDeviceBelongInfo(); deviceLogInfluxData.deviceBelongInfo = deviceLogData.getDeviceBelongInfo();
deviceLogInfluxData.houseId = deviceLogData.getHouseId(); deviceLogInfluxData.houseId = deviceLogData.getHouseId();
deviceLogInfluxData.time = time != null ? time : Instant.now(); deviceLogInfluxData.time = time != null ? time : Instant.now();
try (WriteApi writeApi = client.getWriteApi()) {
writeApi.writeMeasurement(LOG_BUCKET_NAME, org, WritePrecision.NS, deviceLogInfluxData);
} catch (Exception e) {
LOG.error("下发日志写入influxdb失败:{}", e.getMessage());
amqpService.SendMessage("shengdilandevicelogback", JSON.toJSONString(deviceLogData));
int maxRetries = 3;
boolean success = false;
for (int retry = 0; retry < maxRetries && !success; retry++) {
try {
influxDBConfig.getCurrentWriteApi().writeMeasurement(LOG_BUCKET_NAME, org, WritePrecision.NS, deviceLogInfluxData);
success = true;
LOG.info("下发日志写入influxdb成功:{}", deviceLogData.getImei());
} catch (Exception e) {
LOG.error("下发日志写入influxdb失败(重试 {}/{}):{},错误:{}",
retry + 1, maxRetries, deviceLogData.getImei(), e.getMessage());
// 如果是服务器内部错误如shard损坏不重试直接发送到MQ
if (e.getMessage() != null && (e.getMessage().contains("InternalServerErrorException") ||
e.getMessage().contains("not attempting to open shard") ||
e.getMessage().contains("short buffer"))) {
LOG.error("InfluxDB服务器内部错误,可能是shard损坏,数据发送到MQ备份。错误详情: {}", e.getMessage());
influxDBConfig.markConnectionUnhealthy();
amqpService.SendMessage("shengdilandevicelogback", JSON.toJSONString(deviceLogData));
return;
}
// 如果是超时/连接异常立即标记连接不健康
if (isInfluxDBTimeoutException(e)) {
LOG.warn("检测到 InfluxDB 超时异常,标记连接不健康: {}", e.getMessage());
influxDBConfig.markConnectionUnhealthy();
}
// 最后一次重试失败发送到MQ
if (retry == maxRetries - 1) {
LOG.error("下发日志写入influxdb最终失败:{}", deviceLogData.getImei());
amqpService.SendMessage("shengdilandevicelogback", JSON.toJSONString(deviceLogData));
} else {
try {
Thread.sleep(2000 * (retry + 1));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
} }
LOG.info("下发日志写入influxdb成功:{}", deviceLogData.getImei());
} }
/** /**
@ -119,9 +159,7 @@ public class DeviceLogService {
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date()); calendar.setTime(new Date());
InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org); InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org);
// 1. 拼接公共过滤条件片段总条数和分页查询共用
StringBuilder predicates = new StringBuilder(); StringBuilder predicates = new StringBuilder();
// 2. 拼接固定过滤条件_measurement和_field必选
predicates.append("_measurement=devicelog"); predicates.append("_measurement=devicelog");
if (deviceLogId != null) { if (deviceLogId != null) {
if (predicates.length() > 0) { if (predicates.length() > 0) {
@ -131,7 +169,6 @@ public class DeviceLogService {
System.out.println(predicates.toString()); System.out.println(predicates.toString());
DeleteApi deleteApi = client.getDeleteApi(); DeleteApi deleteApi = client.getDeleteApi();
try { try {
// // 删除指定时间范围内所有数据
OffsetDateTime start = OffsetDateTime.of(calendar.get(Calendar.YEAR) - 10, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); OffsetDateTime start = OffsetDateTime.of(calendar.get(Calendar.YEAR) - 10, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
OffsetDateTime stop = OffsetDateTime.of(calendar.get(Calendar.YEAR) + 1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); OffsetDateTime stop = OffsetDateTime.of(calendar.get(Calendar.YEAR) + 1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
deleteApi.delete(start, stop, predicates.toString(), LOG_BUCKET_NAME, org); deleteApi.delete(start, stop, predicates.toString(), LOG_BUCKET_NAME, org);
@ -143,9 +180,6 @@ public class DeviceLogService {
} }
} }
// from(bucket: "devicelog") |> range(start: -3600d) |> filter(fn: (r) => r["_measurement"] == "devicelog") |> filter(fn: (r) => r["_field"] == "logjson") |> filter(fn: (r) => r["deviceBelongInfo"] =~ /.*白志.*/) |> sort(columns:["_time"], desc:true) |> limit(n: 10, offset: 0) |> yield(name: "data")
/** /**
* 查询设备下发命令日志信息 * 查询设备下发命令日志信息
* *
@ -164,10 +198,11 @@ public class DeviceLogService {
public JSONObject getPageDeviceLog(Integer pageNode, Integer pageSize, String startTime, String endTime, String result, String statusIssue, String imei, String supplierId, String companyId, String operator, Integer houseId, String bindingInfo) { public JSONObject getPageDeviceLog(Integer pageNode, Integer pageSize, String startTime, String endTime, String result, String statusIssue, String imei, String supplierId, String companyId, String operator, Integer houseId, String bindingInfo) {
JSONObject resultSet = new JSONObject(); JSONObject resultSet = new JSONObject();
InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org); InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org);
try {
// 1. 拼接公共过滤条件片段总条数和分页查询共用 // 1. 拼接公共过滤条件片段总条数和分页查询共用
StringBuilder filterFragment = new StringBuilder(); StringBuilder filterFragment = new StringBuilder();
filterFragment.append("from(bucket: \"devicelog\") "); filterFragment.append("from(bucket: \"devicelog\") ");
if (startTime != null && startTime != "" && endTime != null && endTime != "") {
if (startTime != null && !startTime.isEmpty() && endTime != null && !endTime.isEmpty()) {
SimpleDateFormat oldFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat oldFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat newFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); SimpleDateFormat newFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
newFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//时区转换 newFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//时区转换
@ -185,63 +220,52 @@ public class DeviceLogService {
// 2. 拼接固定过滤条件_measurement和_field必选 // 2. 拼接固定过滤条件_measurement和_field必选
filterFragment.append(" |> filter(fn: (r) => r[\"_measurement\"] == \"devicelog\")"); filterFragment.append(" |> filter(fn: (r) => r[\"_measurement\"] == \"devicelog\")");
filterFragment.append(" |> filter(fn: (r) => r[\"_field\"] == \"logjson\")"); filterFragment.append(" |> filter(fn: (r) => r[\"_field\"] == \"logjson\")");
// 3. 动态拼接imei过滤条件仅当imei非空且非空白字符串时拼接
// 3. 动态拼接imei过滤条件
if (imei != null && !imei.trim().isEmpty()) { if (imei != null && !imei.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"imei\"] == \"%s\")", imei.trim())); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"imei\"] == \"%s\")", imei.trim()));
} }
// 4. 动态拼接result过滤条件仅当result非空且非空白字符串时拼接
// 4. 动态拼接result过滤条件
if (result != null && !result.trim().isEmpty()) { if (result != null && !result.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"result\"] == \"%s\")", result.trim())); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"result\"] == \"%s\")", result.trim()));
} }
if (statusIssue != null && !statusIssue.trim().isEmpty()) { if (statusIssue != null && !statusIssue.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"statusIssue\"] == \"%s\")", statusIssue.trim())); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"statusIssue\"] == \"%s\")", statusIssue.trim()));
} }
// 5. 动态拼接supplierId过滤条件仅当supplierId非空且非空白字符串时拼接
// 5. 动态拼接supplierId过滤条件
if (supplierId != null && !supplierId.trim().isEmpty()) { if (supplierId != null && !supplierId.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"supplierId\"] == \"%s\")", supplierId.trim())); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"supplierId\"] == \"%s\")", supplierId.trim()));
} }
// 6. 动态拼接companyId过滤条件仅当companyId非空且非空白字符串时拼接
// 6. 动态拼接companyId过滤条件
if (companyId != null && !companyId.trim().isEmpty()) { if (companyId != null && !companyId.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"companyId\"] == \"%s\")", companyId.trim())); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"companyId\"] == \"%s\")", companyId.trim()));
} }
// 7. 动态拼接operator过滤条件仅当operator非空且非空白字符串时拼接
// 7. 动态拼接operator过滤条件
if (operator != null && !operator.trim().isEmpty()) { if (operator != null && !operator.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"operator\"] == \"%s\")", operator.trim())); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"operator\"] == \"%s\")", operator.trim()));
} }
// 8. 动态拼接houseId过滤条件仅当houseId非空且非空白字符串时拼接
// 8. 动态拼接houseId过滤条件
if (houseId != null) { if (houseId != null) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"houseId\"] == \"%s\")", houseId)); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"houseId\"] == \"%s\")", houseId));
} }
// ========== 核心修复Java 8不支持"""改用+拼接多行字符串 ==========
// 1. 拼接模糊过滤条件Flux逻辑不变仅改字符串写法
// 3. 核心修复belonginfo模糊查询两步修复
// 9. 拼接模糊过滤条件
if (bindingInfo != null && !bindingInfo.trim().isEmpty()) { if (bindingInfo != null && !bindingInfo.trim().isEmpty()) {
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"deviceBelongInfo\"] =~ /.*" + bindingInfo.trim() + ".*/)")); filterFragment.append(String.format(" |> filter(fn: (r) => r[\"deviceBelongInfo\"] =~ /.*" + bindingInfo.trim() + ".*/)"));
} }
// if (bindingInfo != null && !bindingInfo.trim().isEmpty()) {
// // 核心containsStr实现模糊匹配先判断字段非空避免报错
// filterFragment.append(String.format(
// " |> filter(fn: (r) => contains(value: r[\"deviceBelongInfo\"], set: [ \"%s\"]))",
// bindingInfo.trim()
// ));
// }
// 5. 拼接固定排序条件按时间倒序
// 2. 构建总条数查询子句yield命名为total
// 构建总条数查询子句
StringBuilder totalQuery = new StringBuilder(); StringBuilder totalQuery = new StringBuilder();
totalQuery.append(filterFragment); // 复用过滤条件
totalQuery.append(" |> count(column: \"_value\")"); // 统计总条数
totalQuery.append(" |> yield(name: \"total\")\n"); // 标记结果集为total
totalQuery.append(filterFragment);
totalQuery.append(" |> count(column: \"_value\")");
totalQuery.append(" |> yield(name: \"total\")\n");
// 3. 构建分页数据查询子句yield命名为data
// 构建分页数据查询子句
StringBuilder dataQuery = new StringBuilder(); StringBuilder dataQuery = new StringBuilder();
dataQuery.append(filterFragment); // 复用过滤条件
dataQuery.append(filterFragment);
dataQuery.append(" |> sort(columns:[\"_time\"], desc:true)"); dataQuery.append(" |> sort(columns:[\"_time\"], desc:true)");
// 处理分页参数默认值 // 处理分页参数默认值
int finalPageNum = pageNode != null ? pageNode : DEFAULT_PAGE_NUM; int finalPageNum = pageNode != null ? pageNode : DEFAULT_PAGE_NUM;
int finalPageSize = pageSize != null ? pageSize : DEFAULT_PAGE_SIZE; int finalPageSize = pageSize != null ? pageSize : DEFAULT_PAGE_SIZE;
int offset = (finalPageNum - 1) * finalPageSize; int offset = (finalPageNum - 1) * finalPageSize;
// dataQuery.append(" |> limit(n: " + finalPageSize + ", offset: " + offset + ")");
dataQuery.append(" |> yield(name: \"data\")"); // 标记结果集为data
dataQuery.append(" |> yield(name: \"data\")");
System.out.println("查询数量语句:" + totalQuery.toString()); System.out.println("查询数量语句:" + totalQuery.toString());
System.out.println("查询数据语句:" + dataQuery.toString()); System.out.println("查询数据语句:" + dataQuery.toString());
//查询总条数 //查询总条数
@ -269,7 +293,6 @@ public class DeviceLogService {
rerurnList.add(data); rerurnList.add(data);
} }
} }
client.close();
Collections.reverse(rerurnList); Collections.reverse(rerurnList);
resultSet.put("count", totalCount); resultSet.put("count", totalCount);
//对结果按照分页要求截取构造数据 //对结果按照分页要求截取构造数据
@ -278,17 +301,18 @@ public class DeviceLogService {
} }
resultSet.put("list", rerurnList); resultSet.put("list", rerurnList);
return resultSet; return resultSet;
} finally {
client.close();
}
} }
/** /**
* Flux正则转义仅转义RE2引擎的特殊字符避免\Q\E * Flux正则转义仅转义RE2引擎的特殊字符避免\Q\E
* Flux正则特殊字符. * + ? | ( ) [ ] { } ^ $ \
*/ */
private static String escapeFluxRegex(String keyword) { private static String escapeFluxRegex(String keyword) {
if (keyword == null || keyword.isEmpty()) { if (keyword == null || keyword.isEmpty()) {
return ""; return "";
} }
// 转义Flux正则的特殊字符替换为\+字符
String[] specialChars = {"\\", ".", "*", "+", "?", "|", "(", ")", "[", "]", "{", "}", "^", "$"}; String[] specialChars = {"\\", ".", "*", "+", "?", "|", "(", ")", "[", "]", "{", "}", "^", "$"};
String escaped = keyword; String escaped = keyword;
for (String ch : specialChars) { for (String ch : specialChars) {
@ -301,17 +325,14 @@ public class DeviceLogService {
* 转存设备命令下发日志数据 * 转存设备命令下发日志数据
*/ */
public void transferDeviceLogData(String searchImei, Integer companyId) { public void transferDeviceLogData(String searchImei, Integer companyId) {
//1. 查询所有的设备编号
List<SyncDataFlag> syncDataFlags = deviceInfoMapper.querySyncDeviceLogFlagInfo(searchImei); List<SyncDataFlag> syncDataFlags = deviceInfoMapper.querySyncDeviceLogFlagInfo(searchImei);
if (syncDataFlags != null && syncDataFlags.size() > 0) { if (syncDataFlags != null && syncDataFlags.size() > 0) {
//2. 根据设备编号查询设备命令下发日志数据
for (SyncDataFlag syncDataFlag : syncDataFlags) { for (SyncDataFlag syncDataFlag : syncDataFlags) {
Boolean flag = false; Boolean flag = false;
String imei = syncDataFlag.getImei(); String imei = syncDataFlag.getImei();
List<DeviceLogData> deviceLogDataList = deviceInfoMapper.queryDeviceLogData(imei, companyId); List<DeviceLogData> deviceLogDataList = deviceInfoMapper.queryDeviceLogData(imei, companyId);
if (deviceLogDataList != null && deviceLogDataList.size() > 0) { if (deviceLogDataList != null && deviceLogDataList.size() > 0) {
DeviceBelongInfo deviceBelongInfo = deviceInfoMapper.queryDeviceBelongInfo(imei);
//3. 批量插入设备命令下发日志数据到influxdb中
DeviceBelongInfo deviceBelongInfo = deviceInfoMapper.queryDeviceBelongInfo(imei);
for (DeviceLogData deviceLogData : deviceLogDataList) { for (DeviceLogData deviceLogData : deviceLogDataList) {
deviceLogData.setDeviceBelongInfo(deviceBelongInfo != null ? deviceBelongInfo.getDeviceBelongInfo() : null); deviceLogData.setDeviceBelongInfo(deviceBelongInfo != null ? deviceBelongInfo.getDeviceBelongInfo() : null);
deviceLogData.setHouseId(deviceBelongInfo != null ? deviceBelongInfo.getHouseId() : null); deviceLogData.setHouseId(deviceBelongInfo != null ? deviceBelongInfo.getHouseId() : null);
@ -329,7 +350,6 @@ public class DeviceLogService {
} }
} }
if (flag) { if (flag) {
//4. 更新同步设备命令下发日志数据状态
deviceInfoMapper.updateSyncDeviceLogFlagInfo(syncDataFlag.getId(), 1); deviceInfoMapper.updateSyncDeviceLogFlagInfo(syncDataFlag.getId(), 1);
} }
} }
@ -340,44 +360,71 @@ public class DeviceLogService {
* 更新设备命令下发日志数据 * 更新设备命令下发日志数据
*/ */
public void updateDeviceLogMqListener(DeviceLogData deviceLogData) { public void updateDeviceLogMqListener(DeviceLogData deviceLogData) {
//步骤1查出ID对应的日志
InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org); InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org);
// 1. 拼接公共过滤条件片段总条数和分页查询共用
StringBuilder filterFragment = new StringBuilder();
filterFragment.append("from(bucket: \"devicelog\") ");
filterFragment.append("|> range(start: -360d)");
// 2. 拼接固定过滤条件_measurement和_field必选
filterFragment.append(" |> filter(fn: (r) => r[\"_measurement\"] == \"devicelog\")");
filterFragment.append(" |> filter(fn: (r) => r[\"_field\"] == \"logjson\")");
// 3. 动态拼接imei过滤条件仅当imei非空且非空白字符串时拼接
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"id\"] == \"%s\")", deviceLogData.getId()));
// 3. 构建分页数据查询子句yield命名为data
StringBuilder dataQuery = new StringBuilder();
dataQuery.append(filterFragment); // 复用过滤条件
dataQuery.append(" |> sort(columns:[\"_time\"], desc:true)");
dataQuery.append(" |> yield(name: \"data\")"); // 标记结果集为data
System.out.println("查询数据语句:" + dataQuery.toString());
//查询数据结果
List<FluxTable> dataResults = client.getQueryApi().query(dataQuery.toString());
for (FluxTable fluxTable : dataResults) {
List<FluxRecord> records = fluxTable.getRecords();
for (FluxRecord fluxRecord : records) {
String value = (String) fluxRecord.getValueByKey("_value");
DeviceLogData data = JSONObject.parseObject(value, DeviceLogData.class);
//构造数据
DeviceLogData newData = new DeviceLogData();
BeanUtils.copyProperties(data, newData);
newData.setId(deviceLogData.getId());
newData.setFeedbackValue(deviceLogData.getFeedbackValue());
newData.setStatusIssue(deviceLogData.getStatusIssue());
//步骤2删除该日志
//删除设备命令下发日志数据
deleteDeviceLog(deviceLogData.getId());
//步骤3更新该日志
//保存设备命令下发日志数据到influxdb中
saveDeviceLogToInfluxdb(newData);
try {
StringBuilder filterFragment = new StringBuilder();
filterFragment.append("from(bucket: \"devicelog\") ");
filterFragment.append("|> range(start: -360d)");
filterFragment.append(" |> filter(fn: (r) => r[\"_measurement\"] == \"devicelog\")");
filterFragment.append(" |> filter(fn: (r) => r[\"_field\"] == \"logjson\")");
filterFragment.append(String.format(" |> filter(fn: (r) => r[\"id\"] == \"%s\")", deviceLogData.getId()));
StringBuilder dataQuery = new StringBuilder();
dataQuery.append(filterFragment);
dataQuery.append(" |> sort(columns:[\"_time\"], desc:true)");
dataQuery.append(" |> yield(name: \"data\")");
System.out.println("查询数据语句:" + dataQuery.toString());
List<FluxTable> dataResults = client.getQueryApi().query(dataQuery.toString());
for (FluxTable fluxTable : dataResults) {
List<FluxRecord> records = fluxTable.getRecords();
for (FluxRecord fluxRecord : records) {
String value = (String) fluxRecord.getValueByKey("_value");
DeviceLogData data = JSONObject.parseObject(value, DeviceLogData.class);
DeviceLogData newData = new DeviceLogData();
BeanUtils.copyProperties(data, newData);
newData.setId(deviceLogData.getId());
newData.setFeedbackValue(deviceLogData.getFeedbackValue());
newData.setStatusIssue(deviceLogData.getStatusIssue());
deleteDeviceLog(deviceLogData.getId());
saveDeviceLogToInfluxdb(newData);
}
}
} finally {
client.close();
}
}
/**
* 检查异常是否为 InfluxDB 超时连接失败或 shard 损坏异常
*/
private boolean isInfluxDBTimeoutException(Exception e) {
String message = e.getMessage();
if (message != null && (message.contains("not attempting to open shard") ||
message.contains("short buffer") ||
message.contains("InternalServerErrorException"))) {
return true;
}
if (message != null && (message.contains("Failed to connect") ||
message.contains("ConnectException") ||
message.contains("Connection refused") ||
message.contains("Connection timed out") ||
message.contains("connect timed out"))) {
return true;
}
Throwable cause = e.getCause();
Set<Throwable> visitedCauses = new HashSet<>();
while (cause != null && visitedCauses.add(cause)) {
if (cause instanceof java.net.SocketTimeoutException ||
cause instanceof java.net.ConnectException) {
return true;
}
if (cause.getMessage() != null &&
(cause.getMessage().contains("Read timed out") ||
cause.getMessage().contains("connect timed out") ||
cause.getMessage().contains("Connection timed out"))) {
return true;
} }
cause = cause.getCause();
} }
client.close();
return false;
} }
} }

+ 45
- 3
src/main/resources/application.properties View File

@ -8,12 +8,41 @@ spring.rabbitmq.username=topsail
spring.rabbitmq.password=topsail spring.rabbitmq.password=topsail
spring.rabbitmq.virtualHost=/ spring.rabbitmq.virtualHost=/
spring.rabbitmq.listener.simple.acknowledge-mode=manual spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.prefetch=10
spring.rabbitmq.listener.simple.prefetch=100
spring.rabbitmq.listener.simple.concurrency=5
spring.rabbitmq.listener.simple.max-concurrency=10
spring.datasource.url=jdbc:mysql://rm-2ze77qng1ddlfur9g4o.mysql.rds.aliyuncs.com:3306/zhrl?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull
# RabbitMQ Management API 配置(用于获取队列列表)
rabbitmq.management.url=http://182.92.218.150:15672
rabbitmq.management.username=topsail
rabbitmq.management.password=topsail
rabbitmq.management.vhost=/
spring.datasource.url=jdbc:mysql://rm-2ze77qng1ddlfur9g4o.mysql.rds.aliyuncs.com:3306/zhrl?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&failOverReadOnly=false&maxReconnects=3&initialTimeout=2&connectTimeout=60000&socketTimeout=60000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=topsail spring.datasource.username=topsail
spring.datasource.password=Topsail2020 spring.datasource.password=Topsail2020
# HikariCP 连接池优化配置
# 最小空闲连接数
spring.datasource.hikari.minimum-idle=5
# 最大连接池大小(根据并发需求调整)
spring.datasource.hikari.maximum-pool-size=20
# 连接超时时间(毫秒)
spring.datasource.hikari.connection-timeout=30000
# 连接最大生命周期(毫秒),设置为 2 分钟,必须小于 MySQL 的 wait_timeout
spring.datasource.hikari.max-lifetime=120000
# 空闲连接超时时间(毫秒),设置为 1 分钟
spring.datasource.hikari.idle-timeout=60000
# 连接测试查询
spring.datasource.hikari.connection-test-query=SELECT 1
# 自动提交
spring.datasource.hikari.auto-commit=true
# 泄漏检测阈值(毫秒),超过此时间的连接会被记录警告
spring.datasource.hikari.leak-detection-threshold=60000
# 保持连接活跃,每 30 秒发送一次心跳
spring.datasource.hikari.keepalive-time=30000
##正式环境influxdb配置 ##正式环境influxdb配置
shengdilan.influxdb.token=0rg4n4KBC6x65pljf-OzaqvXrRCJGKQxl_ZGSSijdTRKNuVgbeTDMf5UKIHZPYHKjCHVrnKKNOu9hVVVNUCaZw== shengdilan.influxdb.token=0rg4n4KBC6x65pljf-OzaqvXrRCJGKQxl_ZGSSijdTRKNuVgbeTDMf5UKIHZPYHKjCHVrnKKNOu9hVVVNUCaZw==
shengdilan.influxdb.url=http://113.137.28.150:8086 shengdilan.influxdb.url=http://113.137.28.150:8086
@ -30,4 +59,17 @@ shengdilan.influxdb.oldorg=topsail
##旧influxdb配置参数 ##旧influxdb配置参数
#shengdilan.influxdb.oldtoken=C2sfXsMC475aTtin7HbRkUXa9tEZTUU0S928ZdPzFktcFW8gZD_zY8-hKhgPxkLLodVS4YcsL3RcwgsJWYlURw== #shengdilan.influxdb.oldtoken=C2sfXsMC475aTtin7HbRkUXa9tEZTUU0S928ZdPzFktcFW8gZD_zY8-hKhgPxkLLodVS4YcsL3RcwgsJWYlURw==
#shengdilan.influxdb.oldurl=http://192.168.139.128:8086 #shengdilan.influxdb.oldurl=http://192.168.139.128:8086
#shengdilan.influxdb.oldorg=shengdilan
#shengdilan.influxdb.oldorg=shengdilan
# ==================== MySQL SQL 打印配置 ====================
# 方式1:MyBatis 日志打印(推荐)
# 打印所有 Mapper 接口的 SQL 语句
logging.level.com.topsail.influxdb.mapper=DEBUG
# 方式2:Spring JDBC 日志打印(可选)
# 打印数据源相关的 SQL
# logging.level.org.springframework.jdbc.core=DEBUG
# logging.level.org.springframework.jdbc.datasource=DEBUG
# 方式3:HikariCP 连接池日志(可选)
# logging.level.com.zaxxer.hikari=DEBUG

Loading…
Cancel
Save