feat(user): 引入本地缓存优化用户信息查询性能
- 添加 Caffeine 依赖并配置本地缓存 - 实现用户信息多级缓存:本地缓存 -> Redis -> 数据库 - 新增用户信息缓存KEY常量及构建方法 - 配置自定义线程池用于异步缓存操作 - 实现缓存空对象防止击穿与过期时间随机化 - 添加 JsonUtils 工具类解析 JSON 字符串为对象的方法
This commit is contained in:
@@ -88,6 +88,11 @@
|
|||||||
<groupId>com.hanserwei</groupId>
|
<groupId>com.hanserwei</groupId>
|
||||||
<artifactId>han-note-distributed-id-generator-api</artifactId>
|
<artifactId>han-note-distributed-id-generator-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Caffeine 本地缓存 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -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:";
|
private static final String ROLE_PERMISSIONS_KEY_PREFIX = "role:permissions:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户信息数据 KEY 前缀
|
||||||
|
*/
|
||||||
|
private static final String USER_INFO_KEY_PREFIX = "user:info:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建用户-角色 Key
|
* 构建用户-角色 Key
|
||||||
*
|
*
|
||||||
@@ -36,4 +41,13 @@ public class RedisKeyConstants {
|
|||||||
public static String buildRolePermissionsKey(String roleKey) {
|
public static String buildRolePermissionsKey(String roleKey) {
|
||||||
return ROLE_PERMISSIONS_KEY_PREFIX + 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;
|
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.core.conditions.query.QueryWrapper;
|
||||||
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.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
||||||
import com.hanserwei.framework.common.enums.DeletedEnum;
|
import com.hanserwei.framework.common.enums.DeletedEnum;
|
||||||
@@ -34,6 +37,7 @@ 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.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -43,6 +47,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -58,6 +63,19 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
|
|||||||
private RedisTemplate<String, Object> redisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
@Resource
|
@Resource
|
||||||
private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
|
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
|
@Override
|
||||||
public Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO) {
|
public Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO) {
|
||||||
@@ -235,10 +253,42 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
|
|||||||
@Override
|
@Override
|
||||||
public Response<FindUserByIdRspDTO> findById(FindUserByIdReqDTO findUserByIdReqDTO) {
|
public Response<FindUserByIdRspDTO> findById(FindUserByIdReqDTO findUserByIdReqDTO) {
|
||||||
Long userId = findUserByIdReqDTO.getId();
|
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));
|
UserDO userDO = this.getOne(new QueryWrapper<UserDO>().eq("id", userId));
|
||||||
|
|
||||||
// 判空
|
// 判空
|
||||||
if (Objects.isNull(userDO)) {
|
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);
|
throw new ApiException(ResponseCodeEnum.USER_NOT_FOUND);
|
||||||
}
|
}
|
||||||
// 构建返参
|
// 构建返参
|
||||||
@@ -247,6 +297,11 @@ public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implement
|
|||||||
.nickName(userDO.getNickname())
|
.nickName(userDO.getNickname())
|
||||||
.avatar(userDO.getAvatar())
|
.avatar(userDO.getAvatar())
|
||||||
.build();
|
.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);
|
return Response.success(findUserByIdRspDTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
public class JsonUtils {
|
public class JsonUtils {
|
||||||
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
@@ -33,4 +34,21 @@ public class JsonUtils {
|
|||||||
public static String toJsonString(Object obj) {
|
public static String toJsonString(Object obj) {
|
||||||
return OBJECT_MAPPER.writeValueAsString(obj);
|
return OBJECT_MAPPER.writeValueAsString(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 JSON 字符串转换为对象
|
||||||
|
*
|
||||||
|
* @param jsonStr JSON 字符串
|
||||||
|
* @param clazz 目标对象类型
|
||||||
|
* @return 目标对象
|
||||||
|
* @param <T> 目标对象类型
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static <T> T parseObject(String jsonStr, Class<T> clazz) {
|
||||||
|
if (StringUtils.isBlank(jsonStr)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OBJECT_MAPPER.readValue(jsonStr, clazz);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
pom.xml
8
pom.xml
@@ -55,6 +55,7 @@
|
|||||||
<jaxb-runtime.version>2.3.3</jaxb-runtime.version>
|
<jaxb-runtime.version>2.3.3</jaxb-runtime.version>
|
||||||
<cos-api.version>5.6.227</cos-api.version>
|
<cos-api.version>5.6.227</cos-api.version>
|
||||||
<feign-form.version>3.8.0</feign-form.version>
|
<feign-form.version>3.8.0</feign-form.version>
|
||||||
|
<caffeine.version>3.2.2</caffeine.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -234,7 +235,12 @@
|
|||||||
<artifactId>han-note-distributed-id-generator-api</artifactId>
|
<artifactId>han-note-distributed-id-generator-api</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- caffeine本地缓存-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>${caffeine.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|||||||
Reference in New Issue
Block a user