feat(comment): 一级评论:子评论总数更新与查询
- 新增批量查询评论计数的数据库接口及SQL实现 - 优化本地缓存中评论ID失效判断逻辑,修正变量命名 - 增加从Redis中获取评论计数数据的功能,并支持缺失时回源数据库 - 实现评论计数数据异步同步至Redis的逻辑,包括子评论总数和点赞数 - 在消费端增加更新Redis中评论子评论总数的逻辑 - 添加评论计数相关的Redis Key和Field常量定义 - 更新HTTP测试用例中的评论内容和回复ID,验证计数同步功能
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -102,4 +102,12 @@ public interface CommentDOMapper extends BaseMapper<CommentDO> {
|
||||
List<CommentDO> selectChildPageList(@Param("parentId") Long parentId,
|
||||
@Param("offset") long offset,
|
||||
@Param("pageSize") long pageSize);
|
||||
|
||||
/**
|
||||
* 批量查询计数数据
|
||||
*
|
||||
* @param commentIds 评论 ID 列表
|
||||
* @return 计数数据
|
||||
*/
|
||||
List<CommentDO> selectCommentCountByIds(@Param("commentIds") List<Long> commentIds);
|
||||
}
|
||||
@@ -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<CommentDOMapper, CommentDO>
|
||||
|
||||
// 先查询本地缓存
|
||||
// 新建一个集合用于存储本地缓存中不存在的评论ID
|
||||
List<Long> localeCacheExpiredCommentIds = Lists.newArrayList();
|
||||
List<Long> localCacheExpiredCommentIds = Lists.newArrayList();
|
||||
|
||||
// 构建本地缓存的key集合
|
||||
List<Long> localCacheKeys = commentIdList.stream()
|
||||
@@ -225,7 +224,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
||||
Map<Long, String> 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<CommentDOMapper, CommentDO>
|
||||
});
|
||||
|
||||
// 如果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<CommentDOMapper, CommentDO>
|
||||
}
|
||||
|
||||
// 如果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<String> commentIdKeys = localeCacheExpiredCommentIds.stream()
|
||||
List<String> commentIdKeys = localCacheExpiredCommentIds.stream()
|
||||
.map(RedisKeyConstants::buildCommentDetailKey)
|
||||
.toList();
|
||||
|
||||
@@ -635,4 +638,142 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置评论 VO 的计数
|
||||
*
|
||||
* @param commentRspVOS 返参 VO 集合
|
||||
* @param expiredCommentIds 缓存中已失效的评论 ID 集合
|
||||
*/
|
||||
private void setCommentCountData(List<FindCommentItemRspVO> commentRspVOS,
|
||||
List<Long> expiredCommentIds) {
|
||||
// 准备从评论 Hash 中查询计数 (子评论总数、被点赞数)
|
||||
// 缓存中存在的评论 ID
|
||||
List<Long> 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<Long> expiredCountCommentIds = Lists.newArrayList();
|
||||
// 构建需要查询的 Hash Key 集合
|
||||
List<String> commentCountKeys = notExpiredCommentIds.stream()
|
||||
.map(RedisKeyConstants::buildCountCommentKey).toList();
|
||||
|
||||
// 使用 RedisTemplate 执行管道批量操作
|
||||
List<Object> 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<Long, Map<Object, Object>> commentIdAndCountMap = Maps.newHashMap();
|
||||
// 遍历未过期的评论 ID 集合
|
||||
for (int i = 0; i < notExpiredCommentIds.size(); i++) {
|
||||
// 当前评论 ID
|
||||
Long currCommentId = Long.valueOf(notExpiredCommentIds.get(i).toString());
|
||||
// 从缓存查询结果中,获取对应 Hash
|
||||
Map<Object, Object> hash = (Map<Object, Object>) 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<CommentDO> commentDOS = commentDOMapper.selectCommentCountByIds(expiredCountCommentIds);
|
||||
|
||||
commentDOS.forEach(commentDO -> {
|
||||
Integer level = commentDO.getLevel();
|
||||
Map<Object, Object> 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<String, Long> 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<Object, Object> 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<Object, Object> firstCommentHash = commentIdAndCountMap.get(firstCommentId);
|
||||
if (CollUtil.isNotEmpty(firstCommentHash)) {
|
||||
Long firstCommentLikeTotal = Long.valueOf(firstCommentHash.get(RedisKeyConstants.FIELD_LIKE_TOTAL).toString());
|
||||
firstCommentVO.setLikeTotal(firstCommentLikeTotal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -180,4 +180,16 @@
|
||||
order by id
|
||||
limit #{offset}, #{pageSize}
|
||||
</select>
|
||||
|
||||
<select id="selectCommentCountByIds" resultMap="BaseResultMap" parameterType="list">
|
||||
select id,
|
||||
child_comment_total,
|
||||
like_total,
|
||||
level
|
||||
from t_comment
|
||||
where id in
|
||||
<foreach collection="commentIds" open="(" separator="," close=")" item="commentId">
|
||||
#{commentId}
|
||||
</foreach>
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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<String> {
|
||||
@Resource
|
||||
private CommentDOMapper commentDOMapper;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private final BufferTrigger<String> bufferTrigger = BufferTrigger.<String>batchBlocking()
|
||||
.bufferSize(50000) // 缓存队列的最大容量
|
||||
.batchSize(1000) // 一批次最多聚合 1000 条
|
||||
@@ -82,6 +87,19 @@ public class CountNoteChildCommentConsumer implements RocketMQListener<String> {
|
||||
// 评论数
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
### 批量添加评论
|
||||
|
||||
Reference in New Issue
Block a user