feat(user): 引入本地缓存优化用户信息查询性能

- 添加 Caffeine 依赖并配置本地缓存
- 实现用户信息多级缓存:本地缓存 -> Redis -> 数据库
- 新增用户信息缓存KEY常量及构建方法
- 配置自定义线程池用于异步缓存操作
- 实现缓存空对象防止击穿与过期时间随机化
- 添加 JsonUtils 工具类解析 JSON 字符串为对象的方法
This commit is contained in:
Hanserwei
2025-10-08 20:29:54 +08:00
parent fcdbda4c56
commit ae9720b7cb
6 changed files with 136 additions and 1 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}