feat: 解决设备分配并发竞争问题,优化冷却机制和设备分配服务

This commit is contained in:
zyh
2025-09-13 10:47:28 +08:00
parent 40479fa38e
commit c4781b88dc
4 changed files with 917 additions and 0 deletions

View File

@@ -0,0 +1,381 @@
package com.gameplatform.server.service.cooldown;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* 基于内存的机器冷却服务 - 高性能版本
* 实现同一台机器在10分钟内不会重复调用的机制
*
* 优化点:
* 1. 纯内存操作无数据库IO性能极高
* 2. 使用ReentrantLock确保线程安全
* 3. 自动清理过期记录,避免内存泄漏
* 4. 支持高并发场景
*/
@Service
public class MemoryMachineCooldownService {
private static final Logger log = LoggerFactory.getLogger(MemoryMachineCooldownService.class);
// 冷却时间10分钟
private static final int COOLDOWN_MINUTES = 10;
// 内存缓存machineId -> 冷却信息
private final ConcurrentMap<String, CooldownInfo> cooldownMap = new ConcurrentHashMap<>();
// 为每个设备提供独立的锁,避免全局锁竞争
private final ConcurrentMap<String, ReentrantLock> deviceLocks = new ConcurrentHashMap<>();
// 定期清理过期记录的阈值
private static final int CLEANUP_THRESHOLD = 1000;
private volatile int lastCleanupSize = 0;
/**
* 冷却信息内部类
*/
private static class CooldownInfo {
private final LocalDateTime cooldownStartTime;
private final LocalDateTime cooldownEndTime;
private final String reason;
private final Long linkTaskId;
public CooldownInfo(LocalDateTime cooldownStartTime, LocalDateTime cooldownEndTime,
String reason, Long linkTaskId) {
this.cooldownStartTime = cooldownStartTime;
this.cooldownEndTime = cooldownEndTime;
this.reason = reason;
this.linkTaskId = linkTaskId;
}
public boolean isActive() {
return LocalDateTime.now().isBefore(cooldownEndTime);
}
public long getRemainingMinutes() {
if (!isActive()) {
return 0;
}
return java.time.Duration.between(LocalDateTime.now(), cooldownEndTime).toMinutes() + 1;
}
public LocalDateTime getCooldownStartTime() { return cooldownStartTime; }
public LocalDateTime getCooldownEndTime() { return cooldownEndTime; }
public String getReason() { return reason; }
public Long getLinkTaskId() { return linkTaskId; }
}
/**
* 获取设备锁(双重检查锁定模式)
*/
private ReentrantLock getDeviceLock(String machineId) {
ReentrantLock lock = deviceLocks.get(machineId);
if (lock == null) {
synchronized (deviceLocks) {
lock = deviceLocks.get(machineId);
if (lock == null) {
lock = new ReentrantLock();
deviceLocks.put(machineId, lock);
}
}
}
return lock;
}
/**
* 检查机器是否在冷却期内
* @param machineId 机器ID
* @return true表示在冷却期内false表示可以操作
*/
public boolean isMachineInCooldown(String machineId) {
if (machineId == null || machineId.trim().isEmpty()) {
return false;
}
CooldownInfo cooldownInfo = cooldownMap.get(machineId);
if (cooldownInfo == null) {
return false;
}
// 检查是否已过期
if (!cooldownInfo.isActive()) {
// 异步清理过期记录
cooldownMap.remove(machineId);
return false;
}
long remainingMinutes = cooldownInfo.getRemainingMinutes();
log.debug("机器{}在冷却期内,剩余冷却时间:{}分钟,原因:{}",
machineId, remainingMinutes, cooldownInfo.getReason());
return true;
}
/**
* 将机器加入冷却队列(线程安全)
* @param machineId 机器ID
* @param reason 加入冷却的原因
* @param linkTaskId 关联的链接任务ID可选
* @return true表示成功加入冷却false表示设备已在冷却中
*/
public boolean addMachineToCooldown(String machineId, String reason, Long linkTaskId) {
if (machineId == null || machineId.trim().isEmpty()) {
log.warn("尝试添加空的机器ID到冷却队列");
return false;
}
ReentrantLock lock = getDeviceLock(machineId);
lock.lock();
try {
// 双重检查:在锁内再次检查是否已在冷却中
CooldownInfo existingCooldown = cooldownMap.get(machineId);
if (existingCooldown != null && existingCooldown.isActive()) {
long remainingMinutes = existingCooldown.getRemainingMinutes();
log.info("机器{}已在冷却期内,剩余时间:{}分钟,原因:{},跳过重复添加",
machineId, remainingMinutes, existingCooldown.getReason());
return false;
}
// 创建新的冷却记录
LocalDateTime now = LocalDateTime.now();
LocalDateTime cooldownEndTime = now.plusMinutes(COOLDOWN_MINUTES);
CooldownInfo newCooldown = new CooldownInfo(now, cooldownEndTime, reason, linkTaskId);
cooldownMap.put(machineId, newCooldown);
log.info("机器{}已加入冷却队列,原因:{},冷却时间:{}分钟,冷却结束时间:{},关联任务:{}",
machineId, reason, COOLDOWN_MINUTES, cooldownEndTime, linkTaskId);
// 定期清理过期记录
cleanupExpiredCooldownsIfNeeded();
return true;
} finally {
lock.unlock();
}
}
/**
* 重载方法不指定linkTaskId
*/
public boolean addMachineToCooldown(String machineId, String reason) {
return addMachineToCooldown(machineId, reason, null);
}
/**
* 获取机器剩余冷却时间(分钟)
* @param machineId 机器ID
* @return 剩余冷却时间如果不在冷却期则返回0
*/
public long getRemainingCooldownMinutes(String machineId) {
if (machineId == null || machineId.trim().isEmpty()) {
return 0;
}
CooldownInfo cooldownInfo = cooldownMap.get(machineId);
if (cooldownInfo == null) {
return 0;
}
if (!cooldownInfo.isActive()) {
// 清理过期记录
cooldownMap.remove(machineId);
return 0;
}
return cooldownInfo.getRemainingMinutes();
}
/**
* 手动移除机器的冷却状态(用于测试或管理员操作)
* @param machineId 机器ID
* @return true表示成功移除false表示设备不在冷却中
*/
public boolean removeMachineFromCooldown(String machineId) {
if (machineId == null || machineId.trim().isEmpty()) {
return false;
}
ReentrantLock lock = getDeviceLock(machineId);
lock.lock();
try {
CooldownInfo removed = cooldownMap.remove(machineId);
if (removed != null) {
log.info("手动移除机器{}的冷却状态,原冷却原因:{}", machineId, removed.getReason());
return true;
}
return false;
} finally {
lock.unlock();
}
}
/**
* 获取当前冷却队列大小
* @return 冷却队列中的设备数量
*/
public int getCooldownQueueSize() {
// 先清理过期记录再返回大小
cleanupExpiredCooldownsIfNeeded();
return cooldownMap.size();
}
/**
* 清理过期的冷却记录
*/
public void cleanupExpiredCooldowns() {
int sizeBefore = cooldownMap.size();
// 使用迭代器安全地移除过期记录
cooldownMap.entrySet().removeIf(entry -> {
CooldownInfo cooldownInfo = entry.getValue();
if (!cooldownInfo.isActive()) {
log.debug("清理过期冷却记录:机器{},原因:{}", entry.getKey(), cooldownInfo.getReason());
return true;
}
return false;
});
int removedCount = sizeBefore - cooldownMap.size();
if (removedCount > 0) {
log.info("清理了{}个过期的冷却记录,当前冷却队列大小:{}", removedCount, cooldownMap.size());
}
}
/**
* 在需要时清理过期记录(避免频繁清理影响性能)
*/
private void cleanupExpiredCooldownsIfNeeded() {
int currentSize = cooldownMap.size();
if (currentSize > CLEANUP_THRESHOLD && currentSize > lastCleanupSize * 1.5) {
cleanupExpiredCooldowns();
lastCleanupSize = currentSize;
}
}
/**
* 原子方式尝试分配设备(检查+分配一体化,防止并发竞争)
* @param machineId 要分配的机器ID
* @param reason 分配原因
* @param linkTaskId 关联的链接任务ID
* @return true表示分配成功false表示设备已被占用或在冷却中
*/
public boolean tryAllocateDevice(String machineId, String reason, Long linkTaskId) {
if (machineId == null || machineId.trim().isEmpty()) {
log.warn("尝试分配空的机器ID");
return false;
}
ReentrantLock lock = getDeviceLock(machineId);
lock.lock();
try {
// 原子检查:确保设备不在冷却期内
CooldownInfo existingCooldown = cooldownMap.get(machineId);
if (existingCooldown != null && existingCooldown.isActive()) {
long remainingMinutes = existingCooldown.getRemainingMinutes();
log.debug("设备{}在冷却期内,无法分配,剩余时间:{}分钟,原因:{}",
machineId, remainingMinutes, existingCooldown.getReason());
return false;
}
// 原子分配:立即将设备加入冷却队列
LocalDateTime now = LocalDateTime.now();
LocalDateTime cooldownEndTime = now.plusMinutes(COOLDOWN_MINUTES);
CooldownInfo newCooldown = new CooldownInfo(now, cooldownEndTime, reason, linkTaskId);
cooldownMap.put(machineId, newCooldown);
log.info("设备{}原子分配成功,原因:{},冷却时间:{}分钟,冷却结束时间:{},关联任务:{}",
machineId, reason, COOLDOWN_MINUTES, cooldownEndTime, linkTaskId);
return true;
} finally {
lock.unlock();
}
}
/**
* 释放设备分配(如果后续操作失败时回滚)
* @param machineId 要释放的机器ID
* @param linkTaskId 关联的链接任务ID用于验证
* @return true表示释放成功
*/
public boolean releaseDeviceAllocation(String machineId, Long linkTaskId) {
if (machineId == null || machineId.trim().isEmpty()) {
return false;
}
ReentrantLock lock = getDeviceLock(machineId);
lock.lock();
try {
CooldownInfo cooldownInfo = cooldownMap.get(machineId);
if (cooldownInfo == null) {
return false;
}
// 验证是否是同一个任务的分配
if (linkTaskId != null && !linkTaskId.equals(cooldownInfo.getLinkTaskId())) {
log.warn("尝试释放设备{}失败任务ID不匹配当前任务{},请求任务:{}",
machineId, cooldownInfo.getLinkTaskId(), linkTaskId);
return false;
}
cooldownMap.remove(machineId);
log.info("设备{}分配已释放,原关联任务:{}", machineId, linkTaskId);
return true;
} finally {
lock.unlock();
}
}
/**
* 获取冷却统计信息
*/
public CooldownStats getCooldownStats() {
int totalDevices = cooldownMap.size();
int activeDevices = 0;
long totalRemainingMinutes = 0;
for (CooldownInfo cooldownInfo : cooldownMap.values()) {
if (cooldownInfo.isActive()) {
activeDevices++;
totalRemainingMinutes += cooldownInfo.getRemainingMinutes();
}
}
return new CooldownStats(totalDevices, activeDevices, totalRemainingMinutes);
}
/**
* 冷却统计信息
*/
public static class CooldownStats {
private final int totalDevices;
private final int activeDevices;
private final long totalRemainingMinutes;
public CooldownStats(int totalDevices, int activeDevices, long totalRemainingMinutes) {
this.totalDevices = totalDevices;
this.activeDevices = activeDevices;
this.totalRemainingMinutes = totalRemainingMinutes;
}
public int getTotalDevices() { return totalDevices; }
public int getActiveDevices() { return activeDevices; }
public long getTotalRemainingMinutes() { return totalRemainingMinutes; }
@Override
public String toString() {
return String.format("CooldownStats{total=%d, active=%d, totalRemainingMinutes=%d}",
totalDevices, activeDevices, totalRemainingMinutes);
}
}
}

View File

@@ -0,0 +1,154 @@
package com.gameplatform.server.service.link;
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.service.cooldown.MemoryMachineCooldownService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* 设备分配服务 - 提供线程安全的设备分配机制
* 解决并发环境下多个请求争抢同一设备的问题
*/
@Service
public class DeviceAllocationService {
private static final Logger log = LoggerFactory.getLogger(DeviceAllocationService.class);
private final MemoryMachineCooldownService machineCooldownService;
private final LinkTaskMapper linkTaskMapper;
public DeviceAllocationService(MemoryMachineCooldownService machineCooldownService,
LinkTaskMapper linkTaskMapper) {
this.machineCooldownService = machineCooldownService;
this.linkTaskMapper = linkTaskMapper;
}
/**
* 原子方式分配设备,避免并发竞争
* @param availableDevices 可用设备列表
* @param linkTaskId 链接任务ID
* @param reason 分配原因
* @return 分配成功的设备ID如果所有设备都被占用则返回null
*/
public String allocateDevice(List<String> availableDevices, Long linkTaskId, String reason) {
if (availableDevices == null || availableDevices.isEmpty()) {
log.warn("设备分配失败:没有可用设备");
return null;
}
log.info("开始设备分配流程:候选设备数={}, 任务ID={}, 原因={}",
availableDevices.size(), linkTaskId, reason);
// 1. 过滤掉被其他任务占用的设备
List<String> filteredDevices = filterOccupiedDevices(availableDevices);
if (filteredDevices.isEmpty()) {
log.warn("设备分配失败:所有候选设备都被其他任务占用");
return null;
}
log.info("设备占用检查完成:原候选设备数={}, 过滤后设备数={}, 可用设备={}",
availableDevices.size(), filteredDevices.size(), filteredDevices);
// 2. 打乱设备列表,实现负载均衡
List<String> shuffledDevices = new ArrayList<>(filteredDevices);
Collections.shuffle(shuffledDevices, ThreadLocalRandom.current());
log.info("设备列表已随机化:{}", shuffledDevices);
// 3. 尝试原子分配设备(按随机顺序)
for (String deviceId : shuffledDevices) {
if (machineCooldownService.tryAllocateDevice(deviceId, reason, linkTaskId)) {
log.info("设备分配成功:设备={}, 任务ID={}, 原因={}", deviceId, linkTaskId, reason);
return deviceId;
} else {
log.debug("设备{}分配失败(在冷却期或已被占用),尝试下一个设备", deviceId);
}
}
log.warn("设备分配失败:所有候选设备都在冷却期内");
return null;
}
/**
* 过滤掉被其他链接任务占用的设备
* @param devices 设备列表
* @return 未被占用的设备列表
*/
private List<String> filterOccupiedDevices(List<String> devices) {
List<String> availableDevices = new ArrayList<>();
for (String deviceId : devices) {
// 检查设备是否被其他链接任务占用USING、LOGGED_IN状态
List<LinkTask> occupiedTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "USING");
occupiedTasks.addAll(linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN"));
if (occupiedTasks.isEmpty()) {
availableDevices.add(deviceId);
} else {
log.debug("设备{}被其他链接任务占用,占用任务数={}", deviceId, occupiedTasks.size());
for (LinkTask occupiedTask : occupiedTasks) {
log.debug("占用设备{}的链接codeNo={}, status={}, 任务ID={}",
deviceId, occupiedTask.getCodeNo(), occupiedTask.getStatus(), occupiedTask.getId());
}
}
}
return availableDevices;
}
/**
* 验证设备分配结果(分配后的双重检查)
* @param deviceId 设备ID
* @param linkTaskId 链接任务ID
* @return true表示分配有效false表示存在冲突
*/
public boolean validateDeviceAllocation(String deviceId, Long linkTaskId) {
if (deviceId == null || linkTaskId == null) {
return false;
}
// 检查是否有其他任务也占用了这个设备
List<LinkTask> conflictTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "USING");
conflictTasks.addAll(linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN"));
// 过滤掉自己的任务
conflictTasks.removeIf(task -> task.getId().equals(linkTaskId));
if (!conflictTasks.isEmpty()) {
log.error("设备分配冲突检测:设备{}被多个任务占用当前任务ID={},冲突任务数={}",
deviceId, linkTaskId, conflictTasks.size());
for (LinkTask conflictTask : conflictTasks) {
log.error("冲突任务详情任务ID={}, codeNo={}, status={}, 设备={}",
conflictTask.getId(), conflictTask.getCodeNo(),
conflictTask.getStatus(), conflictTask.getMachineId());
}
return false;
}
log.debug("设备分配验证通过:设备={}, 任务ID={}", deviceId, linkTaskId);
return true;
}
/**
* 释放设备分配(失败回滚时使用)
* @param deviceId 设备ID
* @param linkTaskId 链接任务ID
*/
public void releaseDeviceAllocation(String deviceId, Long linkTaskId) {
if (deviceId != null && linkTaskId != null) {
boolean released = machineCooldownService.releaseDeviceAllocation(deviceId, linkTaskId);
if (released) {
log.info("设备分配已释放:设备={}, 任务ID={}", deviceId, linkTaskId);
} else {
log.warn("设备分配释放失败:设备={}, 任务ID={}", deviceId, linkTaskId);
}
}
}
}