feat(user): 引入本地缓存优化用户信息查询性能
- 添加 Caffeine 依赖并配置本地缓存 - 实现用户信息多级缓存:本地缓存 -> Redis -> 数据库 - 新增用户信息缓存KEY常量及构建方法 - 配置自定义线程池用于异步缓存操作 - 实现缓存空对象防止击穿与过期时间随机化 - 添加 JsonUtils 工具类解析 JSON 字符串为对象的方法
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package com.hanserwei.hannote.user.biz.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
@Configuration
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
@Bean(name = "userTaskExecutor")
|
||||
public ThreadPoolTaskExecutor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
// 核心线程数
|
||||
executor.setCorePoolSize(10);
|
||||
// 最大线程数
|
||||
executor.setMaxPoolSize(50);
|
||||
// 队列容量
|
||||
executor.setQueueCapacity(200);
|
||||
// 线程活跃时间(秒)
|
||||
executor.setKeepAliveSeconds(30);
|
||||
// 线程名前缀
|
||||
executor.setThreadNamePrefix("UserExecutor-");
|
||||
|
||||
// 拒绝策略:由调用线程处理(一般为主线程)
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
|
||||
// 等待所有任务结束后再关闭线程池
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
// 设置等待时间,如果超过这个时间还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是被没有完成的任务阻塞
|
||||
executor.setAwaitTerminationSeconds(60);
|
||||
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@ public class RedisKeyConstants {
|
||||
*/
|
||||
private static final String ROLE_PERMISSIONS_KEY_PREFIX = "role:permissions:";
|
||||
|
||||
/**
|
||||
* 用户信息数据 KEY 前缀
|
||||
*/
|
||||
private static final String USER_INFO_KEY_PREFIX = "user:info:";
|
||||
|
||||
/**
|
||||
* 构建用户-角色 Key
|
||||
*
|
||||
@@ -36,4 +41,13 @@ public class RedisKeyConstants {
|
||||
public static String buildRolePermissionsKey(String roleKey) {
|
||||
return ROLE_PERMISSIONS_KEY_PREFIX + roleKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建角色对应的权限集合 KEY
|
||||
* @param userId 用户ID
|
||||
* @return 用户信息key
|
||||
*/
|
||||
public static String buildUserInfoKey(Long userId) {
|
||||
return USER_INFO_KEY_PREFIX + userId;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.hanserwei.hannote.user.biz.service.impl;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
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.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
||||
import com.hanserwei.framework.common.enums.DeletedEnum;
|
||||
@@ -34,6 +37,7 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -43,6 +47,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -58,6 +63,19 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource
|
||||
private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
|
||||
@Resource(name = "userTaskExecutor")
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
|
||||
/**
|
||||
* 用户信息本地缓存
|
||||
*/
|
||||
@SuppressWarnings("NullableProblems")
|
||||
private static final Cache<Long, FindUserByIdRspDTO> LOCAL_CACHE = Caffeine.newBuilder()
|
||||
.initialCapacity(10000) // 设置初始容量为 10000 个条目
|
||||
.maximumSize(10000) // 设置缓存的最大容量为 10000 个条目
|
||||
.expireAfterWrite(1, TimeUnit.HOURS) // 设置缓存条目在写入后 1 小时过期
|
||||
.build();
|
||||
|
||||
|
||||
@Override
|
||||
public Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO) {
|
||||
@@ -235,10 +253,42 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
|
||||
@Override
|
||||
public Response<FindUserByIdRspDTO> findById(FindUserByIdReqDTO findUserByIdReqDTO) {
|
||||
Long userId = findUserByIdReqDTO.getId();
|
||||
|
||||
// 先从本地缓存获取
|
||||
FindUserByIdRspDTO findUserByIdRspDTOLocalCache = LOCAL_CACHE.getIfPresent(userId);
|
||||
if (Objects.nonNull(findUserByIdRspDTOLocalCache)) {
|
||||
log.info("==> 本地缓存获取用户信息成功,用户 ID: {}, findUserByIdRspDTOLocalCache: {}", userId, JsonUtils.toJsonString(findUserByIdRspDTOLocalCache));
|
||||
return Response.success(findUserByIdRspDTOLocalCache);
|
||||
}
|
||||
|
||||
// 用户缓存 RedisKey
|
||||
String userInfoKey = RedisKeyConstants.buildUserInfoKey(userId);
|
||||
// 先从Redis中获取
|
||||
String userInfoRedisValue = (String) redisTemplate.opsForValue().get(userInfoKey);
|
||||
// 若 Redis 中有缓存,则直接返回
|
||||
if (StringUtils.isNotBlank(userInfoRedisValue)) {
|
||||
// 将 Redis 中缓存的 JSON 字符串转为对象
|
||||
FindUserByIdRspDTO findUserByIdRspDTO = JsonUtils.parseObject(userInfoRedisValue, FindUserByIdRspDTO.class);
|
||||
// 异步缓存到本地缓存中
|
||||
threadPoolTaskExecutor.submit(() -> {
|
||||
// 缓存到本地缓存中
|
||||
if (findUserByIdRspDTO != null) {
|
||||
LOCAL_CACHE.put(userId, findUserByIdRspDTO);
|
||||
}
|
||||
});
|
||||
return Response.success(findUserByIdRspDTO);
|
||||
}
|
||||
// 若 Redis 中没有缓存,则从数据库中获取
|
||||
UserDO userDO = this.getOne(new QueryWrapper<UserDO>().eq("id", userId));
|
||||
|
||||
// 判空
|
||||
if (Objects.isNull(userDO)) {
|
||||
threadPoolTaskExecutor.submit(() -> {
|
||||
// 防止缓存击穿,缓存空对象
|
||||
// 过期时间保底1分钟+随机秒数,避免缓存雪崩
|
||||
long expireTime = 60 + RandomUtil.randomInt(60);
|
||||
redisTemplate.opsForValue().set(userInfoKey, "null", expireTime, TimeUnit.SECONDS);
|
||||
});
|
||||
throw new ApiException(ResponseCodeEnum.USER_NOT_FOUND);
|
||||
}
|
||||
// 构建返参
|
||||
@@ -247,6 +297,11 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
|
||||
.nickName(userDO.getNickname())
|
||||
.avatar(userDO.getAvatar())
|
||||
.build();
|
||||
threadPoolTaskExecutor.submit(() -> {
|
||||
// 过期时间保底1天+随机秒数,避免缓存雪崩
|
||||
long expireTime = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
||||
redisTemplate.opsForValue().set(userInfoKey, JsonUtils.toJsonString(findUserByIdRspDTO), expireTime, TimeUnit.SECONDS);
|
||||
});
|
||||
return Response.success(findUserByIdRspDTO);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user