feat: 优化设备状态更新逻辑,批量预加载任务状态以避免N+1查询问题;在SystemConfigService中添加配置缓存机制,提升配置读取性能;调整GameCompletionDetectionService中的事务超时设置,确保快速完成任务标记

This commit is contained in:
zyh
2025-10-06 15:13:21 +08:00
parent a7e02936ee
commit f339f16ded
4 changed files with 174 additions and 46 deletions

View File

@@ -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;

View File

@@ -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;
}
// 获取链接相关的默认配置

View File

@@ -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;
// 将设备加入冷却队列
machineCooldownService.addMachineToCooldown(machineId,
"游戏完成 - " + detectionSource, task.getId());
// 异步记录历史,避免阻塞主事务
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);
}
}

View File

@@ -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
# 连接池健康监控和自动重连