评论分数排序定时任务优化

This commit is contained in:
tianfeng 2026-02-05 12:27:47 +08:00
parent 312c80ca2a
commit 0735f8d0ca
3 changed files with 188 additions and 37 deletions

View File

@ -4,6 +4,7 @@ import com.red.circle.common.business.core.ReplaceString;
import com.red.circle.common.business.core.SensitiveWordFilter;
import com.red.circle.common.business.enums.AccountStatusEnum;
import com.red.circle.common.business.enums.DynamicMessageEnum;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.app.common.room.DynamicCommon;
import com.red.circle.other.app.dto.clientobject.dynamic.DynamicCommentListCO;
@ -59,6 +60,7 @@ public class DynamicCommentCmdExe {
private final DynamicPopularService dynamicPopularService;
private final UserAccountCommon userAccountCommon;
private final DynamicPictureService dynamicPictureService;
private final RedisService redisService;
public DynamicCommentListCO execute(DynamicCommentCmd cmd) {
ResponseAssert.isTrue(DynamicErrorCode.CURRENT_LEVEL_IS_TOO_LOW, commentLimit(cmd));
@ -201,9 +203,29 @@ public class DynamicCommentCmdExe {
comment.setReplyCommentId(cmd.getCommentId());
dynamicCommentService.save(comment);
// 标记根评论需要更新热度新增子评论时
if (Objects.nonNull(cmd.getRootCommentId())) {
markCommentForHotScoreUpdate(cmd.getRootCommentId());
}
return comment;
}
/**
* 标记根评论需要更新热度分数
*/
private void markCommentForHotScoreUpdate(Long rootCommentId) {
if (rootCommentId == null) {
return;
}
try {
redisService.setAdd("comment:hot_score:pending", rootCommentId.toString());
} catch (Exception e) {
// 标记失败不影响主流程只记录日志
// 定时任务会兜底更新
}
}
private boolean isNotBlank(DynamicCommentCmd cmd) {
return StringUtils.isNotBlank(cmd.getContent().replaceAll(" ", ""));
}

View File

@ -41,6 +41,7 @@ public class DynamicCommentLikeCmdExe {
private final DynamicCommentLikeService dynamicCommentLikeService;
private final DynamicMessageService dynamicMessageService;
private final DynamicPictureService dynamicPictureService;
private final com.red.circle.component.redis.service.RedisService redisService;
public Long execute(DynamicLikeCmd cmd) {
@ -52,6 +53,9 @@ public class DynamicCommentLikeCmdExe {
ResponseAssert.isFalse(DynamicErrorCode.LIKED, getExist(cmd));
save(cmd, id);
// 标记根评论需要更新热度
markCommentForHotScoreUpdate(cmd.getRootCommentId());
if (Objects.equals(cmd.getReqUserId(), cmd.getToUserId())) {
return getIncrQuantity(cmd);
}
@ -68,6 +72,9 @@ public class DynamicCommentLikeCmdExe {
deleteCommentLike(cmd);
// 标记根评论需要更新热度
markCommentForHotScoreUpdate(cmd.getRootCommentId());
if (Objects.equals(cmd.getReqUserId(), cmd.getToUserId())) {
return getDecrQuantity(cmd);
}
@ -140,4 +147,19 @@ public class DynamicCommentLikeCmdExe {
dynamicMessageService.save(message);
}
/**
* 标记根评论需要更新热度分数
*/
private void markCommentForHotScoreUpdate(Long rootCommentId) {
if (rootCommentId == null) {
return;
}
try {
redisService.setAdd("comment:hot_score:pending", rootCommentId.toString());
} catch (Exception e) {
// 标记失败不影响主流程只记录日志
// 定时任务会兜底更新
}
}
}

View File

@ -1,28 +1,38 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.component.redis.annotation.TaskCacheLock;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.other.infra.database.cache.service.other.DynamicCacheService;
import com.red.circle.other.infra.database.rds.entity.dynamic.DynamicComment;
import com.red.circle.other.infra.database.rds.service.dynamic.DynamicCommentService;
import com.red.circle.tool.core.collection.CollectionUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 评论热度分数更新定时任务.
* 每10分钟执行一次更新最近7天的根评论热度分数
* 评论热度分数更新定时任务增量更新.
* 每10分钟执行一次优先更新有变化的评论
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicCommentHotScoreTask {
private static final String PENDING_SET_KEY = "comment:hot_score:pending";
private final RedisService redisService;
private final DynamicCommentService dynamicCommentService;
private final DynamicCacheService dynamicCacheService;
@ -36,34 +46,29 @@ public class DynamicCommentHotScoreTask {
log.info("评论热度分数更新任务 - 开始执行");
long startTime = System.currentTimeMillis();
// 查询最近7天的根评论分批处理
long sevenDaysAgo = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000L);
int totalUpdated = 0;
Long lastId = null;
boolean hasMore = true;
int incrementalUpdated = 0;
int fallbackUpdated = 0;
while (hasMore) {
// 分批查询每次1000条
List<DynamicComment> rootComments = queryRecentRootComments(sevenDaysAgo, lastId, 1000);
if (CollectionUtils.isEmpty(rootComments)) {
hasMore = false;
break;
}
// 1. 优先更新有变化的评论增量更新
Set<Long> pendingIds = getPendingCommentIds();
if (CollectionUtils.isNotEmpty(pendingIds)) {
incrementalUpdated = updatePendingComments(pendingIds);
}
// 批量更新热度分数
for (DynamicComment rootComment : rootComments) {
updateCommentHotScore(rootComment);
totalUpdated++;
}
// 准备下一批
lastId = rootComments.get(rootComments.size() - 1).getId();
hasMore = rootComments.size() >= 1000;
// 2. 兜底更新最近24小时的评论每小时整点执行
int currentMinute = LocalDateTime.now().getMinute();
if (currentMinute == 0) {
fallbackUpdated = updateRecentComments(pendingIds);
}
long costTime = System.currentTimeMillis() - startTime;
log.info("评论热度分数更新任务 - 执行完成,更新{}条评论,耗时{}ms", totalUpdated, costTime);
if (fallbackUpdated > 0) {
log.info("评论热度分数更新任务 - 执行完成,增量更新{}条,兜底更新{}条,总计{}条,耗时{}ms",
incrementalUpdated, fallbackUpdated, incrementalUpdated + fallbackUpdated, costTime);
} else {
log.info("评论热度分数更新任务 - 执行完成,增量更新{}条,耗时{}ms",
incrementalUpdated, costTime);
}
} catch (Exception e) {
log.error("评论热度分数更新任务 - 执行失败", e);
@ -71,16 +76,118 @@ public class DynamicCommentHotScoreTask {
}
/**
* 查询最近的根评论root_comment_id null
* 获取待更新的评论ID集合
*/
private List<DynamicComment> queryRecentRootComments(long since, Long lastId, int limit) {
return dynamicCommentService.query()
.isNull(DynamicComment::getRootCommentId)
.ge(DynamicComment::getCreateTime, new java.sql.Timestamp(since))
.gt(lastId != null, DynamicComment::getId, lastId)
.orderByAsc(DynamicComment::getId)
.last(PageConstant.formatLimit(limit))
.list();
private Set<Long> getPendingCommentIds() {
try {
Set<String> pendingStrSet = redisService.setGet(PENDING_SET_KEY).stream().map(String::valueOf).collect(Collectors.toSet());
if (CollectionUtils.isEmpty(pendingStrSet)) {
return new HashSet<>();
}
Set<Long> pendingIds = new HashSet<>();
for (String idStr : pendingStrSet) {
try {
pendingIds.add(Long.parseLong(idStr));
} catch (NumberFormatException e) {
log.warn("无效的评论ID: {}", idStr);
}
}
return pendingIds;
} catch (Exception e) {
log.error("获取待更新评论ID集合失败", e);
return new HashSet<>();
}
}
/**
* 更新待处理的评论
*/
private int updatePendingComments(Set<Long> pendingIds) {
int updated = 0;
List<String> successIds = new ArrayList<>();
for (Long commentId : pendingIds) {
try {
DynamicComment comment = dynamicCommentService.getById(commentId);
if (comment == null) {
// 评论已被删除从待处理集合移除
successIds.add(commentId.toString());
continue;
}
// 只更新根评论
if (comment.getRootCommentId() == null) {
updateCommentHotScore(comment);
updated++;
}
// 更新成功从待处理集合移除
successIds.add(commentId.toString());
} catch (Exception e) {
log.error("更新评论热度失败commentId={}", commentId, e);
// 失败的不移除下次继续尝试
}
}
// 批量移除已处理的ID
if (CollectionUtils.isNotEmpty(successIds)) {
try {
redisService.setRemove(PENDING_SET_KEY, successIds.toArray(new String[0]));
} catch (Exception e) {
log.error("移除已处理的评论ID失败", e);
}
}
return updated;
}
/**
* 兜底更新最近24小时的评论应对时间衰减
*/
private int updateRecentComments(Set<Long> excludeIds) {
int updated = 0;
try {
long oneDayAgo = System.currentTimeMillis() - (24 * 60 * 60 * 1000L);
Long lastId = null;
boolean hasMore = true;
int batchSize = 500;
while (hasMore && updated < 2000) { // 限制最多2000条避免执行时间过长
List<DynamicComment> comments = dynamicCommentService.query()
.isNull(DynamicComment::getRootCommentId)
.ge(DynamicComment::getCreateTime, new java.sql.Timestamp(oneDayAgo))
.gt(lastId != null, DynamicComment::getId, lastId)
.orderByAsc(DynamicComment::getId)
.last(PageConstant.formatLimit(batchSize))
.list();
if (CollectionUtils.isEmpty(comments)) {
hasMore = false;
break;
}
for (DynamicComment comment : comments) {
// 跳过已经在增量更新中处理过的
if (excludeIds != null && excludeIds.contains(comment.getId())) {
continue;
}
updateCommentHotScore(comment);
updated++;
}
lastId = comments.get(comments.size() - 1).getId();
hasMore = comments.size() >= batchSize;
}
} catch (Exception e) {
log.error("兜底更新最近评论失败", e);
}
return updated;
}
/**
@ -123,7 +230,7 @@ public class DynamicCommentHotScoreTask {
.execute();
if (totalChildLikes > 0) {
log.debug("更新评论热度 - 根评论ID:{}, 根评论点赞:{}, 子评论点赞:{}, 总点赞:{}, 热度分数:{}",
log.debug("更新评论热度 - 根评论ID:{}, 根评论点赞:{}, 子评论点赞:{}, 总点赞:{}, 热度分数:{}",
rootComment.getId(), rootLikeCount, totalChildLikes, totalLikeCount, hotScore);
}
@ -140,7 +247,7 @@ public class DynamicCommentHotScoreTask {
long currentTime = System.currentTimeMillis();
long ageMillis = currentTime - comment.getCreateTime().getTime();
double ageHours = Math.max(ageMillis / (1000.0 * 60 * 60), 0.1);
return totalLikeCount / Math.pow(ageHours + 2, 1.5);
}