fix(live): 修复心跳高频调用导致 live 服务 CPU 过载问题

1. 移除 HeartbeatUploadCmdExe 中冗余的 addRoomOnlineUser 同步调用,
   MQ Listener 已处理,避免每次心跳双重触发 IM 群消息广播
2. UserHeartbeatListener 合并两次 roomManagerClient.getById() HTTP 调用为一次,
   并将结果传入 processLiveHeartBeat 复用
3. 修复麦克风锁定时 MQ 递归重试风暴,retryRefreshMick 已置 true 时直接 return
4. refreshUserProcess 发 IM 群消息前加 Redis 限流(同一房间 30s 内最多 1 次),
   防止用户过期触发无限广播

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

(cherry picked from commit 84792459a5b11e4862cde42dad280b3aec7731aa)
This commit is contained in:
tianfeng 2026-04-07 17:46:11 +08:00
parent 18591d21ed
commit 4eb2f8330d
3 changed files with 19 additions and 34 deletions

View File

@ -5,13 +5,8 @@ import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.live.app.dto.cmd.HeartbeatStatusEnum;
import com.red.circle.live.app.dto.cmd.HeartbeatUploadCmd;
import com.red.circle.live.domain.gateway.LiveMicrophoneGateway;
import com.red.circle.live.domain.online.LiveRoomUser;
import com.red.circle.mq.business.model.event.user.UserHeartbeatEvent;
import com.red.circle.mq.rocket.business.producer.UserMqMessageService;
import com.red.circle.tool.core.parse.DataTypeUtils;
import com.red.circle.tool.core.regex.RegexConstant;
import com.red.circle.tool.core.text.StringUtils;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
@ -28,24 +23,11 @@ public class HeartbeatUploadCmdExe {
private final RedisService redisService;
private final UserMqMessageService userMqMessageService;
private final LiveMicrophoneGateway liveMicrophoneGateway;
public void execute(HeartbeatUploadCmd cmd) {
ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
HeartbeatStatusEnum.toEnum(cmd.getStatus()));
// 实时加入房间
if (StringUtils.isNotBlank(cmd.getSessionId())
&& cmd.getSessionId().trim().matches(RegexConstant.NUMBER)) {
liveMicrophoneGateway.addRoomOnlineUser(
DataTypeUtils.toLong(cmd.getSessionId()),
new LiveRoomUser()
.setUserId(cmd.requiredReqUserId())
.setWeights(0)
.updateLastActivityTime()
);
}
// 如果是定时触发, 限流15s 1次
if (Objects.equals(cmd.getTask(), Boolean.TRUE)
&& !redisService.setIfAbsent("UHU:" + cmd.requiredReqUserId(), 1, 15, TimeUnit.SECONDS)) {

View File

@ -110,7 +110,6 @@ public class UserHeartbeatListener implements MessageListener {
UserProfileDTO userProfileDTO = new UserProfileDTO();
userProfileDTO.setId(event.getUserId());
processLiveHeartBeat(event, userProfileDTO);
uploadHistoryHeartBeat(event);
Long roomId = DataTypeUtils.toLong(event.getSessionId());
@ -123,6 +122,13 @@ public class UserHeartbeatListener implements MessageListener {
return;
}
// 缓存 roomAccount供后续 processLiveHeartBeat 直接使用避免重复 HTTP 调用
if (Objects.nonNull(roomProfile.getRoomAccount())) {
liveRoomCacheService.save(roomProfile.getRoomAccount(), roomId);
}
processLiveHeartBeat(event, userProfileDTO, roomProfile);
// 房间内同时在线用户数
if (Objects.equals(roomProfile.getUserId(), event.getUserId())) {
List<LiveRoomUser> liveRoomUsers = liveMicUserCacheService.listUser(roomProfile.getId());
@ -188,19 +194,8 @@ public class UserHeartbeatListener implements MessageListener {
return redisService.setIfAbsent(key, "1", HEARTBEAT_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
private void processLiveHeartBeat(UserHeartbeatEvent event, UserProfileDTO userProfile) {
if (StringUtils.isBlank(event.getSessionId())
|| !event.getSessionId().trim().matches(RegexConstant.NUMBER)) {
return;
}
Long roomId = DataTypeUtils.toLong(event.getSessionId());
if (Objects.isNull(roomId) || roomId <= 0) {
log.warn("房间ID错误: {}", event.getSessionId());
return;
}
RoomProfileDTO roomProfile = getRoomProfileDto(roomId);
private void processLiveHeartBeat(UserHeartbeatEvent event, UserProfileDTO userProfile,
RoomProfileDTO roomProfile) {
if (Objects.isNull(roomProfile.getRoomAccount())) {
log.warn("没有找到房间ID: {}", event.getSessionId());
return;
@ -278,8 +273,11 @@ public class UserHeartbeatListener implements MessageListener {
LiveMicrophone liveMicrophone = liveMicrophones.get(0);
// 刷新活跃时间
// 刷新活跃时间锁定时最多重试一次避免 MQ 递归风暴
if (liveMicCacheService.checkLockMic(roomProfile.getId(), liveMicrophone.getMicIndex())) {
if (Boolean.TRUE.equals(event.getRetryRefreshMick())) {
return;
}
event.setRetryRefreshMick(true);
event.setReqTrackId("retry-" + IdWorkerUtils.getIdStr());
userMqMessageService.sendUserHeartbeat(event);

View File

@ -56,6 +56,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.red.circle.tool.core.thread.ThreadPoolManager;
@ -751,7 +752,11 @@ public class LiveMicrophoneGatewayImpl implements LiveMicrophoneGateway {
liveMusicHeartbeatService.removeMickUser(roomProfile.getId());
}
// 发送通知
// 发送通知30s 内同一房间最多推送一次防止心跳高频触发 IM 消息风暴
String imLimitKey = "REFRESH_ONLINE_USER_LIMIT:" + roomId;
if (!redisService.setIfAbsent(imLimitKey, 1, 30, TimeUnit.SECONDS)) {
return;
}
RefreshOnlineUserNoticeDTO noticeRes = new RefreshOnlineUserNoticeDTO()
.setTimeId(IdWorkerUtils.getIdStr())
.setType(RefreshOnlineUserType.KILL)