feat: 优化设备状态更新逻辑,批量预加载任务状态以避免N+1查询问题;在SystemConfigService中添加配置缓存机制,提升配置读取性能;调整GameCompletionDetectionService中的事务超时设置,确保快速完成任务标记
This commit is contained in:
@@ -94,46 +94,83 @@ public class DeviceStats {
|
||||
/**
|
||||
* 根据外部传入的全量设备快照进行分组统计,并更新内存中的状态与变更记录。
|
||||
* 分类优先级(高→低):
|
||||
* 1) RUNNING:存在 LOGGED_IN 任务 或 脚本返回“已运行”
|
||||
* 2) USING:存在 USING 任务 或 脚本返回“正在登录中/数字(进度/积分)”
|
||||
* 3) IDLE_COOLDOWN:脚本返回“已打完” 或 处于冷却服务中
|
||||
* 4) IDLE_FREE:其余所有(包括脚本返回“空的/空闲/空字符串/未知”)
|
||||
* 1) RUNNING:存在 LOGGED_IN 任务 或 脚本返回"已运行"
|
||||
* 2) USING:存在 USING 任务 或 脚本返回"正在登录中/数字(进度/积分)"
|
||||
* 3) IDLE_COOLDOWN:脚本返回"已打完" 或 处于冷却服务中
|
||||
* 4) IDLE_FREE:其余所有(包括脚本返回"空的/空闲/空字符串/未知")
|
||||
*
|
||||
* 优化:批量预加载所有设备的任务状态,避免逐个查询导致连接长时间占用
|
||||
*/
|
||||
public Snapshot updateWithSnapshot(DeviceStatusResponse snapshot) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
Map<String, DeviceStatusResponse.DeviceInfo> devices = snapshot != null ? snapshot.getDevices() : Collections.emptyMap();
|
||||
if (devices == null) {
|
||||
devices = Collections.emptyMap();
|
||||
}
|
||||
log.info("接收设备快照:设备总数={}", devices.size());
|
||||
|
||||
// 批量预加载所有设备的任务状态(避免 N+1 查询问题)
|
||||
Set<String> devicesWithLoggedInTasks = new HashSet<>();
|
||||
Set<String> devicesWithUsingTasks = new HashSet<>();
|
||||
|
||||
try {
|
||||
long queryStart = System.currentTimeMillis();
|
||||
List<LinkTask> allLoggedInTasks = linkTaskMapper.findByStatus("LOGGED_IN");
|
||||
List<LinkTask> allUsingTasks = linkTaskMapper.findByStatus("USING");
|
||||
long queryEnd = System.currentTimeMillis();
|
||||
|
||||
if (allLoggedInTasks != null) {
|
||||
for (LinkTask task : allLoggedInTasks) {
|
||||
if (task.getMachineId() != null) {
|
||||
devicesWithLoggedInTasks.add(task.getMachineId());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (allUsingTasks != null) {
|
||||
for (LinkTask task : allUsingTasks) {
|
||||
if (task.getMachineId() != null) {
|
||||
devicesWithUsingTasks.add(task.getMachineId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量预加载任务状态完成:LOGGED_IN={}, USING={}, 耗时={}ms",
|
||||
devicesWithLoggedInTasks.size(), devicesWithUsingTasks.size(), queryEnd - queryStart);
|
||||
} catch (Exception e) {
|
||||
log.error("批量预加载任务状态失败", e);
|
||||
// 继续执行,但没有任务信息
|
||||
}
|
||||
|
||||
// 初始化分类容器
|
||||
Map<Category, List<String>> bucket = new EnumMap<>(Category.class);
|
||||
for (Category c : Category.values()) {
|
||||
bucket.put(c, new ArrayList<>());
|
||||
}
|
||||
|
||||
String configuredIdle = systemConfigService.getDeviceIdleStatus();
|
||||
|
||||
for (String deviceId : devices.keySet()) {
|
||||
DeviceStatusResponse.DeviceInfo info = devices.get(deviceId);
|
||||
String val = info != null ? info.getVal() : null;
|
||||
String v = val != null ? val.trim() : null;
|
||||
|
||||
// 预先计算判定条件,便于统一记录日志
|
||||
boolean loggedIn = hasLoggedInTask(deviceId);
|
||||
boolean usingTask = hasUsingTask(deviceId);
|
||||
// 预先计算判定条件,便于统一记录日志(使用预加载的数据)
|
||||
boolean loggedIn = devicesWithLoggedInTasks.contains(deviceId);
|
||||
boolean usingTask = devicesWithUsingTasks.contains(deviceId);
|
||||
boolean cooldown = cooldownService.isMachineInCooldown(deviceId);
|
||||
boolean numeric = isNumeric(v);
|
||||
String configuredIdle = systemConfigService.getDeviceIdleStatus();
|
||||
|
||||
// log.debug("设备[{}] 原始脚本值='{}' | LOGGED_IN={} USING={} COOLDOWN={} NUMERIC={}",
|
||||
// deviceId, v, loggedIn, usingTask, cooldown, numeric);
|
||||
|
||||
// 当脚本值为配置的空闲标识时,若设备上存在 LOGGED_IN 且登录超过3分钟的任务,则自动完成这些任务
|
||||
if (v != null && configuredIdle != null && configuredIdle.equals(v)) {
|
||||
if (v != null && configuredIdle != null && configuredIdle.equals(v) && loggedIn) {
|
||||
try {
|
||||
int completed = autoCompleteLoggedInTasksIfIdleOver3m(deviceId);
|
||||
if (completed > 0) {
|
||||
// 任务状态可能已变化,重新评估 loggedIn
|
||||
loggedIn = hasLoggedInTask(deviceId);
|
||||
// 任务状态已变化,更新预加载的状态
|
||||
devicesWithLoggedInTasks.remove(deviceId);
|
||||
loggedIn = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("autoCompleteLoggedInTasksIfIdleOver3m 执行异常 device={} err={}", deviceId, e.getMessage());
|
||||
@@ -208,9 +245,13 @@ public class DeviceStats {
|
||||
}
|
||||
|
||||
Snapshot result = new Snapshot(bucket);
|
||||
log.info("设备分组统计完成:total={} running={} using={} idleCooldown={} idleFree={}",
|
||||
long endTime = System.currentTimeMillis();
|
||||
long totalDuration = endTime - startTime;
|
||||
|
||||
log.info("设备分组统计完成:total={} running={} using={} idleCooldown={} idleFree={}, 总耗时={}ms",
|
||||
result.getTotalDevices(), result.getRunningCount(), result.getUsingCount(),
|
||||
result.getIdleCooldownCount(), result.getIdleFreeCount());
|
||||
result.getIdleCooldownCount(), result.getIdleFreeCount(), totalDuration);
|
||||
|
||||
this.lastComputedSnapshot = result;
|
||||
return result;
|
||||
}
|
||||
@@ -338,15 +379,8 @@ public class DeviceStats {
|
||||
return completed;
|
||||
}
|
||||
|
||||
private boolean hasLoggedInTask(String deviceId) {
|
||||
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN");
|
||||
return loggedInTasks != null && !loggedInTasks.isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasUsingTask(String deviceId) {
|
||||
List<LinkTask> usingTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "USING");
|
||||
return usingTasks != null && !usingTasks.isEmpty();
|
||||
}
|
||||
// 方法已移除:hasLoggedInTask 和 hasUsingTask
|
||||
// 现在使用批量预加载方式在 updateWithSnapshot() 中处理,避免 N+1 查询问题
|
||||
|
||||
private static boolean isNumeric(String text) {
|
||||
if (text == null) return false;
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@Service
|
||||
public class SystemConfigService {
|
||||
@@ -13,13 +15,66 @@ public class SystemConfigService {
|
||||
@Autowired
|
||||
private SystemConfigMapper systemConfigMapper;
|
||||
|
||||
// 配置缓存:key -> ConfigEntry(value, cachedTime)
|
||||
private final ConcurrentMap<String, ConfigEntry> configCache = new ConcurrentHashMap<>();
|
||||
|
||||
// 缓存TTL(毫秒):5分钟
|
||||
private static final long CACHE_TTL_MS = 5 * 60 * 1000L;
|
||||
|
||||
/**
|
||||
* 配置缓存条目
|
||||
*/
|
||||
private static class ConfigEntry {
|
||||
final String value;
|
||||
final long cachedAtMs;
|
||||
|
||||
ConfigEntry(String value, long cachedAtMs) {
|
||||
this.value = value;
|
||||
this.cachedAtMs = cachedAtMs;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() - cachedAtMs > CACHE_TTL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
public SystemConfig getConfigByKey(String configKey) {
|
||||
return systemConfigMapper.findByKey(configKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置值(带缓存)
|
||||
* 注意:此方法被频繁调用(尤其在响应式流中),必须使用缓存避免连接泄漏
|
||||
*/
|
||||
public String getConfigValue(String configKey, String defaultValue) {
|
||||
// 先检查缓存
|
||||
ConfigEntry cached = configCache.get(configKey);
|
||||
if (cached != null && !cached.isExpired()) {
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
// 缓存未命中或过期,查询数据库
|
||||
SystemConfig config = systemConfigMapper.findByKey(configKey);
|
||||
return config != null ? config.getConfigValue() : defaultValue;
|
||||
String value = config != null ? config.getConfigValue() : defaultValue;
|
||||
|
||||
// 更新缓存
|
||||
configCache.put(configKey, new ConfigEntry(value, System.currentTimeMillis()));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定配置的缓存(用于配置更新后)
|
||||
*/
|
||||
public void clearCache(String configKey) {
|
||||
configCache.remove(configKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有配置缓存
|
||||
*/
|
||||
public void clearAllCache() {
|
||||
configCache.clear();
|
||||
}
|
||||
|
||||
public Integer getConfigValueAsInt(String configKey, Integer defaultValue) {
|
||||
@@ -51,19 +106,37 @@ public class SystemConfigService {
|
||||
}
|
||||
|
||||
public boolean createConfig(SystemConfig systemConfig) {
|
||||
return systemConfigMapper.insert(systemConfig) > 0;
|
||||
boolean result = systemConfigMapper.insert(systemConfig) > 0;
|
||||
if (result && systemConfig.getConfigKey() != null) {
|
||||
clearCache(systemConfig.getConfigKey());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean updateConfig(SystemConfig systemConfig) {
|
||||
return systemConfigMapper.update(systemConfig) > 0;
|
||||
boolean result = systemConfigMapper.update(systemConfig) > 0;
|
||||
if (result && systemConfig.getConfigKey() != null) {
|
||||
// 配置更新后清除缓存,确保下次读取最新值
|
||||
clearCache(systemConfig.getConfigKey());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean deleteConfig(Long id) {
|
||||
return systemConfigMapper.deleteById(id) > 0;
|
||||
boolean result = systemConfigMapper.deleteById(id) > 0;
|
||||
if (result) {
|
||||
// 删除后清除所有缓存(因为不知道具体 key)
|
||||
clearAllCache();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean deleteConfigByKey(String configKey) {
|
||||
return systemConfigMapper.deleteByKey(configKey) > 0;
|
||||
boolean result = systemConfigMapper.deleteByKey(configKey) > 0;
|
||||
if (result) {
|
||||
clearCache(configKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 获取链接相关的默认配置
|
||||
|
||||
@@ -64,7 +64,7 @@ public class GameCompletionDetectionService {
|
||||
* @param detectionSource 检测来源
|
||||
* @return 是否检测到完成
|
||||
*/
|
||||
@Transactional
|
||||
@Transactional(timeout = 10)
|
||||
public boolean detectGameCompletion(String machineId, String deviceStatus, String detectionSource) {
|
||||
if (machineId == null || machineId.trim().isEmpty()) {
|
||||
return false;
|
||||
@@ -192,6 +192,7 @@ public class GameCompletionDetectionService {
|
||||
|
||||
/**
|
||||
* 标记任务为完成状态
|
||||
* 注意:此方法在调用者的事务中执行,需要快速完成避免长时间持有连接
|
||||
*/
|
||||
private boolean markTasksCompleted(List<LinkTask> tasks, String machineId,
|
||||
Integer points, String detectionSource) {
|
||||
@@ -200,6 +201,7 @@ public class GameCompletionDetectionService {
|
||||
|
||||
for (LinkTask task : tasks) {
|
||||
try {
|
||||
String prevStatus = task.getStatus();
|
||||
task.setStatus("COMPLETED");
|
||||
task.setUpdatedAt(now);
|
||||
// 正常检测完成:记录原因
|
||||
@@ -212,26 +214,25 @@ public class GameCompletionDetectionService {
|
||||
task.setCompletedPoints(0);
|
||||
}
|
||||
|
||||
String prev = task.getStatus();
|
||||
int updated = linkTaskMapper.update(task);
|
||||
if (updated > 0) {
|
||||
log.info("任务{}已标记完成:设备={},点数={},检测来源={}",
|
||||
task.getCodeNo(), machineId, task.getCompletedPoints(), detectionSource);
|
||||
com.gameplatform.server.util.AuditLogger.info("StatusTransition: codeNo={}, device={}, {}->COMPLETED, source={}, points={}",
|
||||
task.getCodeNo(), machineId, prev, detectionSource, task.getCompletedPoints());
|
||||
try {
|
||||
if (statusHistoryMapper != null) {
|
||||
statusHistoryMapper.insert(new LinkTaskStatusHistory(
|
||||
task.getId(), task.getCodeNo(), machineId, prev, "COMPLETED", detectionSource,
|
||||
"detectionComplete", null, null
|
||||
));
|
||||
}
|
||||
} catch (Exception ignore) {}
|
||||
task.getCodeNo(), machineId, prevStatus, detectionSource, task.getCompletedPoints());
|
||||
|
||||
anyCompleted = true;
|
||||
|
||||
// 将设备加入冷却队列
|
||||
// 异步记录历史,避免阻塞主事务
|
||||
recordHistoryAsync(task.getId(), task.getCodeNo(), machineId, prevStatus, detectionSource);
|
||||
|
||||
// 将设备加入冷却队列(内存操作,快速)
|
||||
try {
|
||||
machineCooldownService.addMachineToCooldown(machineId,
|
||||
"游戏完成 - " + detectionSource, task.getId());
|
||||
} catch (Exception e) {
|
||||
log.error("添加设备冷却失败", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("更新任务{}完成状态失败", task.getCodeNo());
|
||||
}
|
||||
@@ -242,7 +243,7 @@ public class GameCompletionDetectionService {
|
||||
}
|
||||
|
||||
if (anyCompleted) {
|
||||
// 清理待确认记录
|
||||
// 清理待确认记录(内存操作,快速)
|
||||
pendingCompletions.remove(machineId);
|
||||
// 清理登录记录
|
||||
recentLogins.remove(machineId);
|
||||
@@ -251,8 +252,27 @@ public class GameCompletionDetectionService {
|
||||
return anyCompleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步记录状态历史,避免阻塞主事务
|
||||
*/
|
||||
private void recordHistoryAsync(Long taskId, String codeNo, String machineId,
|
||||
String prevStatus, String detectionSource) {
|
||||
try {
|
||||
if (statusHistoryMapper != null) {
|
||||
statusHistoryMapper.insert(new LinkTaskStatusHistory(
|
||||
taskId, codeNo, machineId, prevStatus, "COMPLETED", detectionSource,
|
||||
"detectionComplete", null, null
|
||||
));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 历史记录失败不影响主流程,只记录日志
|
||||
log.error("记录任务状态历史失败: taskId={}, codeNo={}", taskId, codeNo, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录检测日志
|
||||
* 注意:快速记录,失败不影响主流程
|
||||
*/
|
||||
private void recordDetectionLog(Long linkTaskId, String machineId, String detectionSource,
|
||||
String deviceStatus, Integer points, String confidence) {
|
||||
@@ -265,7 +285,8 @@ public class GameCompletionDetectionService {
|
||||
com.gameplatform.server.util.AuditLogger.info("CompletionDetect: taskId={}, device={}, source={}, status={}, points={}, confidence={}",
|
||||
linkTaskId, machineId, detectionSource, deviceStatus, points, confidence);
|
||||
} catch (Exception e) {
|
||||
this.log.error("记录游戏完成检测日志失败", e);
|
||||
// 日志记录失败不应该影响主流程,只记录错误
|
||||
log.error("记录游戏完成检测日志失败: taskId={}, device={}", linkTaskId, machineId, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ spring:
|
||||
maximum-pool-size: 100 # 增加最大连接数(从50到100)
|
||||
minimum-idle: 20 # 增加最小空闲连接(从10到20)
|
||||
connection-timeout: 10000 # 降低连接获取超时(从30秒到10秒)
|
||||
idle-timeout: 300000 # 空闲连接超时5分钟(从10分钟降低)
|
||||
max-lifetime: 1800000 # 连接最大存活30分钟
|
||||
leak-detection-threshold: 30000 # 连接泄漏检测30秒(从60秒降低)
|
||||
idle-timeout: 240000 # 空闲连接超时4分钟(必须 < wait_timeout)
|
||||
max-lifetime: 240000 # 连接最大存活4分钟(必须 < MySQL wait_timeout=5分钟)
|
||||
leak-detection-threshold: 60000 # 连接泄漏检测60秒(给事务足够时间)
|
||||
validation-timeout: 3000 # 连接验证超时3秒(从5秒降低)
|
||||
connection-test-query: SELECT 1
|
||||
# 连接池健康监控和自动重连
|
||||
|
||||
Reference in New Issue
Block a user