--- title: "05-redis与session" created: 2025-12-02 tags: - 项目 aliases: - redis与session --- # redis与session ## 基本引入 本质就是map 以key value的形式进行一个缓存 所以简单使用的时候也当作map用就行了 加入依赖 ```xml org.springframework.boot spring-boot-starter-data-redis ``` 修改配置文件 ```yaml spring: redis: host: localhost # Redis 主机 port: 6379 # Redis 端口 database: 2 # Redis 使用的数据库索引 ``` 使用只需要简单设置key 然后注入 @Resource private RedisTemplate redisTemplate; 使用其get set方法即可 以下是一个简单的示例 实现密码重试过多锁定功能 ```java private static final String PASSWORD_RETRY_KEY = "YunPicture:user:password_retry:%s"; @Value("${user.max-password-retry}") private int maxPwdRetryLimit; @Override public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) { User user = getUserByAccount(userAccount); //密码重试过多锁定 (redis) done String retryKey = String.format(PASSWORD_RETRY_KEY, userAccount); Integer retryCount = (Integer) redisTemplate.opsForValue().get(retryKey); if (retryCount != null && retryCount >= maxPwdRetryLimit) { throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "密码输入次数过多,请10分钟后重试"); } boolean isPasswordValid = passwordUtils.verifyPassword(userPassword, user.getUserPassword(), user.getSalt()); if (!isPasswordValid) { // 密码错误时记录次数 redisTemplate.opsForValue().set(retryKey, retryCount != null ? retryCount + 1 : 1, 600, TimeUnit.SECONDS); throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误"); } else { // 登录成功重置计数器 redisTemplate.delete(retryKey); } LoginUserVO LoginuserVO = convertToLoginUserVO(user); //todo 引入redis 控制登入设备数量 request.getSession().setAttribute(USER_LOGIN_STATE, LoginuserVO); return LoginuserVO; } ``` 测试下来功能是没问题的 但是打开quickredis一看全是乱码 这是因为默认情况下,Spring Boot的RedisTemplate使用JdkSerializationRedisSerializer作为键/值的序列化器,这会导致: 键名会被序列化为二进制格式 QuickRedis等客户端无法直接识别二进制键名 即使数据存储成功,但通过客户端查看时显示为乱码或无法识别 (虽然没影响 但分别调试 最好还是修改以下) ## 配置序列化 其实就是简单去规定一下键值对的存储格式 自定义一下redistemplate ```java package com.zwnsyw.yunpicturebackend.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import com.zwnsyw.yunpicturebackend.exception.BusinessException; import com.zwnsyw.yunpicturebackend.exception.ErrorCode; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; @Configuration @Slf4j public class RedisTemplateConfig { @Value("${redis.host:localhost}") private String redisHost; @Value("${redis.port:6379}") private int redisPort; /** * 配置 RedisTemplate 使用 Jackson 序列化 */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 键的序列化:纯文本 template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); // 值的序列化:JSON 格式 GenericJackson2JsonRedisSerializer valueSerializer = genericJackson2JsonRedisSerializer(); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.setDefaultSerializer(valueSerializer); template.setEnableTransactionSupport(true); // 开启事务支持 template.afterPropertiesSet(); try { template.afterPropertiesSet(); } catch (RedisConnectionFailureException e) { log.error("Redis 连接失败:主机:{},端口:{}", redisHost, redisPort); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "连接 Redis 失败"); } catch (Exception e) { log.error("Redis 配置失败:主机:{},端口:{}", redisHost, redisPort); throw new BusinessException(ErrorCode.SYSTEM_ERROR, "Redis 配置出错"); } log.info("Redis 配置成功,主机:{},端口:{}", redisHost, redisPort); return template; } /** * 配置通用 JSON 序列化器 */ private GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping( BasicPolymorphicTypeValidator.builder() // 允许的业务包路径 .allowIfSubType("com.zwnsyw") .allowIfSubType("com.zwnsyw.yunpicturebackend.model.*") .allowIfSubType("com.zwnsyw.yunpicturebackend.model.entity.*") .allowIfSubType("com.zwnsyw.yunpicturebackend.model.vo.*") // 允许的集合类型 .allowIfSubType("java.util.ArrayList") .allowIfSubType("java.util.HashMap") .allowIfSubType("java.util.LinkedList") .allowIfSubType("java.util.TreeMap") .allowIfSubType("java.util.LinkedHashMap") // 允许基础类型和 Date .allowIfSubType("java.lang.String") .allowIfSubType("java.lang.Long") .allowIfSubType("java.lang.Integer") .allowIfSubType("java.util.Date") // 其他类型(如 MyBatis 分页类) .allowIfSubType("com.baomidou.mybatisplus.extension.plugins.pagination.Page") .allowIfSubType("java.sql.Timestamp") .build(), ObjectMapper.DefaultTyping.NON_FINAL ); return new GenericJackson2JsonRedisSerializer(objectMapper); } } ``` ## 配置分布式session 需引入 ```xml org.springframework.session spring-session-data-redis 2.6.3 ``` 配置 ```yaml session: store-type: redis redis: namespace: session # 设置 Redis 中存储 session 数据的命名空间 flush-mode: on_save # 表示当 session 更新时,才会将数据保存到 Redis timeout: 86400 # session 过期时间:24小时 ``` SessionConfig ```java package com.zwnsyw.yunpicturebackend.config; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import java.time.Duration; @Configuration @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30) public class SessionConfig { // 配置Lettuce客户端参数 @Bean public LettuceClientConfiguration redisSessionConfig() { return LettuceClientConfiguration.builder() .commandTimeout(Duration.ofMinutes(30)) .shutdownTimeout(Duration.ofSeconds(10)) // 添加关闭超时配置 .build(); } @Bean @Qualifier("springSessionDefaultRedisSerializer") public RedisSerializer springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } } ``` 实现登陆设备数量限制: ```java @Override public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) { User user = getUserByAccount(userAccount); //to do 密码重试过多锁定 (redis) done String retryKey = String.format(PASSWORD_RETRY_KEY, userAccount); Integer retryCount = (Integer) redisTemplate.opsForValue().get(retryKey); if (retryCount != null && retryCount >= maxPwdRetryLimit) { throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "密码输入次数过多,请10分钟后重试"); } boolean isPasswordValid = passwordUtils.verifyPassword(userPassword, user.getUserPassword(), user.getSalt()); if (!isPasswordValid) { // 密码错误时记录次数 redisTemplate.opsForValue().set(retryKey, retryCount != null ? retryCount + 1 : 1, 600, TimeUnit.SECONDS); throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误"); } else { // 登录成功重置计数器 redisTemplate.delete(retryKey); } LoginUserVO LoginuserVO = convertToLoginUserVO(user); // 获取用户 IP 和会话 SessionId String userIp = request.getRemoteAddr(); String sessionId = request.getSession().getId(); // 验证设备数量,并记录登录设备 if (!recordUserDevice(user.getId(), sessionId, userIp)) { throw new BusinessException(ErrorCode.FORBIDDEN_ERROR, "设备登录数量超限制"); } //to do 引入redis 控制登入设备数量 done request.getSession().setAttribute(USER_LOGIN_STATE, LoginuserVO); log.info("用户登录成功: userId={}, account={}, sessionId={}, ip={}", user.getId(), userAccount, sessionId, userIp); return LoginuserVO; } /** * 记录用户设备信息,同时清理旧设备的登录状态 */ private boolean recordUserDevice(long userId, String sessionId, String userIp) { String redisKey = "login_devices:" + userId; String deviceKey = sessionId + "_" + userIp; // 使用 ZSet 存储设备信息,并以时间戳为权重 long currentTime = System.currentTimeMillis(); redisTemplate.opsForZSet().add(redisKey, deviceKey, currentTime); // 检查设备数量是否超出限制 Long deviceCountObj = redisTemplate.opsForZSet().size(redisKey); long deviceCount = deviceCountObj != null ? deviceCountObj : 0; if (deviceCount > maxDeviceLimit) { log.warn("超过设备数量限制: userId={}, deviceCount={}, maxDeviceLimit={}", userId, deviceCount, maxDeviceLimit); // 获取超出部分的旧设备 Set devicesToRemove = redisTemplate.opsForZSet() .range(redisKey, 0, deviceCount - maxDeviceLimit - 1); if (devicesToRemove != null) { for (Object device : devicesToRemove) { String[] parts = device.toString().split("_"); if (parts.length > 0) { String oldestSessionId = parts[0]; // 销毁旧设备登录态 invalidateSession(oldestSessionId); Message message = new Message(); message.setUserId(userId); message.setTitle("下线通知"); message.setContent("您的账户已达到最大设备登录数量限制,已自动下线最早登入设备"); //todo session(vo)中记录ip 登录地点 优化通知 message.setType(MessageTypeEnum.SYSTEM_MESSAGE.getValue()); messageService.addMessage(message); } // 从 Redis 中移除旧设备 redisTemplate.opsForZSet().remove(redisKey, device); } } } // 设置 Redis 键的过期时间 redisTemplate.expire(redisKey, 7, TimeUnit.DAYS); return true; } /** * 销毁给定会话的 HttpSession */ private void invalidateSession(String sessionId) { // 使用分布式 Session 管理销毁会话 Session session = sessionRepository.findById(sessionId); if (session != null) { sessionRepository.deleteById(sessionId); // 强制删除 Redis 中的 Session log.info("Session 已销毁: sessionId={}", sessionId); } else { log.warn("未能找到 Session: sessionId={}", sessionId); } } @Override public boolean userLogout(HttpServletRequest request) { // 获取当前用户ID LoginUserVO loginUser = (LoginUserVO) request.getSession().getAttribute(USER_LOGIN_STATE); if (loginUser != null) { String redisKey = "login_devices:" + loginUser.getId(); String sessionId = request.getSession().getId(); String userIp = request.getRemoteAddr(); // 移除设备记录 String deviceKey = sessionId + "_" + userIp; redisTemplate.opsForZSet().remove(redisKey, deviceKey); log.info("用户注销成功: userId={}, sessionId={}, userIp={}", loginUser.getId(), sessionId, userIp); } // 销毁 Session request.getSession().invalidate(); return true; } ``` --- **项目分区导航**:⬅️ [[04-@Cacheable和@CacheEvict 缓存失效策略|04-@Cacheable和@CacheEvict 缓存失效策略]] | 05-redis与session | ➡️ [[06-图片优化|06-图片优化]]