diff --git a/pom.xml b/pom.xml index 8903da4..662c59e 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ com.alibaba fastjson - 1.2.15 + 1.2.83 com.google.code.gson @@ -134,6 +134,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + diff --git a/src/main/java/com/topsail/influxdb/config/InfluxDBConfig.java b/src/main/java/com/topsail/influxdb/config/InfluxDBConfig.java new file mode 100644 index 0000000..04e4c76 --- /dev/null +++ b/src/main/java/com/topsail/influxdb/config/InfluxDBConfig.java @@ -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 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()); + } + } + } +} diff --git a/src/main/java/com/topsail/influxdb/rabbitmq/AmqpListener.java b/src/main/java/com/topsail/influxdb/rabbitmq/AmqpListener.java index 2c1b656..1e59992 100644 --- a/src/main/java/com/topsail/influxdb/rabbitmq/AmqpListener.java +++ b/src/main/java/com/topsail/influxdb/rabbitmq/AmqpListener.java @@ -1,6 +1,7 @@ package com.topsail.influxdb.rabbitmq; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONException; import com.rabbitmq.client.Channel; import com.topsail.influxdb.entity.DeviceLogData; 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.util.StringUtils; +import java.util.HashSet; +import java.util.Set; + @Component public class AmqpListener { public static final Logger LOG = LoggerFactory.getLogger(AmqpListener.class); @@ -36,24 +40,112 @@ public class AmqpListener { * @param channel * @throws Exception */ - - @RabbitListener(queues = "shengdilandevicedataall") +// @RabbitListener(queues = "shengdilandevicedataall") public void deviceDataMqListener(@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); - 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); } 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 * @throws Exception */ - - @RabbitListener(queues = "shengdilandevicelogall") +// @RabbitListener(queues = "shengdilandevicelogall") public void deviceLogMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception { if (StringUtils.isEmpty(message)) { channel.basicAck(deliveryTag, false); return; } + try { DeviceLogData deviceLogData = JSON.parseObject(message, DeviceLogData.class); 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); } 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 * @throws Exception */ - @RabbitListener(queues = "shengdilandevicelogupdate") public void updateDeviceLogMqListener(@Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag, Channel channel) throws Exception { if (StringUtils.isEmpty(message)) { channel.basicAck(deliveryTag, false); return; } + try { DeviceLogData deviceLogData = JSON.parseObject(message, DeviceLogData.class); 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); } 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 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; } } diff --git a/src/main/java/com/topsail/influxdb/rabbitmq/service/AmqpService.java b/src/main/java/com/topsail/influxdb/rabbitmq/service/AmqpService.java index a076c0e..8553e35 100644 --- a/src/main/java/com/topsail/influxdb/rabbitmq/service/AmqpService.java +++ b/src/main/java/com/topsail/influxdb/rabbitmq/service/AmqpService.java @@ -1,46 +1,255 @@ 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.LoggerFactory; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.AmqpTemplate; 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.Value; 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 public class AmqpService { public static final Logger LOG = LoggerFactory.getLogger(AmqpService.class); private final AmqpAdmin amqpAdmin; 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 - public AmqpService(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { + public AmqpService(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate, ConnectionFactory connectionFactory) { this.amqpAdmin = amqpAdmin; 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); - if(msg!=null){ - this.amqpTemplate.send(queue,msg); + if (msg != null) { + this.amqpTemplate.send(queue, msg); return false; - }else{ + } else { return true; } - }catch (Exception e){ + } catch (Exception e) { 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 getAllQueueNames() { + List 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; + } } diff --git a/src/main/java/com/topsail/influxdb/service/DeviceDataService.java b/src/main/java/com/topsail/influxdb/service/DeviceDataService.java index 012b7a1..8486ff2 100644 --- a/src/main/java/com/topsail/influxdb/service/DeviceDataService.java +++ b/src/main/java/com/topsail/influxdb/service/DeviceDataService.java @@ -5,10 +5,10 @@ import com.alibaba.fastjson.JSONObject; import com.influxdb.client.DeleteApi; import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClientFactory; -import com.influxdb.client.WriteApi; import com.influxdb.client.domain.WritePrecision; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; +import com.topsail.influxdb.config.InfluxDBConfig; import com.topsail.influxdb.entity.*; import com.topsail.influxdb.mapper.DeviceInfoMapper; import com.topsail.influxdb.pojo.History; @@ -33,7 +33,8 @@ import java.util.*; @Service public class DeviceDataService { 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 dateFormatLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // InfluxDB基础配置 private static final String DEVICEDATA_BUCKET_NAME = "iot"; @Value("${shengdilan.influxdb.token}") @@ -54,24 +55,54 @@ public class DeviceDataService { DeviceInfoMapper deviceInfoMapper; @Autowired AmqpService amqpService; - private InfluxDBClient influxDBClient; + @Autowired + InfluxDBClient influxDBClient; + @Autowired + InfluxDBConfig influxDBConfig; + + // 旧InfluxDB客户端(用于数据迁移) private InfluxDBClient oldInfluxDBClient; + /** 备份队列名称 */ + private static final String BACKUP_QUEUE_NAME = "shengdilandevicedataback"; + @PostConstruct public void init() { - this.influxDBClient = InfluxDBClientFactory.create(url, token.toCharArray(), org); this.oldInfluxDBClient = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg); + // 注册连接恢复回调:当InfluxDB恢复时,报告备份队列积压情况 + influxDBConfig.addRecoveryCallback(this::onConnectionRecovered); + LOG.info("DeviceDataService初始化完成,已注册InfluxDB连接恢复回调"); } @PreDestroy public void destroy() { - if (influxDBClient != null) { - influxDBClient.close(); - } 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 getDeviceHistoryData(String uid, Integer pageNo, Integer pageSize, String startTime, String endTime, String imei) { 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 tables = client.getQueryApi().query(query.toString()); -// List returnList = new ArrayList<>(); - List resultSet = new ArrayList<>(); - for (FluxTable fluxTable : tables) { - List 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 tables = client.getQueryApi().query(query.toString()); + List resultSet = new ArrayList<>(); + for (FluxTable fluxTable : tables) { + List 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 getOldInfluxdbData(String imei) { DeviceBelongInfo deviceBelongInfo = deviceInfoMapper.queryDeviceBelongInfo(imei); -// InfluxDBClient client = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg); StringBuffer query = new StringBuffer(); query.append("from(bucket: \"iot\") "); 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)); -// 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); - if(oldInfluxDBClient==null){ + if (oldInfluxDBClient == null) { oldInfluxDBClient = InfluxDBClientFactory.create(oldurl, oldtoken.toCharArray(), oldorg); } List tables = oldInfluxDBClient.getQueryApi().query(query.toString()); @@ -204,10 +232,25 @@ public class DeviceDataService { List records = fluxTable.getRecords(); for (FluxRecord fluxRecord : records) { 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) { Date createtime = history.getSenddate(); - //将时间转换成Instant Instant time = null; if (createtime != null) { time = createtime.toInstant(); @@ -221,7 +264,6 @@ public class DeviceDataService { } } } -// client.close(); Collections.reverse(rerurnList); return rerurnList; } @@ -232,14 +274,21 @@ public class DeviceDataService { * @param history */ public void saveDeviceDataToInfluxdb(History history) { + // 先进行null检查,避免空指针异常 + if (history == null || (StringUtils.isEmpty(history.getImei()))) { + return; + } if (history.getSenddate() == null) { 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; } //查询设备的所属信息 - if (history != null && (history.getDeviceBelongInfo() == null || history.getHouseId() == null)) { + if (history.getDeviceBelongInfo() == null || history.getHouseId() == null) { DeviceBelongInfo deviceBelongInfo = getDeviceBelongInfo(history.getImei().trim()); if (deviceBelongInfo == null) { return; @@ -248,21 +297,72 @@ public class DeviceDataService { history.setHouseId(deviceBelongInfo.getHouseId()); } Date createtime = history.getSenddate(); - //将时间转换成Instant Instant time = null; if (createtime != null) { time = createtime.toInstant(); } - // 复用已创建的客户端实例 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.value4 = 0; 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) { - LOG.info(e.getMessage()); + LOG.warn("解析设备数据值失败,imei: {}, value: {}, 错误: {}", history.getImei(), value, e.getMessage()); } } - //从内部移动处出来,value有没有数值都进行插入 deviceDataInfluxData.battery = history.getBatterylevel(); deviceDataInfluxData.sigal = history.getSingalstrength(); deviceDataInfluxData.jsondata = JSON.toJSONString(history); @@ -308,6 +407,32 @@ public class DeviceDataService { 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() { 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 { - // 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); System.out.println("Data deleted successfully"); } catch (Exception e) { @@ -345,45 +464,69 @@ public class DeviceDataService { * @param imei */ public void transferDeviceData(String imei) { - //1.查询所有的设备编号 List syncDataFlags = deviceInfoMapper.querySyncDeviceDataFlagInfo(imei); - //2.根据设备编号查询历史Influxdb所有的设备数据 Boolean flag = true; if (syncDataFlags != null && syncDataFlags.size() > 0) { for (int i = 0; i < syncDataFlags.size(); i++) { if (!flag) { syncDataFlags = deviceInfoMapper.querySyncDeviceDataFlagInfo(null); + if (syncDataFlags == null || syncDataFlags.isEmpty()) { + LOG.warn("重新查询同步标志列表为空,终止同步"); + break; + } } int index = new Random().nextInt(syncDataFlags.size()); SyncDataFlag syncDataFlag = syncDataFlags.get(index); Boolean syncDeviceData = false; + Long dataCount = 0L; List influxdbDataList = getOldInfluxdbData(syncDataFlag.getImei().toLowerCase(Locale.ROOT)); if (influxdbDataList != null && influxdbDataList.size() > 0) { 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) { - //4.更新同步设备数据状态 deviceInfoMapper.updateSyncDeviceDataFlagInfo(syncDataFlag.getId(), 1); } flag = false; @@ -396,35 +539,126 @@ public class DeviceDataService { */ public Long getDeviceDataCount(String imei) { 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 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 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 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; } } diff --git a/src/main/java/com/topsail/influxdb/service/DeviceLogService.java b/src/main/java/com/topsail/influxdb/service/DeviceLogService.java index 6cc4b9e..564f7d1 100644 --- a/src/main/java/com/topsail/influxdb/service/DeviceLogService.java +++ b/src/main/java/com/topsail/influxdb/service/DeviceLogService.java @@ -5,10 +5,10 @@ import com.alibaba.fastjson.JSONObject; import com.influxdb.client.DeleteApi; import com.influxdb.client.InfluxDBClient; import com.influxdb.client.InfluxDBClientFactory; -import com.influxdb.client.WriteApi; import com.influxdb.client.domain.WritePrecision; import com.influxdb.query.FluxRecord; import com.influxdb.query.FluxTable; +import com.topsail.influxdb.config.InfluxDBConfig; import com.topsail.influxdb.entity.*; import com.topsail.influxdb.mapper.DeviceInfoMapper; import com.topsail.influxdb.rabbitmq.service.AmqpService; @@ -34,7 +34,7 @@ public class DeviceLogService { public static final Logger LOG = LoggerFactory.getLogger(DeviceLogService.class); private static volatile Map supplierInfoMap = new HashMap<>(); - // 初始化时加载圣地蓝项目信息(例如,在构造函数或@PostConstruct方法中) + // 初始化时加载圣地蓝项目信息 @PostConstruct public void initShendianlanProjects() { if (supplierInfoMap.isEmpty()) { @@ -42,7 +42,6 @@ public class DeviceLogService { if (supplierInfoMap.isEmpty()) { List supplierList = deviceInfoMapper.getSupplierList(); if (supplierList != null && !supplierList.isEmpty()) { - //查询出属于圣地蓝项目的IMEI号 for (SupplierVO supplierVO : supplierList) { Integer id = supplierVO.getId(); String name = supplierVO.getSupplierName(); @@ -70,6 +69,8 @@ public class DeviceLogService { DeviceInfoMapper deviceInfoMapper; @Autowired AmqpService amqpService; + @Autowired + InfluxDBConfig influxDBConfig; /** * 存储设备数据到influxdb @@ -84,13 +85,18 @@ public class DeviceLogService { if (createtime == null) { createtime = new Date(); } - //将时间转换成Instant 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.id = deviceLogData.getId(); deviceLogInfluxData.imei = deviceLogData.getImei(); - //从内部移动处出来,value有没有数值都进行插入 deviceLogInfluxData.result = deviceLogData.getResult(); deviceLogInfluxData.statusIssue = deviceLogData.getStatusIssue(); deviceLogInfluxData.supplierId = deviceLogData.getSupplierId(); @@ -103,13 +109,47 @@ public class DeviceLogService { deviceLogInfluxData.deviceBelongInfo = deviceLogData.getDeviceBelongInfo(); deviceLogInfluxData.houseId = deviceLogData.getHouseId(); 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.setTime(new Date()); InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org); - // 1. 拼接公共过滤条件片段(总条数和分页查询共用) StringBuilder predicates = new StringBuilder(); - // 2. 拼接固定过滤条件(_measurement和_field,必选) predicates.append("_measurement=devicelog"); if (deviceLogId != null) { if (predicates.length() > 0) { @@ -131,7 +169,6 @@ public class DeviceLogService { System.out.println(predicates.toString()); DeleteApi deleteApi = client.getDeleteApi(); try { -// // 删除指定时间范围内所有数据 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); 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) { JSONObject resultSet = new JSONObject(); InfluxDBClient client = InfluxDBClientFactory.create(url, token.toCharArray(), org); + try { // 1. 拼接公共过滤条件片段(总条数和分页查询共用) StringBuilder filterFragment = new StringBuilder(); 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 newFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); newFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//时区转换 @@ -185,63 +220,52 @@ public class DeviceLogService { // 2. 拼接固定过滤条件(_measurement和_field,必选) filterFragment.append(" |> filter(fn: (r) => r[\"_measurement\"] == \"devicelog\")"); filterFragment.append(" |> filter(fn: (r) => r[\"_field\"] == \"logjson\")"); - // 3. 动态拼接imei过滤条件(仅当imei非空且非空白字符串时拼接) + // 3. 动态拼接imei过滤条件 if (imei != null && !imei.trim().isEmpty()) { filterFragment.append(String.format(" |> filter(fn: (r) => r[\"imei\"] == \"%s\")", imei.trim())); } - // 4. 动态拼接result过滤条件(仅当result非空且非空白字符串时拼接) + // 4. 动态拼接result过滤条件 if (result != null && !result.trim().isEmpty()) { filterFragment.append(String.format(" |> filter(fn: (r) => r[\"result\"] == \"%s\")", result.trim())); } if (statusIssue != null && !statusIssue.trim().isEmpty()) { filterFragment.append(String.format(" |> filter(fn: (r) => r[\"statusIssue\"] == \"%s\")", statusIssue.trim())); } - // 5. 动态拼接supplierId过滤条件(仅当supplierId非空且非空白字符串时拼接) + // 5. 动态拼接supplierId过滤条件 if (supplierId != null && !supplierId.trim().isEmpty()) { filterFragment.append(String.format(" |> filter(fn: (r) => r[\"supplierId\"] == \"%s\")", supplierId.trim())); } - // 6. 动态拼接companyId过滤条件(仅当companyId非空且非空白字符串时拼接) + // 6. 动态拼接companyId过滤条件 if (companyId != null && !companyId.trim().isEmpty()) { filterFragment.append(String.format(" |> filter(fn: (r) => r[\"companyId\"] == \"%s\")", companyId.trim())); } - // 7. 动态拼接operator过滤条件(仅当operator非空且非空白字符串时拼接) + // 7. 动态拼接operator过滤条件 if (operator != null && !operator.trim().isEmpty()) { filterFragment.append(String.format(" |> filter(fn: (r) => r[\"operator\"] == \"%s\")", operator.trim())); } - // 8. 动态拼接houseId过滤条件(仅当houseId非空且非空白字符串时拼接) + // 8. 动态拼接houseId过滤条件 if (houseId != null) { filterFragment.append(String.format(" |> filter(fn: (r) => r[\"houseId\"] == \"%s\")", houseId)); } - // ========== 核心修复:Java 8不支持""",改用+拼接多行字符串 ========== - // 1. 拼接模糊过滤条件(Flux逻辑不变,仅改字符串写法) - // 3. 核心修复:belonginfo模糊查询(两步修复) + // 9. 拼接模糊过滤条件 if (bindingInfo != null && !bindingInfo.trim().isEmpty()) { 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(); - 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(); - dataQuery.append(filterFragment); // 复用过滤条件 + dataQuery.append(filterFragment); dataQuery.append(" |> sort(columns:[\"_time\"], desc:true)"); // 处理分页参数默认值 int finalPageNum = pageNode != null ? pageNode : DEFAULT_PAGE_NUM; int finalPageSize = pageSize != null ? pageSize : DEFAULT_PAGE_SIZE; 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("查询数据语句:" + dataQuery.toString()); //查询总条数 @@ -269,7 +293,6 @@ public class DeviceLogService { rerurnList.add(data); } } - client.close(); Collections.reverse(rerurnList); resultSet.put("count", totalCount); //对结果按照分页要求截取构造数据 @@ -278,17 +301,18 @@ public class DeviceLogService { } resultSet.put("list", rerurnList); return resultSet; + } finally { + client.close(); + } } /** * Flux正则转义(仅转义RE2引擎的特殊字符,避免\Q\E) - * Flux正则特殊字符:. * + ? | ( ) [ ] { } ^ $ \ */ private static String escapeFluxRegex(String keyword) { if (keyword == null || keyword.isEmpty()) { return ""; } - // 转义Flux正则的特殊字符(替换为\+字符) String[] specialChars = {"\\", ".", "*", "+", "?", "|", "(", ")", "[", "]", "{", "}", "^", "$"}; String escaped = keyword; for (String ch : specialChars) { @@ -301,17 +325,14 @@ public class DeviceLogService { * 转存设备命令下发日志数据 */ public void transferDeviceLogData(String searchImei, Integer companyId) { - //1. 查询所有的设备编号 List syncDataFlags = deviceInfoMapper.querySyncDeviceLogFlagInfo(searchImei); if (syncDataFlags != null && syncDataFlags.size() > 0) { - //2. 根据设备编号查询设备命令下发日志数据 for (SyncDataFlag syncDataFlag : syncDataFlags) { Boolean flag = false; String imei = syncDataFlag.getImei(); List deviceLogDataList = deviceInfoMapper.queryDeviceLogData(imei, companyId); if (deviceLogDataList != null && deviceLogDataList.size() > 0) { - DeviceBelongInfo deviceBelongInfo = deviceInfoMapper.queryDeviceBelongInfo(imei); - //3. 批量插入设备命令下发日志数据到influxdb中 + DeviceBelongInfo deviceBelongInfo = deviceInfoMapper.queryDeviceBelongInfo(imei); for (DeviceLogData deviceLogData : deviceLogDataList) { deviceLogData.setDeviceBelongInfo(deviceBelongInfo != null ? deviceBelongInfo.getDeviceBelongInfo() : null); deviceLogData.setHouseId(deviceBelongInfo != null ? deviceBelongInfo.getHouseId() : null); @@ -329,7 +350,6 @@ public class DeviceLogService { } } if (flag) { - //4. 更新同步设备命令下发日志数据状态 deviceInfoMapper.updateSyncDeviceLogFlagInfo(syncDataFlag.getId(), 1); } } @@ -340,44 +360,71 @@ public class DeviceLogService { * 更新设备命令下发日志数据 */ public void updateDeviceLogMqListener(DeviceLogData deviceLogData) { - //步骤1:查出ID对应的日志 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 dataResults = client.getQueryApi().query(dataQuery.toString()); - for (FluxTable fluxTable : dataResults) { - List 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 dataResults = client.getQueryApi().query(dataQuery.toString()); + for (FluxTable fluxTable : dataResults) { + List 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 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; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7ef22a9..4aef16d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,12 +8,41 @@ spring.rabbitmq.username=topsail spring.rabbitmq.password=topsail spring.rabbitmq.virtualHost=/ 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.username=topsail 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配置 shengdilan.influxdb.token=0rg4n4KBC6x65pljf-OzaqvXrRCJGKQxl_ZGSSijdTRKNuVgbeTDMf5UKIHZPYHKjCHVrnKKNOu9hVVVNUCaZw== shengdilan.influxdb.url=http://113.137.28.150:8086 @@ -30,4 +59,17 @@ shengdilan.influxdb.oldorg=topsail ##旧influxdb配置参数 #shengdilan.influxdb.oldtoken=C2sfXsMC475aTtin7HbRkUXa9tEZTUU0S928ZdPzFktcFW8gZD_zY8-hKhgPxkLLodVS4YcsL3RcwgsJWYlURw== #shengdilan.influxdb.oldurl=http://192.168.139.128:8086 -#shengdilan.influxdb.oldorg=shengdilan \ No newline at end of file +#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