火箭能量异步刷新数据丢失 问题处理2

This commit is contained in:
tianfeng 2025-11-18 01:18:09 +08:00
parent b39b6ac26c
commit 1be939fd0e
5 changed files with 286 additions and 105 deletions

View File

@ -0,0 +1,35 @@
package com.red.circle.other.adapter.app;
import com.red.circle.other.app.manager.RocketEnergyAggregator;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 火箭能量队列监控接口
*/
@RestController
@RequestMapping("/rocket/monitor")
@RequiredArgsConstructor
public class RocketMonitorRestController {
private final RocketEnergyAggregator rocketEnergyAggregator;
/**
* 获取队列状态
*/
@GetMapping("/queue/status")
public Map<String, Object> getQueueStatus() {
return rocketEnergyAggregator.getQueueStatus();
}
/**
* 手动触发刷新
*/
@PostMapping("/queue/flush")
public String manualFlush() {
rocketEnergyAggregator.manualFlush();
return "刷新任务已触发";
}
}

View File

@ -62,7 +62,7 @@ public class RocketLaunchCmdExe {
* @param triggerUserId 触发升级的用户ID
*/
public void execute(Long roomId, Long triggerUserId) {
String lockKey = RocketKeys.LOCK.getKey(roomId);
String lockKey = RocketKeys.LAUNCH.getKey(roomId);
distributedLockUtil.executeWithLock(lockKey, 10L, () -> {
doLaunch(roomId, triggerUserId);

View File

@ -1,5 +1,6 @@
package com.red.circle.other.app.manager;
import com.alibaba.fastjson.JSON;
import com.red.circle.other.app.convertor.RocketConvertor;
import com.red.circle.other.app.dto.clientobject.RocketStatusCO;
import com.red.circle.other.app.dto.cmd.RocketEnergyAddCmd;
@ -8,27 +9,23 @@ import com.red.circle.other.domain.gateway.RocketConfigGateway;
import com.red.circle.other.domain.gateway.RocketStatusGateway;
import com.red.circle.other.domain.rocket.RocketConfig;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.domain.rocket.RocketStatusEnum;
import com.red.circle.other.infra.database.cache.key.RocketKeys;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.infra.gateway.RocketStatusGatewayImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
/**
* 火箭能量聚合器
* 职责
* 1. 接收能量增加请求缓存到内存
* 2. 定时批量刷新到数据库
* 3. 定时推送能量更新消息
* 火箭能量聚合器Redis队列 + 多实例消费版本
*
* @author system
* @date 2025-01-15
@ -45,145 +42,235 @@ public class RocketEnergyAggregator {
private final RocketImPushManager rocketImPushManager;
private final RocketLaunchManager rocketLaunchManager;
private final RoomProfileManagerService roomProfileManagerService;
private final StringRedisTemplate redisTemplate;
/**
* 房间能量聚合缓存高性能线程安全队列
* Key: roomId
* Value: 该房间待聚合的能量队列
* Redis 队列 Key 前缀
*/
private final Map<Long, ConcurrentLinkedQueue<RocketEnergyAddCmd>> energyBuffer = new ConcurrentHashMap<>();
private static final String ENERGY_QUEUE_PREFIX = "rocket:energy:queue:";
/**
* Redis 房间列表 Key记录所有有数据的房间
*/
private static final String ACTIVE_ROOMS_KEY = "rocket:energy:active:rooms";
/**
* 推送间隔
*/
private static final long PUSH_INTERVAL = 3000L;
/**
* 批量刷新间隔
*/
private static final long FLUSH_INTERVAL = 5000L;
/**
* 每次最大处理数量
*/
private static final int MAX_BATCH_SIZE = 100;
/**
* 锁超时时间
*/
private static final long LOCK_TIMEOUT = 10L;
/**
* 上次推送时间
* Key: roomId
* Value: 上次推送的时间戳
* 上次推送时间本地缓存
*/
private final Map<Long, Long> lastPushTime = new ConcurrentHashMap<>();
/**
* 推送间隔毫秒
*/
private static final long PUSH_INTERVAL = 3000L; // 3秒推送一次
/**
* 批量刷新间隔毫秒
*/
private static final long FLUSH_INTERVAL = 5000L; // 5秒刷新一次
/**
* 接收能量增加请求
* 接收能量增加请求写入Redis队列
*/
public void addEnergy(RocketEnergyAddCmd cmd) {
energyBuffer.computeIfAbsent(cmd.getRoomId(), k -> new ConcurrentLinkedQueue<>())
.offer(cmd);
log.debug("缓存火箭能量: roomId={}, userId={}, energy={}",
cmd.getRoomId(), cmd.getUserId(), cmd.getGiftGoldValue());
try {
Long roomId = cmd.getRoomId();
String queueKey = ENERGY_QUEUE_PREFIX + roomId;
String value = JSON.toJSONString(cmd);
// 写入队列
redisTemplate.opsForList().rightPush(queueKey, value);
// 记录活跃房间使用 Set 去重
redisTemplate.opsForSet().add(ACTIVE_ROOMS_KEY, roomId.toString());
// 设置队列过期时间24小时
redisTemplate.expire(queueKey, 24, TimeUnit.HOURS);
log.debug("能量写入Redis队列: roomId={}, userId={}, energy={}",
roomId, cmd.getUserId(), cmd.getGiftGoldValue());
} catch (Exception e) {
log.error("写入能量队列失败: roomId={}", cmd.getRoomId(), e);
throw e;
}
}
/**
* 定时批量刷新到数据库每5秒
* 定时批量刷新
*/
@Scheduled(fixedDelay = FLUSH_INTERVAL)
public void flushToDatabase() {
if (energyBuffer.isEmpty()) {
return;
}
log.info("开始批量刷新火箭能量, 待处理房间数={}", energyBuffer.size());
// 遍历所有房间
energyBuffer.forEach((roomId, queue) -> {
if (queue.isEmpty()) {
try {
// 获取所有活跃房间
Set<String> activeRooms = redisTemplate.opsForSet().members(ACTIVE_ROOMS_KEY);
if (activeRooms == null || activeRooms.isEmpty()) {
log.debug("没有待处理的房间");
return;
}
try {
String lockKey = RocketKeys.LOCK.getKey(roomId);
log.info("开始批量刷新火箭能量, 待处理房间数={}", activeRooms.size());
int successCount = 0;
int skipCount = 0;
int errorCount = 0;
distributedLockUtil.executeWithLock(lockKey, 10L, () -> {
// peek 不删除等处理成功后再删除
List<RocketEnergyAddCmd> batch = new ArrayList<>(queue); // 复制数据不删除原队列
if (batch.isEmpty()) {
return null;
// 遍历所有活跃房间尝试获取锁并处理
for (String roomIdStr : activeRooms) {
Long roomId = null;
try {
roomId = Long.parseLong(roomIdStr);
// 尝试获取房间锁非阻塞
String lockKey = RocketKeys.LOCK.getKey(roomId);
Boolean locked = distributedLockUtil.tryLock(lockKey, LOCK_TIMEOUT);
if (!locked) {
// 其他实例正在处理这个房间跳过
skipCount++;
log.debug("房间{}被其他实例占用,跳过", roomId);
continue;
}
try {
// 处理这个房间的能量增加
processRoomEnergyBatch(roomId, batch);
// 处理成功后才删除队列中的数据
for (int i = 0; i < batch.size(); i++) {
queue.poll();
// 获取锁成功处理这个房间
boolean processed = processRoomEnergyFromRedis(roomId);
if (processed) {
successCount++;
}
} catch (Exception e) {
log.error("处理房间{}火箭能量失败,数据保留在队列中等待重试", roomId, e);
throw e; // 重新抛出让外层处理
} finally {
// 释放锁
distributedLockUtil.unlock(lockKey);
}
return null;
}, "火箭能量刷新操作中");
} catch (Exception e) {
log.error("刷新房间{}火箭能量失败", roomId, e);
} catch (Exception e) {
errorCount++;
log.error("处理房间{}能量失败", roomId, e);
}
}
});
log.info("批量刷新完成: 成功={}, 跳过={}, 失败={}", successCount, skipCount, errorCount);
} catch (Exception e) {
log.error("批量刷新火箭能量失败", e);
}
}
/**
* 从Redis队列处理房间能量
* @return 是否处理了数据
*/
private boolean processRoomEnergyFromRedis(Long roomId) {
String queueKey = ENERGY_QUEUE_PREFIX + roomId;
try {
// 检查队列长度
Long queueSize = redisTemplate.opsForList().size(queueKey);
if (queueSize == null || queueSize == 0) {
// 队列为空从活跃房间列表中移除
redisTemplate.opsForSet().remove(ACTIVE_ROOMS_KEY, roomId.toString());
log.debug("房间{}队列为空,从活跃列表移除", roomId);
return false;
}
// 批量取出数据使用 LRANGE 查看不删除
List<String> values = redisTemplate.opsForList().range(queueKey, 0, MAX_BATCH_SIZE - 1);
if (values == null || values.isEmpty()) {
return false;
}
// 解析数据
List<RocketEnergyAddCmd> batch = new ArrayList<>();
for (String value : values) {
try {
RocketEnergyAddCmd cmd = JSON.parseObject(value, RocketEnergyAddCmd.class);
batch.add(cmd);
} catch (Exception e) {
log.error("解析能量数据失败,写入死信队列: {}", value, e);
redisTemplate.opsForList().rightPush("rocket:energy:dead:letter", value);
}
}
if (batch.isEmpty()) {
// 数据解析失败删除这些无效数据
redisTemplate.opsForList().trim(queueKey, values.size(), -1);
return false;
}
// 处理能量增加
processRoomEnergyBatch(roomId, batch);
// 处理成功后才删除Redis中的数据使用 LTRIM
redisTemplate.opsForList().trim(queueKey, batch.size(), -1);
log.info("成功处理房间{}能量, 处理数量={}, 剩余={}",
roomId, batch.size(), queueSize - batch.size());
return true;
} catch (Exception e) {
log.error("处理房间{}能量失败数据保留在Redis中等待重试", roomId, e);
throw e;
}
}
/**
* 处理单个房间的能量批次
*/
private void processRoomEnergyBatch(Long roomId, List<RocketEnergyAddCmd> batch) {
// 1. 查询当前火箭状态不包含贡献者列表性能优化
String today = LocalDate.now().toString();
// 查询当前火箭状态
RocketStatus rocketStatus = rocketStatusGatewayImpl
.findByRoomIdAndDateWithoutContributors(roomId, today);
if (rocketStatus == null) {
// 查询一级火箭配置
// 初始化火箭
RocketConfig level1Config = rocketConfigGateway.findByLevel(1);
if (level1Config == null || level1Config.getStatus() != 1) {
log.error("一级火箭配置不存在或未启用");
return;
throw new RuntimeException("一级火箭配置不存在");
}
// 使用配置初始化
rocketStatus = RocketStatus.init(roomId, level1Config);
log.info("初始化房间火箭, roomId={}, maxEnergy={}", roomId, level1Config.getMaxEnergy());
}
// 2. 检查状态
// 校验状态
if (!rocketStatus.canAddEnergy()) {
log.warn("火箭不可充能, roomId={}, status={}", roomId, rocketStatus.getStatus());
return;
throw new RuntimeException("火箭状态不允许充能");
}
// 3. 记录增加能量前的状态
// 记录增加前的状态
long energyBeforeAdd = rocketStatus.getCurrentEnergy();
long maxEnergy = rocketStatus.getMaxEnergy();
// 4. 批量增加能量并找出触发升级的用户
// 批量增加能量
long totalEnergy = 0L;
Long triggerUserId = null;
for (RocketEnergyAddCmd cmd : batch) {
// 增加前的能量
long beforeEnergy = rocketStatus.getCurrentEnergy();
// 增加能量
rocketStatus.addEnergy(
cmd.getUserId(),
cmd.getGiftGoldValue()
);
rocketStatus.addEnergy(cmd.getUserId(), cmd.getGiftGoldValue());
totalEnergy += cmd.getGiftGoldValue();
// 增加后的能量
long afterEnergy = rocketStatus.getCurrentEnergy();
// 判断是否是这个用户触发了满能量
// 记录触发满能量的用户
if (beforeEnergy < maxEnergy && afterEnergy >= maxEnergy && triggerUserId == null) {
triggerUserId = cmd.getUserId();
log.info("找到触发升级的用户: userId={}, beforeEnergy={}, afterEnergy={}, maxEnergy={}",
@ -191,22 +278,27 @@ public class RocketEnergyAggregator {
}
}
// 5. 保存状态一次性保存
rocketStatusGateway.save(rocketStatus);
// 保存状态
try {
rocketStatusGateway.save(rocketStatus);
} catch (Exception e) {
log.error("保存火箭状态失败: roomId={}", roomId, e);
throw e;
}
log.info("批量增加火箭能量: roomId={}, 批次数量={}, 总能量={}, 当前能量={}/{}",
roomId, batch.size(), totalEnergy,
log.info("批量增加火箭能量: roomId={}, 批次数量={}, 总能量={}, 更新前={}, 更新后={}/{}",
roomId, batch.size(), totalEnergy, energyBeforeAdd,
rocketStatus.getCurrentEnergy(), rocketStatus.getMaxEnergy());
// 6. 推送更新消息节流
// 推送更新消息节流
pushEnergyUpdate(roomId, rocketStatus);
// 7. 检查是否需要发射
if (rocketStatus.isFullEnergy()) {
// 检查是否需要发射添加状态检查
if (rocketStatus.isFullEnergy() && rocketStatus.getStatus() == RocketStatusEnum.CHARGING) {
log.info("火箭能量已满, 准备发射, roomId={}, level={}, 触发用户={}",
roomId, rocketStatus.getLevel().getLevel(), triggerUserId);
// 异步触发发射传入触发用户信息
// 异步触发发射
rocketLaunchManager.launchAsync(roomId, triggerUserId);
}
}
@ -240,17 +332,30 @@ public class RocketEnergyAggregator {
}
/**
* 定时清理空闲房间的资源每小时
* 定时清理过期数据每小时
*/
@Scheduled(fixedRate = 3600000)
public void cleanupIdleRooms() {
// 清理buffer中的空队列
energyBuffer.entrySet().removeIf(entry -> entry.getValue().isEmpty());
// 清理推送时间
lastPushTime.keySet().removeIf(roomId -> !energyBuffer.containsKey(roomId));
log.info("清理空闲房间资源, 当前活跃房间数={}", energyBuffer.size());
public void cleanupExpiredData() {
try {
// 清理本地推送时间缓存
Set<String> activeRooms = redisTemplate.opsForSet().members(ACTIVE_ROOMS_KEY);
if (activeRooms != null) {
Set<Long> activeRoomIds = new HashSet<>();
for (String roomIdStr : activeRooms) {
try {
activeRoomIds.add(Long.parseLong(roomIdStr));
} catch (Exception ignored) {
}
}
// 移除不活跃房间的推送时间
lastPushTime.keySet().removeIf(roomId -> !activeRoomIds.contains(roomId));
}
log.info("清理过期数据完成, 当前活跃房间数={}", activeRooms != null ? activeRooms.size() : 0);
} catch (Exception e) {
log.error("清理过期数据失败", e);
}
}
/**
@ -259,6 +364,45 @@ public class RocketEnergyAggregator {
@javax.annotation.PreDestroy
public void shutdown() {
log.info("应用关闭,强制刷新所有待处理能量");
try {
flushToDatabase();
} catch (Exception e) {
log.error("关闭时刷新失败", e);
}
}
/**
* 手动触发刷新供运维使用
*/
public void manualFlush() {
log.info("手动触发刷新");
flushToDatabase();
}
}
/**
* 获取队列状态监控使用
*/
public Map<String, Object> getQueueStatus() {
Map<String, Object> status = new HashMap<>();
Set<String> activeRooms = redisTemplate.opsForSet().members(ACTIVE_ROOMS_KEY);
status.put("activeRoomCount", activeRooms != null ? activeRooms.size() : 0);
status.put("localPushCacheSize", lastPushTime.size());
if (activeRooms != null && !activeRooms.isEmpty()) {
Map<Long, Long> queueSizes = new HashMap<>();
for (String roomIdStr : activeRooms) {
try {
Long roomId = Long.parseLong(roomIdStr);
String queueKey = ENERGY_QUEUE_PREFIX + roomId;
Long size = redisTemplate.opsForList().size(queueKey);
queueSizes.put(roomId, size != null ? size : 0L);
} catch (Exception ignored) {
}
}
status.put("queueSizes", queueSizes);
}
return status;
}
}

View File

@ -89,7 +89,7 @@ public class RocketHistory {
history.setFinalEnergy(status.getCurrentEnergy());
// 计算超出能量
Long overEnergy = status.getCurrentEnergy() - status.getLevel().getMaxEnergy();
Long overEnergy = status.getCurrentEnergy() - status.getMaxEnergy();
history.setOverEnergy(Math.max(overEnergy, 0L));
// 转换贡献者

View File

@ -42,6 +42,8 @@ public enum RocketKeys implements RedisKeys {
*/
CLAIMED,
LAUNCH,
/**
* 火箭配置缓存
* KEY: ROCKET:CONFIG:{level}