diff --git a/han-note-user/han-note-user-biz/pom.xml b/han-note-user/han-note-user-biz/pom.xml
index 43d5042..a6ed967 100644
--- a/han-note-user/han-note-user-biz/pom.xml
+++ b/han-note-user/han-note-user-biz/pom.xml
@@ -88,6 +88,11 @@
com.hanserwei
han-note-distributed-id-generator-api
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+
diff --git a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/config/ThreadPoolConfig.java b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/config/ThreadPoolConfig.java
new file mode 100644
index 0000000..e011771
--- /dev/null
+++ b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/config/ThreadPoolConfig.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/constant/RedisKeyConstants.java b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/constant/RedisKeyConstants.java
index 99c6593..c770fb9 100644
--- a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/constant/RedisKeyConstants.java
+++ b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/constant/RedisKeyConstants.java
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java
index ce9b388..6b37dd2 100644
--- a/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java
+++ b/han-note-user/han-note-user-biz/src/main/java/com/hanserwei/hannote/user/biz/service/impl/UserServiceImpl.java
@@ -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 implement
private RedisTemplate redisTemplate;
@Resource
private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
+ @Resource(name = "userTaskExecutor")
+ private ThreadPoolTaskExecutor threadPoolTaskExecutor;
+
+ /**
+ * 用户信息本地缓存
+ */
+ @SuppressWarnings("NullableProblems")
+ private static final Cache 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 implement
@Override
public Response 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().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 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);
}
}
diff --git a/hanserwei-framework/hanserwei-common/src/main/java/com/hanserwei/framework/common/utils/JsonUtils.java b/hanserwei-framework/hanserwei-common/src/main/java/com/hanserwei/framework/common/utils/JsonUtils.java
index 2edbf57..903fd42 100755
--- a/hanserwei-framework/hanserwei-common/src/main/java/com/hanserwei/framework/common/utils/JsonUtils.java
+++ b/hanserwei-framework/hanserwei-common/src/main/java/com/hanserwei/framework/common/utils/JsonUtils.java
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
+import org.apache.commons.lang3.StringUtils;
public class JsonUtils {
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -33,4 +34,21 @@ public class JsonUtils {
public static String toJsonString(Object obj) {
return OBJECT_MAPPER.writeValueAsString(obj);
}
+
+ /**
+ * 将 JSON 字符串转换为对象
+ *
+ * @param jsonStr JSON 字符串
+ * @param clazz 目标对象类型
+ * @return 目标对象
+ * @param 目标对象类型
+ */
+ @SneakyThrows
+ public static T parseObject(String jsonStr, Class clazz) {
+ if (StringUtils.isBlank(jsonStr)) {
+ return null;
+ }
+
+ return OBJECT_MAPPER.readValue(jsonStr, clazz);
+ }
}
diff --git a/pom.xml b/pom.xml
index 33a239d..075e821 100755
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,7 @@
2.3.3
5.6.227
3.8.0
+ 3.2.2
@@ -234,7 +235,12 @@
han-note-distributed-id-generator-api
${revision}
-
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+ ${caffeine.version}
+