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} +