|
|
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Component;
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
import java.util.Date;
|
|
|
+import java.util.Arrays;
|
|
|
import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
@@ -29,13 +30,20 @@ public class DeviceOfflineCheckTask {
|
|
|
|
|
|
/** 已知离线设备集合,用于避免重复API调用 */
|
|
|
private final Set<String> offlineDeviceSet = ConcurrentHashMap.newKeySet();
|
|
|
+ /** 本实例已成功同步在线的设备;不能把尚未检测过离线当作已经在线。 */
|
|
|
+ private final Set<String> onlineDeviceSet = ConcurrentHashMap.newKeySet();
|
|
|
+ private final Object[] deviceLocks = new Object[128];
|
|
|
+
|
|
|
+ public DeviceOfflineCheckTask() {
|
|
|
+ Arrays.setAll(deviceLocks, i -> new Object());
|
|
|
+ }
|
|
|
|
|
|
@Autowired
|
|
|
private RestTemplate restTemplate;
|
|
|
|
|
|
/** 离线判定超时,单位:分钟,默认30分钟 */
|
|
|
@Value("${device.offline.timeout-minutes:30}")
|
|
|
- private int offlineTimeoutMinutes;
|
|
|
+ private int offlineTimeoutMinutes = 30;
|
|
|
|
|
|
/** 检查间隔,单位:毫秒,默认5分钟 */
|
|
|
@Scheduled(fixedRateString = "${device.offline.check-interval-ms:300000}")
|
|
|
@@ -43,14 +51,16 @@ public class DeviceOfflineCheckTask {
|
|
|
Date now = new Date();
|
|
|
long timeoutMs = (long) offlineTimeoutMinutes * 60 * 1000;
|
|
|
for (Map.Entry<String, Date> entry : deviceLastReceiveTimeMap.entrySet()) {
|
|
|
- long diff = now.getTime() - entry.getValue().getTime();
|
|
|
- if (diff > timeoutMs) {
|
|
|
- // 只在设备不在离线集合中时才调用API
|
|
|
+ synchronized (deviceLock(entry.getKey())) {
|
|
|
+ // 与在线更新串行,重新读取时间,避免用旧快照覆盖刚收到的上报。
|
|
|
+ Date latest = deviceLastReceiveTimeMap.get(entry.getKey());
|
|
|
+ if (latest == null || now.getTime() - latest.getTime() <= timeoutMs) continue;
|
|
|
if (offlineDeviceSet.add(entry.getKey())) {
|
|
|
if (!updateDeviceOnlineStatus(entry.getKey(), 0)) {
|
|
|
offlineDeviceSet.remove(entry.getKey());
|
|
|
continue;
|
|
|
}
|
|
|
+ onlineDeviceSet.remove(entry.getKey());
|
|
|
logger.info("设备 {} 已离线(超过{}分钟未收到数据)", entry.getKey(), offlineTimeoutMinutes);
|
|
|
}
|
|
|
}
|
|
|
@@ -59,16 +69,24 @@ public class DeviceOfflineCheckTask {
|
|
|
|
|
|
/**
|
|
|
* 标记设备在线(收到数据时调用)
|
|
|
- * 只在设备当前处于离线状态时才调用API
|
|
|
+ * 首次上报、服务重启或离线恢复均需同步;失败保留下次上报的重试机会。
|
|
|
*/
|
|
|
public void markDeviceOnline(String deviceCode) {
|
|
|
- if (deviceCode != null && offlineDeviceSet.contains(deviceCode)
|
|
|
- && updateDeviceOnlineStatus(deviceCode, 1)) {
|
|
|
- offlineDeviceSet.remove(deviceCode);
|
|
|
- logger.info("设备 {} 恢复在线", deviceCode);
|
|
|
+ if (deviceCode == null || deviceCode.trim().isEmpty()) return;
|
|
|
+ synchronized (deviceLock(deviceCode)) {
|
|
|
+ // 仅在接口明确成功后缓存;新建默认离线的设备首次上报也会进入这里。
|
|
|
+ if (!onlineDeviceSet.contains(deviceCode) && updateDeviceOnlineStatus(deviceCode, 1)) {
|
|
|
+ onlineDeviceSet.add(deviceCode);
|
|
|
+ offlineDeviceSet.remove(deviceCode);
|
|
|
+ logger.info("设备 {} 已在线", deviceCode);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private Object deviceLock(String deviceCode) {
|
|
|
+ return deviceLocks[(deviceCode.hashCode() & Integer.MAX_VALUE) % deviceLocks.length];
|
|
|
+ }
|
|
|
+
|
|
|
private boolean updateDeviceOnlineStatus(String deviceCode, int onlineStatus) {
|
|
|
try {
|
|
|
Map<String, Object> params = Map.of("deviceCode", deviceCode, "onlineStatus", onlineStatus);
|
|
|
@@ -86,4 +104,4 @@ public class DeviceOfflineCheckTask {
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+}
|