feat: 解决设备分配并发竞争问题,优化冷却机制和设备分配服务
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user