redis与session

基本引入

本质就是map 以key value的形式进行一个缓存 所以简单使用的时候也当作map用就行了

加入依赖

<!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

修改配置文件

spring:
  redis:
    host: localhost  # Redis 主机
    port: 6379  # Redis 端口
    database: 2  # Redis 使用的数据库索引

使用只需要简单设置key

然后注入

@Resource

private RedisTemplate<String, Object> redisTemplate;

使用其get set方法即可

以下是一个简单的示例 实现密码重试过多锁定功能

    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

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<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> 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

需引入

<!-- Spring Session + Redis 支持 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
            <version>2.6.3</version>
        </dependency>

配置

  session:

    store-type: redis

    redis:

      namespace: session # 设置 Redis 中存储 session 数据的命名空间

      flush-mode: on_save # 表示当 session 更新时,才会将数据保存到 Redis

      timeout: 86400 # session 过期时间:24小时

SessionConfig

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();

    }

}

实现登陆设备数量限制:

@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 缓存失效策略 | 05-redis与session | ➡️ 06-图片优化