diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 70f0f46..5bc28bd 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -6,6 +6,7 @@ hannote hanserwei jobhandler + mget nacos operationlog rustfs diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java index 1687bf4..0b3fa91 100644 --- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/constants/RedisKeyConstants.java @@ -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; + } + } \ No newline at end of file diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java index 6ac1db6..2f0591d 100644 --- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/dataobject/CommentDO.java @@ -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 (只有一级评论需要) diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java index d1e5327..e3cc5ef 100644 --- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/domain/mapper/CommentDOMapper.java @@ -74,4 +74,12 @@ public interface CommentDOMapper extends BaseMapper { * @return 二级评论 */ List selectTwoLevelCommentByIds(@Param("commentIds") List commentIds); + + /** + * 查询热门评论 + * + * @param noteId 笔记 ID + * @return 热门评论 + */ + List selectHeatComments(Long noteId); } \ No newline at end of file diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java index bc19064..d042c59 100644 --- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/enums/ResponseCodeEnum.java @@ -13,6 +13,7 @@ public enum ResponseCodeEnum implements BaseExceptionInterface { PARAM_NOT_VALID("COMMENT-10001", "参数错误"), // ----------- 业务异常状态码 ----------- + COMMENT_NOT_FOUND("COMMENT-20001", "此评论不存在"), ; // 异常码 diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java index ed542ec..2626db3 100644 --- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/model/vo/FindCommentItemRspVO.java @@ -61,4 +61,9 @@ public class FindCommentItemRspVO { */ private FindCommentItemRspVO firstReplyComment; + /** + * 热度值 + */ + private Double heat; + } \ No newline at end of file diff --git a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java index e933cb8..3abda15 100644 --- a/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java +++ b/han-note-comment/han-note-comment-biz/src/main/java/com/hanserwei/hannote/comment/biz/service/impl/CommentServiceImpl.java @@ -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 private KeyValueRpcService keyValueRpcService; @Resource private UserRpcService userRpcService; + @Resource + private RedisTemplate redisTemplate; + @Resource(name = "taskExecutor") + private ThreadPoolTaskExecutor threadPoolTaskExecutor; @Override public Response publishComment(PublishCommentReqVO publishCommentReqVO) { @@ -127,13 +137,33 @@ public class CommentServiceImpl extends ServiceImpl // 每页展示一级评论数量 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 commentRspVOS = Lists.newArrayList(); // 计算分页查询的offset long offset = PageResponse.getOffset(pageNo, pageSize); - //查询一级评论 - List oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize); - // 过滤出所有最早回复的二级评论ID - List twoLevelCommentIds = oneLevelCommentIds.stream() - .map(CommentDO::getFirstReplyCommentId) - .filter(e -> e != 0) - .toList(); - // 查询二级评论 - Map commentIdAndDOMap = null; - List 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 findCommentContentReqDTOS = Lists.newArrayList(); - // 调用用户服务需要的入参 - List userIds = Lists.newArrayList(); + // 若 ZSET 缓存存在, 并且查询的是前 50 页的评论 + if (hasKey && offset < 500) { + // 使用 ZRevRange 获取某篇笔记下,按热度降序排序的一级评论 ID + Set commentIds = redisTemplate.opsForZSet() + .reverseRangeByScore(commentZSetKey, -Double.MAX_VALUE, Double.MAX_VALUE, offset, pageSize); - // 一二级评论合并到一起 - List allCommentDOS = Lists.newArrayList(); - CollUtil.addAll(allCommentDOS, oneLevelCommentIds); - CollUtil.addAll(allCommentDOS, twoLevelCommentDOS); + // 若结果不为空 + if (CollUtil.isNotEmpty(commentIds)) { + // Set 转 List + List 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 commentIdKeys = commentIdList.stream() + .map(RedisKeyConstants::buildCommentDetailKey) + .toList(); - // 构建用户服务批量查询用户信息的入参 - userIds.add(commentDO.getUserId()); - }); + // MGET 批量获取评论数据 + List commentsJsonList = redisTemplate.opsForValue().multiGet(commentIdKeys); - // RPC: 调用KV服务批量查询评论内容 - List findCommentContentRspDTOS = keyValueRpcService.batchFindCommentContent(noteId, findCommentContentReqDTOS); - // DTO转Map方便后续数据拼接 - Map commentUuidAndContentMap = null; - if (CollUtil.isNotEmpty(findCommentContentRspDTOS)) { - commentUuidAndContentMap = findCommentContentRspDTOS.stream() - .collect(Collectors.toMap(FindCommentContentRspDTO::getContentId, FindCommentContentRspDTO::getContent)); - } - - // RPC: 调用用户服务批量查询用户信息 - List findUserByIdRspDTOS = userRpcService.findByIds(userIds); - // DTO转Map方便后续数据拼接 - Map 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 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 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 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 oneLevelCommentIds, Long noteId, List commentRspVOS) { + // 过滤出所有最早回复的二级评论ID + List twoLevelCommentIds = oneLevelCommentIds.stream() + .map(CommentDO::getFirstReplyCommentId) + .filter(e -> e != 0) + .toList(); + // 查询二级评论 + Map commentIdAndDOMap = null; + List twoLevelCommentDOS = null; + if (CollUtil.isNotEmpty(twoLevelCommentIds)) { + twoLevelCommentDOS = commentDOMapper.selectTwoLevelCommentByIds(twoLevelCommentIds); + // 转Map方便后续数据拼接 + commentIdAndDOMap = twoLevelCommentDOS.stream() + .collect(Collectors.toMap(CommentDO::getId, e -> e)); + } + + // 调用KV服务需要的入参 + List findCommentContentReqDTOS = Lists.newArrayList(); + // 调用用户服务需要的入参 + List userIds = Lists.newArrayList(); + + // 一二级评论合并到一起 + List 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 findCommentContentRspDTOS = keyValueRpcService.batchFindCommentContent(noteId, findCommentContentReqDTOS); + // DTO转Map方便后续数据拼接 + Map commentUuidAndContentMap = null; + if (CollUtil.isNotEmpty(findCommentContentRspDTOS)) { + commentUuidAndContentMap = findCommentContentRspDTOS.stream() + .collect(Collectors.toMap(FindCommentContentRspDTO::getContentId, FindCommentContentRspDTO::getContent)); + } + + // RPC: 调用用户服务批量查询用户信息 + List findUserByIdRspDTOS = userRpcService.findByIds(userIds); + // DTO转Map方便后续数据拼接 + Map 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 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 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 commentDOS = commentDOMapper.selectHeatComments(noteId); + if (CollUtil.isNotEmpty(commentDOS)) { + // 使用 Redis Pipeline 提升写入性能 + redisTemplate.executePipelined((RedisCallback) connection -> { + ZSetOperations 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; // 无返回值 + }); + } + } } diff --git a/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml b/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml index 8834a21..4c5e1d6 100644 --- a/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml +++ b/han-note-comment/han-note-comment-biz/src/main/resources/mapperxml/CommentDOMapper.xml @@ -20,7 +20,7 @@ - + @@ -45,21 +45,28 @@ first_reply_comment_id - 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 - + #{commentId} + 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 #{commentId} + + \ No newline at end of file