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 0b3fa91..fce6f49 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,19 @@ public class RedisKeyConstants { */ private static final String HAVE_FIRST_REPLY_COMMENT_KEY_PREFIX = "comment:havaFirstReplyCommentId:"; + /** + * Hash Field: 子评论总数 + */ + public static final String FIELD_CHILD_COMMENT_TOTAL = "childCommentTotal"; + /** + * Hash Field: 点赞总数 + */ + public static final String FIELD_LIKE_TOTAL = "likeTotal"; + /** + * 评论维度计数 Key 前缀 + */ + private static final String COUNT_COMMENT_KEY_PREFIX = "count:comment:"; + /** * Hash Field 键:评论总数 */ @@ -27,6 +40,15 @@ public class RedisKeyConstants { */ private static final String COMMENT_DETAIL_KEY_PREFIX = "comment:detail:"; + /** + * 构建评论维度计数 Key + * + * @param commentId 评论 ID + * @return 评论维度计数 Key + */ + public static String buildCountCommentKey(Long commentId) { + return COUNT_COMMENT_KEY_PREFIX + commentId; + } /** * 构建完整 KEY 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 c69b38d..1298764 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 @@ -102,4 +102,12 @@ public interface CommentDOMapper extends BaseMapper { List selectChildPageList(@Param("parentId") Long parentId, @Param("offset") long offset, @Param("pageSize") long pageSize); + + /** + * 批量查询计数数据 + * + * @param commentIds 评论 ID 列表 + * @return 计数数据 + */ + List selectCommentCountByIds(@Param("commentIds") List commentIds); } \ 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 29ec7a2..5c1a68f 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 @@ -21,6 +21,7 @@ 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.CommentLevelEnum; 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.*; @@ -37,9 +38,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; import org.jspecify.annotations.NonNull; -import org.springframework.data.redis.core.RedisCallback; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ZSetOperations; +import org.springframework.data.redis.core.*; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; @@ -212,7 +211,7 @@ public class CommentServiceImpl extends ServiceImpl // 先查询本地缓存 // 新建一个集合用于存储本地缓存中不存在的评论ID - List localeCacheExpiredCommentIds = Lists.newArrayList(); + List localCacheExpiredCommentIds = Lists.newArrayList(); // 构建本地缓存的key集合 List localCacheKeys = commentIdList.stream() @@ -225,7 +224,7 @@ public class CommentServiceImpl extends ServiceImpl Map missingData = Maps.newHashMap(); missingKeys.forEach(key -> { // 记录缓存中不存在的ID - localeCacheExpiredCommentIds.add(key); + localCacheExpiredCommentIds.add(key); // 不存在的评论详情,对其Value设置为空字符串 missingData.put(key, Strings.EMPTY); }); @@ -233,7 +232,7 @@ public class CommentServiceImpl extends ServiceImpl }); // 如果localCacheExpiredCommentIds的大小不等于commentIdList的大小,说明本地缓存中有数据 - if (CollUtil.size(localeCacheExpiredCommentIds) != commentIdList.size()) { + if (CollUtil.size(localCacheExpiredCommentIds) != commentIdList.size()) { // 将本地缓存中的评论详情Json转为实体类添加到VO返参集合中 for (String value : commentIdAndDetailJsonMap.values()) { if (StringUtils.isBlank(value)) continue; @@ -243,12 +242,16 @@ public class CommentServiceImpl extends ServiceImpl } // 如果localCacheExpiredCommentIds大小为0,说明评论详情全在本地缓存中,直接响应返参 - if (CollUtil.size(localeCacheExpiredCommentIds) == 0) { + if (CollUtil.size(localCacheExpiredCommentIds) == 0) { + // 计数数据需要从 Redis 中查 + if (CollUtil.isNotEmpty(commentRspVOS)) { + setCommentCountData(commentRspVOS, localCacheExpiredCommentIds); + } return PageResponse.success(commentRspVOS, pageNo, count, pageSize); } // 构建 MGET 批量查询评论详情的 Key 集合 - List commentIdKeys = localeCacheExpiredCommentIds.stream() + List commentIdKeys = localCacheExpiredCommentIds.stream() .map(RedisKeyConstants::buildCommentDetailKey) .toList(); @@ -635,4 +638,142 @@ public class CommentServiceImpl extends ServiceImpl }); } } + + /** + * 设置评论 VO 的计数 + * + * @param commentRspVOS 返参 VO 集合 + * @param expiredCommentIds 缓存中已失效的评论 ID 集合 + */ + private void setCommentCountData(List commentRspVOS, + List expiredCommentIds) { + // 准备从评论 Hash 中查询计数 (子评论总数、被点赞数) + // 缓存中存在的评论 ID + List notExpiredCommentIds = Lists.newArrayList(); + + // 遍历从缓存中解析出的 VO 集合,提取一级、二级评论 ID + commentRspVOS.forEach(commentRspVO -> { + Long oneLevelCommentId = commentRspVO.getCommentId(); + notExpiredCommentIds.add(oneLevelCommentId); + FindCommentItemRspVO firstCommentVO = commentRspVO.getFirstReplyComment(); + if (Objects.nonNull(firstCommentVO)) { + notExpiredCommentIds.add(firstCommentVO.getCommentId()); + } + }); + + // 已失效的 Hash 评论 ID + List expiredCountCommentIds = Lists.newArrayList(); + // 构建需要查询的 Hash Key 集合 + List commentCountKeys = notExpiredCommentIds.stream() + .map(RedisKeyConstants::buildCountCommentKey).toList(); + + // 使用 RedisTemplate 执行管道批量操作 + List results = redisTemplate.executePipelined(new SessionCallback<>() { + @Override + public Object execute(@NonNull RedisOperations operations) { + // 遍历需要查询的评论计数的 Hash 键集合 + commentCountKeys.forEach(key -> + // 在管道中执行 Redis 的 hash.entries 操作 + // 此操作会获取指定 Hash 键中所有的字段和值 + operations.opsForHash().entries(key)); + return null; + } + }); + + // 评论 ID - 计数数据字典 + Map> commentIdAndCountMap = Maps.newHashMap(); + // 遍历未过期的评论 ID 集合 + for (int i = 0; i < notExpiredCommentIds.size(); i++) { + // 当前评论 ID + Long currCommentId = Long.valueOf(notExpiredCommentIds.get(i).toString()); + // 从缓存查询结果中,获取对应 Hash + Map hash = (Map) results.get(i); + // 若 Hash 结果为空,说明缓存中不存在,添加到 expiredCountCommentIds 中,保存一下 + if (CollUtil.isEmpty(hash)) { + expiredCountCommentIds.add(currCommentId); + continue; + } + // 若存在,则将数据添加到 commentIdAndCountMap 中,方便后续读取 + commentIdAndCountMap.put(currCommentId, hash); + } + + // 若已过期的计数评论 ID 集合大于 0,说明部分计数数据不在 Redis 缓存中 + // 需要查询数据库,并将这部分的评论计数 Hash 同步到 Redis 中 + if (CollUtil.size(expiredCountCommentIds) > 0) { + // 查询数据库 + List commentDOS = commentDOMapper.selectCommentCountByIds(expiredCountCommentIds); + + commentDOS.forEach(commentDO -> { + Integer level = commentDO.getLevel(); + Map map = Maps.newHashMap(); + map.put(RedisKeyConstants.FIELD_LIKE_TOTAL, commentDO.getLikeTotal()); + // 只有一级评论需要统计子评论总数 + if (Objects.equals(level, CommentLevelEnum.ONE.getCode())) { + map.put(RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, commentDO.getChildCommentTotal()); + } + // 统一添加到 commentIdAndCountMap 字典中,方便后续查询 + commentIdAndCountMap.put(commentDO.getId(), map); + }); + + // 异步同步到 Redis 中 + threadPoolTaskExecutor.execute(() -> { + redisTemplate.executePipelined(new SessionCallback<>() { + @Override + public Object execute(RedisOperations operations) { + commentDOS.forEach(commentDO -> { + // 构建 Hash Key + String key = RedisKeyConstants.buildCountCommentKey(commentDO.getId()); + // 评论级别 + Integer level = commentDO.getLevel(); + // 设置 Field 数据 + Map fieldsMap = Objects.equals(level, CommentLevelEnum.ONE.getCode()) ? + Map.of(RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, commentDO.getChildCommentTotal(), + RedisKeyConstants.FIELD_LIKE_TOTAL, commentDO.getLikeTotal()) : Map.of(RedisKeyConstants.FIELD_LIKE_TOTAL, commentDO.getLikeTotal()); + // 添加 Hash 数据 + operations.opsForHash().putAll(key, fieldsMap); + + // 设置随机过期时间 (5小时以内) + long expireTime = RandomUtil.randomInt(5 * 60 * 60); + operations.expire(key, expireTime, TimeUnit.SECONDS); + }); + return null; + } + }); + }); + } + + // 遍历 VO, 设置对应评论的二级评论数、点赞数 + for (FindCommentItemRspVO commentRspVO : commentRspVOS) { + // 评论 ID + Long commentId = commentRspVO.getCommentId(); + + // 若当前这条评论是从数据库中查询出来的, 则无需设置二级评论数、点赞数,以数据库查询出来的为主 + if (CollUtil.isNotEmpty(expiredCommentIds) + && expiredCommentIds.contains(commentId)) { + continue; + } + + // 设置一级评论的子评论总数、点赞数 + Map hash = commentIdAndCountMap.get(commentId); + if (CollUtil.isNotEmpty(hash)) { + Object childCommentTotalObj = hash.get(RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL); + Long childCommentTotal = Objects.isNull(childCommentTotalObj) ? 0 : Long.parseLong(childCommentTotalObj.toString()); + Object likeTotalObj = hash.get(RedisKeyConstants.FIELD_LIKE_TOTAL); + Long likeTotal = Objects.isNull(likeTotalObj) ? 0 : Long.parseLong(likeTotalObj.toString()); + commentRspVO.setChildCommentTotal(childCommentTotal); + commentRspVO.setLikeTotal(likeTotal); + // 最初回复的二级评论 + FindCommentItemRspVO firstCommentVO = commentRspVO.getFirstReplyComment(); + if (Objects.nonNull(firstCommentVO)) { + Long firstCommentId = firstCommentVO.getCommentId(); + Map firstCommentHash = commentIdAndCountMap.get(firstCommentId); + if (CollUtil.isNotEmpty(firstCommentHash)) { + Long firstCommentLikeTotal = Long.valueOf(firstCommentHash.get(RedisKeyConstants.FIELD_LIKE_TOTAL).toString()); + firstCommentVO.setLikeTotal(firstCommentLikeTotal); + } + } + } + } + } + } 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 8a80971..55a61f4 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 @@ -180,4 +180,16 @@ order by id limit #{offset}, #{pageSize} + + \ No newline at end of file diff --git a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/constant/RedisKeyConstants.java b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/constant/RedisKeyConstants.java index 29b6284..1e64417 100644 --- a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/constant/RedisKeyConstants.java +++ b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/constant/RedisKeyConstants.java @@ -36,6 +36,25 @@ public class RedisKeyConstants { */ public static final String FIELD_COLLECT_TOTAL = "collectTotal"; + /** + * Hash Field: 子评论总数 + */ + public static final String FIELD_CHILD_COMMENT_TOTAL = "childCommentTotal"; + /** + * 评论维度计数 Key 前缀 + */ + private static final String COUNT_COMMENT_KEY_PREFIX = "count:comment:"; + + /** + * 构建评论维度计数 Key + * + * @param commentId 评论ID + * @return 评论维度计数 Key + */ + public static String buildCountCommentKey(Long commentId) { + return COUNT_COMMENT_KEY_PREFIX + commentId; + } + /** * 构建用户维度计数 Key * diff --git a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteChildCommentConsumer.java b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteChildCommentConsumer.java index 813d5ed..9531815 100644 --- a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteChildCommentConsumer.java +++ b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteChildCommentConsumer.java @@ -5,6 +5,7 @@ import com.github.phantomthief.collection.BufferTrigger; import com.google.common.collect.Lists; import com.hanserwei.framework.common.utils.JsonUtils; import com.hanserwei.hannote.count.biz.constant.MQConstants; +import com.hanserwei.hannote.count.biz.constant.RedisKeyConstants; import com.hanserwei.hannote.count.biz.domain.mapper.CommentDOMapper; import com.hanserwei.hannote.count.biz.enums.CommentLevelEnum; import com.hanserwei.hannote.count.biz.model.dto.CountPublishCommentMqDTO; @@ -15,6 +16,7 @@ import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Component; @@ -39,6 +41,9 @@ public class CountNoteChildCommentConsumer implements RocketMQListener { @Resource private CommentDOMapper commentDOMapper; + @Resource + private RedisTemplate redisTemplate; + private final BufferTrigger bufferTrigger = BufferTrigger.batchBlocking() .bufferSize(50000) // 缓存队列的最大容量 .batchSize(1000) // 一批次最多聚合 1000 条 @@ -82,6 +87,19 @@ public class CountNoteChildCommentConsumer implements RocketMQListener { // 评论数 int count = CollUtil.size(entry.getValue()); + // 更新 Redis 缓存中的评论计数数据 + // 构建 Key + String commentCountHashKey = RedisKeyConstants.buildCountCommentKey(parentId); + // 判断 Hash 是否存在 + boolean hasKey = redisTemplate.hasKey(commentCountHashKey); + + // 若 Hash 存在,则更新子评论总数 + if (hasKey) { + // 累加 + redisTemplate.opsForHash() + .increment(commentCountHashKey, RedisKeyConstants.FIELD_CHILD_COMMENT_TOTAL, count); + } + // 更新一级评论的下级评论总数,进行累加操作 commentDOMapper.updateChildCommentTotal(parentId, count); } diff --git a/http-client/gateApi.http b/http-client/gateApi.http index d3c0382..606d0ec 100644 --- a/http-client/gateApi.http +++ b/http-client/gateApi.http @@ -298,9 +298,9 @@ Authorization: Bearer {{token}} { "noteId": 1862481582414102549, - "content": "这是一条测试同步Redis并更新热度的评论", + "content": "这是一条测试同步Redis更新计数的评论", "imageUrl": "https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg", - "replyCommentId": 8001 + "replyCommentId": 4002 } ### 批量添加评论