feat(comment): 实现评论热度排序及缓存优化

- 修改 CommentDO 中 heat 字段类型从 BigDecimal为 Double
- 新增 selectHeatComments 方法用于查询热门评论- 优化评论分页查询逻辑,引入 Redis 缓存提升性能
- 新增评论总数与热门评论的 Redis 缓存同步机制
- 实现评论详情的 Redis 批量缓存与过期策略
- 添加 COMMENT_NOT_FOUND 业务异常码
- 更新 RedisKeyConstants 增加相关键构建方法
- 调整 XML 映射文件以支持新的查询与字段类型- 引入 RedisTemplate 和线程池异步处理缓存操作
- 在 FindCommentItemRspVO 中新增 heat 字段返回热度值
This commit is contained in:
2025-11-08 11:43:32 +08:00
parent fdee4dc2b4
commit 6fbe8eed25
8 changed files with 380 additions and 114 deletions

View File

@@ -6,6 +6,7 @@
<w>hannote</w>
<w>hanserwei</w>
<w>jobhandler</w>
<w>mget</w>
<w>nacos</w>
<w>operationlog</w>
<w>rustfs</w>

View File

@@ -7,6 +7,26 @@ public class RedisKeyConstants {
*/
private static final String HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX = "comment:havaFirstReplyCommentId:";
/**
* Hash Field 键:评论总数
*/
public static final String FIELD_COMMENT_TOTAL = "commentTotal";
/**
* Key 前缀:笔记评论总数
*/
private static final String COUNT_COMMENT_TOTAL_KEY_PREFIX = "count:note:";
/**
* Key 前缀:评论分页 ZSET
*/
private static final String COMMENT_LIST_KEY_PREFIX = "comment:list:";
/**
* Key 前缀:评论详情 JSON
*/
private static final String COMMENT_DETAIL_KEY_PREFIX = "comment:detail:";
/**
* 构建完整 KEY
@@ -18,4 +38,34 @@ public class RedisKeyConstants {
return HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX + commentId;
}
/**
* 构建笔记评论总数完整 KEY
*
* @param noteId 笔记 ID
* @return 笔记评论总数完整 KEY
*/
public static String buildNoteCommentTotalKey(Long noteId) {
return COUNT_COMMENT_TOTAL_KEY_PREFIX + noteId;
}
/**
* 构建评论分页 ZSET 完整 KEY
*
* @param noteId 笔记 ID
* @return 评论分页 ZSET 完整 KEY
*/
public static String buildCommentListKey(Long noteId) {
return COMMENT_LIST_KEY_PREFIX + noteId;
}
/**
* 构建评论详情完整 KEY
*
* @param commentId 评论 ID
* @return 评论详情完整 KEY
*/
public static String buildCommentDetailKey(Object commentId) {
return COMMENT_DETAIL_KEY_PREFIX + commentId;
}
}

View File

@@ -9,7 +9,6 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
@@ -121,7 +120,7 @@ public class CommentDO {
* 评论热度
*/
@TableField(value = "heat")
private BigDecimal heat;
private Double heat;
/**
* 最早回复的评论ID (只有一级评论需要)

View File

@@ -74,4 +74,12 @@ public interface CommentDOMapper extends BaseMapper<CommentDO> {
* @return 二级评论
*/
List<CommentDO> selectTwoLevelCommentByIds(@Param("commentIds") List<Long> commentIds);
/**
* 查询热门评论
*
* @param noteId 笔记 ID
* @return 热门评论
*/
List<CommentDO> selectHeatComments(Long noteId);
}

View File

@@ -13,6 +13,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
PARAM_NOT_VALID("COMMENT-10001", "参数错误"),
// ----------- 业务异常状态码 -----------
COMMENT_NOT_FOUND("COMMENT-20001", "此评论不存在"),
;
// 异常码

View File

@@ -61,4 +61,9 @@ public class FindCommentItemRspVO {
*/
private FindCommentItemRspVO firstReplyComment;
/**
* 热度值
*/
private Double heat;
}

View File

@@ -1,19 +1,24 @@
package com.hanserwei.hannote.comment.biz.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.constant.DateConstants;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.PageResponse;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.framework.common.utils.DateUtils;
import com.hanserwei.framework.common.utils.JsonUtils;
import com.hanserwei.hannote.comment.biz.constants.MQConstants;
import com.hanserwei.hannote.comment.biz.constants.RedisKeyConstants;
import com.hanserwei.hannote.comment.biz.domain.dataobject.CommentDO;
import com.hanserwei.hannote.comment.biz.domain.mapper.CommentDOMapper;
import com.hanserwei.hannote.comment.biz.domain.mapper.NoteCountDOMapper;
import com.hanserwei.hannote.comment.biz.enums.ResponseCodeEnum;
import com.hanserwei.hannote.comment.biz.model.dto.PublishCommentMqDTO;
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentItemRspVO;
import com.hanserwei.hannote.comment.biz.model.vo.FindCommentPageListReqVO;
@@ -29,12 +34,13 @@ import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.*;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@@ -53,6 +59,10 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
private KeyValueRpcService keyValueRpcService;
@Resource
private UserRpcService userRpcService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "taskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Override
public Response<?> publishComment(PublishCommentReqVO publishCommentReqVO) {
@@ -127,13 +137,33 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
// 每页展示一级评论数量
int pageSize = 10;
// TODO 先从缓存中查询
// 构建评论总数 Redis Key
String noteCommentTotalKey = RedisKeyConstants.buildNoteCommentTotalKey(noteId);
// 先从 Redis 中查询该笔记的评论总数
Number commentTotal = (Number) redisTemplate.opsForHash()
.get(noteCommentTotalKey, RedisKeyConstants.FIELD_COMMENT_TOTAL);
long count = Objects.isNull(commentTotal) ? 0L : commentTotal.longValue();
// 查询评论总数
Long count = noteCountDOMapper.selectCommentTotalByNoteId(noteId);
// 若缓存不存在,则查询数据库
if (Objects.isNull(commentTotal)) {
// 查询评论总数 (从 t_note_count 笔记计数表查,提升查询性能, 避免 count(*))
Long dbCount = noteCountDOMapper.selectCommentTotalByNoteId(noteId);
if (Objects.isNull(count)) {
return PageResponse.success(null, pageNo, pageSize);
// 若数据库中也不存在,则抛出业务异常
if (Objects.isNull(dbCount)) {
throw new ApiException(ResponseCodeEnum.COMMENT_NOT_FOUND);
}
count = dbCount;
// 异步将评论总数同步到 Redis 中
threadPoolTaskExecutor.execute(() ->
syncNoteCommentTotal2Redis(noteCommentTotalKey, dbCount)
);
}
// 若评论总数为 0则直接响应
if (count == 0) {
return PageResponse.success(null, pageNo, 0);
}
// 分页返回参数
@@ -144,113 +174,267 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
commentRspVOS = Lists.newArrayList();
// 计算分页查询的offset
long offset = PageResponse.getOffset(pageNo, pageSize);
//查询一级评论
List<CommentDO> oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize);
// 过滤出所有最早回复的二级评论ID
List<Long> twoLevelCommentIds = oneLevelCommentIds.stream()
.map(CommentDO::getFirstReplyCommentId)
.filter(e -> e != 0)
.toList();
// 查询二级评论
Map<Long, CommentDO> commentIdAndDOMap = null;
List<CommentDO> twoLevelCommentDOS = null;
if (CollUtil.isNotEmpty(twoLevelCommentIds)) {
twoLevelCommentDOS = commentDOMapper.selectTwoLevelCommentByIds(twoLevelCommentIds);
// 转Map方便后续数据拼接
commentIdAndDOMap = twoLevelCommentDOS.stream()
.collect(Collectors.toMap(CommentDO::getId, e -> e));
// 评论分页缓存使用 ZSET + STRING 实现
// 构建评论 ZSET Key
String commentZSetKey = RedisKeyConstants.buildCommentListKey(noteId);
// 先判断 ZSET 是否存在
boolean hasKey = redisTemplate.hasKey(commentZSetKey);
// 若不存在
if (!hasKey) {
// 异步将热点评论同步到 redis 中(最多同步 500 条)
threadPoolTaskExecutor.execute(() ->
syncHeatComments2Redis(commentZSetKey, noteId));
}
// 调用KV服务需要的入参
List<FindCommentContentReqDTO> findCommentContentReqDTOS = Lists.newArrayList();
// 调用用户服务需要的入参
List<Long> userIds = Lists.newArrayList();
// 若 ZSET 缓存存在, 并且查询的是前 50 页的评论
if (hasKey && offset < 500) {
// 使用 ZRevRange 获取某篇笔记下,按热度降序排序的一级评论 ID
Set<Object> commentIds = redisTemplate.opsForZSet()
.reverseRangeByScore(commentZSetKey, -Double.MAX_VALUE, Double.MAX_VALUE, offset, pageSize);
// 一二级评论合并到一起
List<CommentDO> allCommentDOS = Lists.newArrayList();
CollUtil.addAll(allCommentDOS, oneLevelCommentIds);
CollUtil.addAll(allCommentDOS, twoLevelCommentDOS);
// 若结果不为空
if (CollUtil.isNotEmpty(commentIds)) {
// Set 转 List
List<Object> commentIdList = Lists.newArrayList(commentIds);
// 循环提取RPC需要的入参数据
allCommentDOS.forEach(commentDO -> {
// 构建KV服务批量查询评论内容的入参
boolean isContentEmpty = commentDO.getIsContentEmpty();
if (!isContentEmpty) {
FindCommentContentReqDTO findCommentContentReqDTO = FindCommentContentReqDTO.builder()
.contentId(commentDO.getContentUuid())
.yearMonth(DateConstants.DATE_FORMAT_Y_M.format(commentDO.getCreateTime()))
.build();
findCommentContentReqDTOS.add(findCommentContentReqDTO);
}
// 构建 MGET 批量查询评论详情的 Key 集合
List<String> commentIdKeys = commentIdList.stream()
.map(RedisKeyConstants::buildCommentDetailKey)
.toList();
// 构建用户服务批量查询用户信息的入参
userIds.add(commentDO.getUserId());
});
// MGET 批量获取评论数据
List<Object> commentsJsonList = redisTemplate.opsForValue().multiGet(commentIdKeys);
// RPC: 调用KV服务批量查询评论内容
List<FindCommentContentRspDTO> findCommentContentRspDTOS = keyValueRpcService.batchFindCommentContent(noteId, findCommentContentReqDTOS);
// DTO转Map方便后续数据拼接
Map<String, String> commentUuidAndContentMap = null;
if (CollUtil.isNotEmpty(findCommentContentRspDTOS)) {
commentUuidAndContentMap = findCommentContentRspDTOS.stream()
.collect(Collectors.toMap(FindCommentContentRspDTO::getContentId, FindCommentContentRspDTO::getContent));
}
// RPC: 调用用户服务批量查询用户信息
List<FindUserByIdRspDTO> findUserByIdRspDTOS = userRpcService.findByIds(userIds);
// DTO转Map方便后续数据拼接
Map<Long, FindUserByIdRspDTO> userIdAndDTOMap = null;
if (CollUtil.isNotEmpty(findUserByIdRspDTOS)) {
userIdAndDTOMap = findUserByIdRspDTOS.stream()
.collect(Collectors.toMap(FindUserByIdRspDTO::getId, e -> e));
}
// DO转VO组装一二级评论数据
for (CommentDO commentDO : oneLevelCommentIds) {
// 一级评论
Long userId = commentDO.getUserId();
FindCommentItemRspVO oneLevelCommentRspVO = FindCommentItemRspVO.builder()
.userId(userId)
.commentId(commentDO.getId())
.imageUrl(commentDO.getImageUrl())
.createTime(DateUtils.formatRelativeTime(commentDO.getCreateTime()))
.likeTotal(commentDO.getLikeTotal())
.childCommentTotal(commentDO.getChildCommentTotal())
.build();
// 用户信息
if (userIdAndDTOMap != null) {
setUserInfo(userIdAndDTOMap, userId, oneLevelCommentRspVO);
}
// 笔记内容
setCommentContent(commentUuidAndContentMap, commentDO, oneLevelCommentRspVO);
// 二级评论
Long firstReplyCommentId = commentDO.getFirstReplyCommentId();
if (CollUtil.isNotEmpty(commentIdAndDOMap)) {
CommentDO firstReplyCommentDO = commentIdAndDOMap.get(firstReplyCommentId);
if (Objects.nonNull(firstReplyCommentDO)) {
Long firstReplyCommentUserId = firstReplyCommentDO.getUserId();
FindCommentItemRspVO firstReplyCommentRspVO = FindCommentItemRspVO.builder()
.userId(firstReplyCommentDO.getUserId())
.commentId(firstReplyCommentDO.getId())
.imageUrl(firstReplyCommentDO.getImageUrl())
.createTime(DateUtils.formatRelativeTime(firstReplyCommentDO.getCreateTime()))
.likeTotal(firstReplyCommentDO.getLikeTotal())
.build();
if (userIdAndDTOMap != null) {
setUserInfo(userIdAndDTOMap, firstReplyCommentUserId, firstReplyCommentRspVO);
// 可能存在部分评论不在缓存中,已经过期被删除,这些评论 ID 需要提取出来,等会查数据库
List<Long> expiredCommentIds = Lists.newArrayList();
for (int i = 0; i < commentsJsonList.size(); i++) {
String commentJson = (String) commentsJsonList.get(i);
if (Objects.nonNull(commentJson)) {
// 缓存中存在的评论 Json直接转换为 VO 添加到返参集合中
FindCommentItemRspVO commentRspVO = JsonUtils.parseObject(commentJson, FindCommentItemRspVO.class);
commentRspVOS.add(commentRspVO);
} else {
// 评论失效,添加到失效评论列表
expiredCommentIds.add(Long.valueOf(commentIdList.get(i).toString()));
}
}
// 用户信息
oneLevelCommentRspVO.setFirstReplyComment(firstReplyCommentRspVO);
// 笔记内容
setCommentContent(commentUuidAndContentMap, firstReplyCommentDO, firstReplyCommentRspVO);
// 对于不存在的一级评论,需要批量从数据库中查询,并添加到 commentRspVOS 中
if (CollUtil.isNotEmpty(expiredCommentIds)) {
List<CommentDO> commentDOS = commentDOMapper.selectByCommentIds(expiredCommentIds);
getCommentDataAndSync2Redis(commentDOS, noteId, commentRspVOS);
}
}
commentRspVOS.add(oneLevelCommentRspVO);
// 按热度值进行降序排列
commentRspVOS = commentRspVOS.stream()
.sorted(Comparator.comparing(FindCommentItemRspVO::getHeat).reversed())
.collect(Collectors.toList());
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
}
// TODO 后续逻辑
// 缓存中没有,则查询数据库
//查询一级评论
List<CommentDO> oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize);
getCommentDataAndSync2Redis(oneLevelCommentIds, noteId, commentRspVOS);
}
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
}
/**
* 获取一级评论数据,并同步到 Redis 中
*
* @param oneLevelCommentIds 一级评论ID列表
* @param noteId 笔记ID
* @param commentRspVOS 一级评论返回参数
*/
private void getCommentDataAndSync2Redis(List<CommentDO> oneLevelCommentIds, Long noteId, List<FindCommentItemRspVO> commentRspVOS) {
// 过滤出所有最早回复的二级评论ID
List<Long> twoLevelCommentIds = oneLevelCommentIds.stream()
.map(CommentDO::getFirstReplyCommentId)
.filter(e -> e != 0)
.toList();
// 查询二级评论
Map<Long, CommentDO> commentIdAndDOMap = null;
List<CommentDO> twoLevelCommentDOS = null;
if (CollUtil.isNotEmpty(twoLevelCommentIds)) {
twoLevelCommentDOS = commentDOMapper.selectTwoLevelCommentByIds(twoLevelCommentIds);
// 转Map方便后续数据拼接
commentIdAndDOMap = twoLevelCommentDOS.stream()
.collect(Collectors.toMap(CommentDO::getId, e -> e));
}
// 调用KV服务需要的入参
List<FindCommentContentReqDTO> findCommentContentReqDTOS = Lists.newArrayList();
// 调用用户服务需要的入参
List<Long> userIds = Lists.newArrayList();
// 一二级评论合并到一起
List<CommentDO> allCommentDOS = Lists.newArrayList();
CollUtil.addAll(allCommentDOS, oneLevelCommentIds);
CollUtil.addAll(allCommentDOS, twoLevelCommentDOS);
// 循环提取RPC需要的入参数据
allCommentDOS.forEach(commentDO -> {
// 构建KV服务批量查询评论内容的入参
boolean isContentEmpty = commentDO.getIsContentEmpty();
if (!isContentEmpty) {
FindCommentContentReqDTO findCommentContentReqDTO = FindCommentContentReqDTO.builder()
.contentId(commentDO.getContentUuid())
.yearMonth(DateConstants.DATE_FORMAT_Y_M.format(commentDO.getCreateTime()))
.build();
findCommentContentReqDTOS.add(findCommentContentReqDTO);
}
// 构建用户服务批量查询用户信息的入参
userIds.add(commentDO.getUserId());
});
// RPC: 调用KV服务批量查询评论内容
List<FindCommentContentRspDTO> findCommentContentRspDTOS = keyValueRpcService.batchFindCommentContent(noteId, findCommentContentReqDTOS);
// DTO转Map方便后续数据拼接
Map<String, String> commentUuidAndContentMap = null;
if (CollUtil.isNotEmpty(findCommentContentRspDTOS)) {
commentUuidAndContentMap = findCommentContentRspDTOS.stream()
.collect(Collectors.toMap(FindCommentContentRspDTO::getContentId, FindCommentContentRspDTO::getContent));
}
// RPC: 调用用户服务批量查询用户信息
List<FindUserByIdRspDTO> findUserByIdRspDTOS = userRpcService.findByIds(userIds);
// DTO转Map方便后续数据拼接
Map<Long, FindUserByIdRspDTO> userIdAndDTOMap = null;
if (CollUtil.isNotEmpty(findUserByIdRspDTOS)) {
userIdAndDTOMap = findUserByIdRspDTOS.stream()
.collect(Collectors.toMap(FindUserByIdRspDTO::getId, e -> e));
}
// DO转VO组装一二级评论数据
for (CommentDO commentDO : oneLevelCommentIds) {
// 一级评论
Long userId = commentDO.getUserId();
FindCommentItemRspVO oneLevelCommentRspVO = FindCommentItemRspVO.builder()
.userId(userId)
.commentId(commentDO.getId())
.imageUrl(commentDO.getImageUrl())
.createTime(DateUtils.formatRelativeTime(commentDO.getCreateTime()))
.likeTotal(commentDO.getLikeTotal())
.childCommentTotal(commentDO.getChildCommentTotal())
.heat(commentDO.getHeat())
.build();
// 用户信息
if (userIdAndDTOMap != null) {
setUserInfo(userIdAndDTOMap, userId, oneLevelCommentRspVO);
}
// 笔记内容
setCommentContent(commentUuidAndContentMap, commentDO, oneLevelCommentRspVO);
// 二级评论
Long firstReplyCommentId = commentDO.getFirstReplyCommentId();
if (CollUtil.isNotEmpty(commentIdAndDOMap)) {
CommentDO firstReplyCommentDO = commentIdAndDOMap.get(firstReplyCommentId);
if (Objects.nonNull(firstReplyCommentDO)) {
Long firstReplyCommentUserId = firstReplyCommentDO.getUserId();
FindCommentItemRspVO firstReplyCommentRspVO = FindCommentItemRspVO.builder()
.userId(firstReplyCommentDO.getUserId())
.commentId(firstReplyCommentDO.getId())
.imageUrl(firstReplyCommentDO.getImageUrl())
.createTime(DateUtils.formatRelativeTime(firstReplyCommentDO.getCreateTime()))
.likeTotal(firstReplyCommentDO.getLikeTotal())
.heat(firstReplyCommentDO.getHeat())
.build();
if (userIdAndDTOMap != null) {
setUserInfo(userIdAndDTOMap, firstReplyCommentUserId, firstReplyCommentRspVO);
}
// 用户信息
oneLevelCommentRspVO.setFirstReplyComment(firstReplyCommentRspVO);
// 笔记内容
setCommentContent(commentUuidAndContentMap, firstReplyCommentDO, firstReplyCommentRspVO);
}
}
commentRspVOS.add(oneLevelCommentRspVO);
}
// 异步将笔记详情,同步到 Redis 中
threadPoolTaskExecutor.execute(() -> {
// 准备批量写入的数据
Map<String, String> data = Maps.newHashMap();
commentRspVOS.forEach(commentRspVO -> {
// 评论 ID
Long commentId = commentRspVO.getCommentId();
// 构建 Key
String key = RedisKeyConstants.buildCommentDetailKey(commentId);
data.put(key, JsonUtils.toJsonString(commentRspVO));
});
// 使用 Redis Pipeline 提升写入性能
redisTemplate.executePipelined((RedisCallback<?>) (connection) -> {
for (Map.Entry<String, String> entry : data.entrySet()) {
// 将 Java 对象序列化为 JSON 字符串
String jsonStr = JsonUtils.toJsonString(entry.getValue());
// 随机生成过期时间 (5小时以内)
int randomExpire = RandomUtil.randomInt(5 * 60 * 60);
// 批量写入并设置过期时间
connection.setEx(
redisTemplate.getStringSerializer().serialize(entry.getKey()),
randomExpire,
redisTemplate.getStringSerializer().serialize(jsonStr)
);
}
return null;
});
});
}
/**
* 同步笔记评论总数到 Redis 中
*
* @param noteCommentTotalKey 笔记评论总数 Redis key
* @param dbCount 数据库查询到的笔记评论总数
*/
private void syncNoteCommentTotal2Redis(String noteCommentTotalKey, Long dbCount) {
redisTemplate.executePipelined(new SessionCallback<>() {
@Override
public Object execute(RedisOperations operations) {
// 同步 hash 数据
operations.opsForHash().put(noteCommentTotalKey, RedisKeyConstants.FIELD_COMMENT_TOTAL, dbCount);
// 随机过期时间 (保底1小时 + 随机时间),单位:秒
long expireTime = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60);
operations.expire(noteCommentTotalKey, expireTime, TimeUnit.SECONDS);
return null;
}
});
}
/**
* 同步热点评论至 Redis
*
* @param key 热门评论 Redis key
* @param noteId 笔记 ID
*/
private void syncHeatComments2Redis(String key, Long noteId) {
List<CommentDO> commentDOS = commentDOMapper.selectHeatComments(noteId);
if (CollUtil.isNotEmpty(commentDOS)) {
// 使用 Redis Pipeline 提升写入性能
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
ZSetOperations<String, Object> zSetOps = redisTemplate.opsForZSet();
// 遍历评论数据并批量写入 ZSet
for (CommentDO commentDO : commentDOS) {
Long commentId = commentDO.getId();
Double commentHeat = commentDO.getHeat();
zSetOps.add(key, commentId, commentHeat);
}
// 设置随机过期时间,单位:秒
int randomExpiryTime = RandomUtil.randomInt(5 * 60 * 60); // 5小时以内
redisTemplate.expire(key, randomExpiryTime, TimeUnit.SECONDS);
return null; // 无返回值
});
}
}
}

View File

@@ -20,7 +20,7 @@
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="child_comment_total" jdbcType="BIGINT" property="childCommentTotal"/>
<result column="heat" jdbcType="DECIMAL" property="heat"/>
<result column="heat" jdbcType="DOUBLE" property="heat"/>
<result column="first_reply_comment_id" jdbcType="BIGINT" property="firstReplyCommentId"/>
</resultMap>
<sql id="Base_Column_List">
@@ -45,21 +45,28 @@
first_reply_comment_id
</sql>
<select id="selectByCommentIds" parameterType="list" resultMap="BaseResultMap">
<select id="selectByCommentIds" resultMap="BaseResultMap" parameterType="list">
select id,
user_id,
content_uuid,
is_content_empty,
image_url,
like_total,
is_top,
create_time,
first_reply_comment_id,
child_comment_total,
level,
parent_id,
user_id,
child_comment_total,
like_total,
first_reply_comment_id
heat
from t_comment
where id in
<foreach close=")" collection="commentIds" item="commentId" open="(" separator=",">
<foreach collection="commentIds" open="(" separator="," close=")" item="commentId">
#{commentId}
</foreach>
</select>
<insert id="batchInsert" parameterType="list">
insert IGNORE into t_comment (id, note_id, user_id,
content_uuid, is_content_empty, image_url,
@@ -113,7 +120,8 @@
is_top,
create_time,
first_reply_comment_id,
child_comment_total
child_comment_total,
heat
from t_comment
where note_id = #{noteId}
and level = 1
@@ -128,11 +136,21 @@
is_content_empty,
image_url,
like_total,
create_time
create_time,
heat
from t_comment
where id in
<foreach collection="commentIds" open="(" separator="," close=")" item="commentId">
#{commentId}
</foreach>
</select>
<select id="selectHeatComments" resultMap="BaseResultMap">
select id, heat
from t_comment
where note_id = #{noteId}
and level = 1
order by heat desc
limit 500
</select>
</mapper>