feat(comment): 引入本地缓存优化评论查询性能
- 添加 Caffeine 依赖以支持本地缓存 - 实现评论详情的本地缓存机制,减少 Redis 查询压力 -优化分页查询逻辑,优先从本地缓存获取评论数据 - 异步同步评论详情至本地缓存,提升响应速度 - 简化 Redis 操作代码,提高可读性和维护性
This commit is contained in:
@@ -130,6 +130,12 @@
|
|||||||
<artifactId>han-note-user-api</artifactId>
|
<artifactId>han-note-user-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Caffeine 本地缓存 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.hanserwei.hannote.comment.biz.service.impl;
|
|||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
@@ -34,7 +36,11 @@ import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.data.redis.core.*;
|
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.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -64,6 +70,15 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
@Resource(name = "taskExecutor")
|
@Resource(name = "taskExecutor")
|
||||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论详情本地缓存
|
||||||
|
*/
|
||||||
|
private static final Cache<Long, String> LOCAL_CACHE = Caffeine.newBuilder()
|
||||||
|
.initialCapacity(10000) // 设置初始容量为 10000 个条目
|
||||||
|
.maximumSize(10000) // 设置缓存的最大容量为 10000 个条目
|
||||||
|
.expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时过期
|
||||||
|
.build();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<?> publishComment(PublishCommentReqVO publishCommentReqVO) {
|
public Response<?> publishComment(PublishCommentReqVO publishCommentReqVO) {
|
||||||
// 评论正文
|
// 评论正文
|
||||||
@@ -167,10 +182,8 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 分页返回参数
|
// 分页返回参数
|
||||||
List<FindCommentItemRspVO> commentRspVOS = null;
|
List<FindCommentItemRspVO> commentRspVOS;
|
||||||
|
|
||||||
// 若评论总数大于0
|
|
||||||
if (count > 0) {
|
|
||||||
commentRspVOS = Lists.newArrayList();
|
commentRspVOS = Lists.newArrayList();
|
||||||
// 计算分页查询的offset
|
// 计算分页查询的offset
|
||||||
long offset = PageResponse.getOffset(pageNo, pageSize);
|
long offset = PageResponse.getOffset(pageNo, pageSize);
|
||||||
@@ -198,8 +211,45 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
// Set 转 List
|
// Set 转 List
|
||||||
List<Object> commentIdList = Lists.newArrayList(commentIds);
|
List<Object> commentIdList = Lists.newArrayList(commentIds);
|
||||||
|
|
||||||
|
// 先查询本地缓存
|
||||||
|
// 新建一个集合用于存储本地缓存中不存在的评论ID
|
||||||
|
List<Long> localeCacheExpiredCommentIds = Lists.newArrayList();
|
||||||
|
|
||||||
|
// 构建本地缓存的key集合
|
||||||
|
List<Long> localCacheKeys = commentIdList.stream()
|
||||||
|
.map(e -> Long.valueOf(e.toString()))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// 批量查询本地缓存
|
||||||
|
Map<Long, @NonNull String> commentIdAndDetailJsonMap = LOCAL_CACHE.getAll(localCacheKeys, missingKeys -> {
|
||||||
|
// 对应本地缓存缺失的Key返回空字符串
|
||||||
|
Map<Long, String> missingData = Maps.newHashMap();
|
||||||
|
missingKeys.forEach(key -> {
|
||||||
|
// 记录缓存中不存在的ID
|
||||||
|
localeCacheExpiredCommentIds.add(key);
|
||||||
|
// 不存在的评论详情,对其Value设置为空字符串
|
||||||
|
missingData.put(key, Strings.EMPTY);
|
||||||
|
});
|
||||||
|
return missingData;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果localCacheExpiredCommentIds的大小不等于commentIdList的大小,说明本地缓存中有数据
|
||||||
|
if (CollUtil.size(localeCacheExpiredCommentIds) != commentIdList.size()) {
|
||||||
|
// 将本地缓存中的评论详情Json转为实体类添加到VO返参集合中
|
||||||
|
for (String value : commentIdAndDetailJsonMap.values()) {
|
||||||
|
if (StringUtils.isBlank(value)) continue;
|
||||||
|
FindCommentItemRspVO commentRspVO = JsonUtils.parseObject(value, FindCommentItemRspVO.class);
|
||||||
|
commentRspVOS.add(commentRspVO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果localCacheExpiredCommentIds大小为0,说明评论详情全在本地缓存中,直接响应返参
|
||||||
|
if (CollUtil.size(localeCacheExpiredCommentIds) == 0) {
|
||||||
|
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
// 构建 MGET 批量查询评论详情的 Key 集合
|
// 构建 MGET 批量查询评论详情的 Key 集合
|
||||||
List<String> commentIdKeys = commentIdList.stream()
|
List<String> commentIdKeys = localeCacheExpiredCommentIds.stream()
|
||||||
.map(RedisKeyConstants::buildCommentDetailKey)
|
.map(RedisKeyConstants::buildCommentDetailKey)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@@ -208,6 +258,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
|
|
||||||
// 可能存在部分评论不在缓存中,已经过期被删除,这些评论 ID 需要提取出来,等会查数据库
|
// 可能存在部分评论不在缓存中,已经过期被删除,这些评论 ID 需要提取出来,等会查数据库
|
||||||
List<Long> expiredCommentIds = Lists.newArrayList();
|
List<Long> expiredCommentIds = Lists.newArrayList();
|
||||||
|
if (commentsJsonList != null) {
|
||||||
for (int i = 0; i < commentsJsonList.size(); i++) {
|
for (int i = 0; i < commentsJsonList.size(); i++) {
|
||||||
String commentJson = (String) commentsJsonList.get(i);
|
String commentJson = (String) commentsJsonList.get(i);
|
||||||
if (Objects.nonNull(commentJson)) {
|
if (Objects.nonNull(commentJson)) {
|
||||||
@@ -219,6 +270,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
expiredCommentIds.add(Long.valueOf(commentIdList.get(i).toString()));
|
expiredCommentIds.add(Long.valueOf(commentIdList.get(i).toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 对于不存在的一级评论,需要批量从数据库中查询,并添加到 commentRspVOS 中
|
// 对于不存在的一级评论,需要批量从数据库中查询,并添加到 commentRspVOS 中
|
||||||
if (CollUtil.isNotEmpty(expiredCommentIds)) {
|
if (CollUtil.isNotEmpty(expiredCommentIds)) {
|
||||||
@@ -232,16 +284,42 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
.sorted(Comparator.comparing(FindCommentItemRspVO::getHeat).reversed())
|
.sorted(Comparator.comparing(FindCommentItemRspVO::getHeat).reversed())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 异步将评论详情,同步到本地缓存
|
||||||
|
syncCommentDetail2LocalCache(commentRspVOS);
|
||||||
|
|
||||||
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
||||||
}
|
}
|
||||||
// 缓存中没有,则查询数据库
|
// 缓存中没有,则查询数据库
|
||||||
//查询一级评论
|
//查询一级评论
|
||||||
List<CommentDO> oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize);
|
List<CommentDO> oneLevelCommentIds = commentDOMapper.selectPageList(noteId, offset, pageSize);
|
||||||
getCommentDataAndSync2Redis(oneLevelCommentIds, noteId, commentRspVOS);
|
getCommentDataAndSync2Redis(oneLevelCommentIds, noteId, commentRspVOS);
|
||||||
}
|
|
||||||
|
// 异步将评论详情,同步到本地缓存
|
||||||
|
syncCommentDetail2LocalCache(commentRspVOS);
|
||||||
|
|
||||||
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
return PageResponse.success(commentRspVOS, pageNo, count, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步评论详情到本地缓存中
|
||||||
|
*
|
||||||
|
* @param commentRspVOS 评论VO列表
|
||||||
|
*/
|
||||||
|
private void syncCommentDetail2LocalCache(List<FindCommentItemRspVO> commentRspVOS) {
|
||||||
|
// 开启一个异步线程
|
||||||
|
threadPoolTaskExecutor.execute(() -> {
|
||||||
|
// 构建缓存所需的键值
|
||||||
|
Map<Long, String> localCacheData = Maps.newHashMap();
|
||||||
|
commentRspVOS.forEach(commentRspVO -> {
|
||||||
|
Long commentId = commentRspVO.getCommentId();
|
||||||
|
localCacheData.put(commentId, JsonUtils.toJsonString(commentRspVO));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 批量写入本地缓存
|
||||||
|
LOCAL_CACHE.putAll(localCacheData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取一级评论数据,并同步到 Redis 中
|
* 获取一级评论数据,并同步到 Redis 中
|
||||||
*
|
*
|
||||||
@@ -378,10 +456,10 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
|
|
||||||
|
|
||||||
// 批量写入并设置过期时间
|
// 批量写入并设置过期时间
|
||||||
connection.setEx(
|
connection.stringCommands().setEx(
|
||||||
redisTemplate.getStringSerializer().serialize(entry.getKey()),
|
Objects.requireNonNull(redisTemplate.getStringSerializer().serialize(entry.getKey())),
|
||||||
randomExpire,
|
randomExpire,
|
||||||
redisTemplate.getStringSerializer().serialize(jsonStr)
|
Objects.requireNonNull(redisTemplate.getStringSerializer().serialize(jsonStr))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -396,18 +474,12 @@ public class CommentServiceImpl extends ServiceImpl<CommentDOMapper, CommentDO>
|
|||||||
* @param dbCount 数据库查询到的笔记评论总数
|
* @param dbCount 数据库查询到的笔记评论总数
|
||||||
*/
|
*/
|
||||||
private void syncNoteCommentTotal2Redis(String noteCommentTotalKey, Long dbCount) {
|
private void syncNoteCommentTotal2Redis(String noteCommentTotalKey, Long dbCount) {
|
||||||
redisTemplate.executePipelined(new SessionCallback<>() {
|
|
||||||
@Override
|
|
||||||
public Object execute(RedisOperations operations) {
|
|
||||||
// 同步 hash 数据
|
// 同步 hash 数据
|
||||||
operations.opsForHash().put(noteCommentTotalKey, RedisKeyConstants.FIELD_COMMENT_TOTAL, dbCount);
|
redisTemplate.opsForHash().put(noteCommentTotalKey, RedisKeyConstants.FIELD_COMMENT_TOTAL, dbCount);
|
||||||
|
|
||||||
// 随机过期时间 (保底1小时 + 随机时间),单位:秒
|
// 随机过期时间 (保底1小时 + 随机时间),单位:秒
|
||||||
long expireTime = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60);
|
long expireTime = 60 * 60 + RandomUtil.randomInt(4 * 60 * 60);
|
||||||
operations.expire(noteCommentTotalKey, expireTime, TimeUnit.SECONDS);
|
redisTemplate.expire(noteCommentTotalKey, expireTime, TimeUnit.SECONDS);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user