@@ -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 > locale CacheExpiredCommentIds = 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
locale CacheExpiredCommentIds . 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 ( locale CacheExpiredCommentIds ) ! = 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 ( locale CacheExpiredCommentIds ) = = 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 = locale CacheExpiredCommentIds . 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 ) ;
}
}
}
}
}
}