• spring-redis-session 自定义 key 和过期时间


    对于分布式应用来说,最开始遇到的问题就是 session 的存储了,解决方案大致有如下几种

    • 使用 spring-session 它可以把 session 存储到你想存储的位置,如 redis,mysql 等
    • 使用 JWTs ,它使用算法来验证 token 的合法性,是否过期,并且 token 无法被伪造,信息也是无法被篡改的

    本文内容主要说 spring-session 使用 redis 来存储 session ,实现原理,修改过期时间,自定义 key 等

    spring-session 对于内部系统来说还是可以的,使用方便,但如果用户量上来了的话,会使 redis 有很大的 session 存储开销,不太划算。

    使用

    使用起来比较简单,简单说一下,引包,配置,加注解 。如下面三步,就配置好了使用 redis-session

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    
    spring.redis.host=localhost  
    # 其它 超时,端口,库,连接池,集群,就自己去找了
    
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800)
    

    测试:因为是在 getSession 的时候才会创建 Session ,所以我们必须在接口中调用一次才能看到效果

    @GetMapping("/sessionId")
    public String sessionId(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        HttpSession session = request.getSession();
        session.setAttribute("user","sanri");
        return session.getId();
    }
    

    它的存储结果如下

    hash spring:session:sessions:e3d4d84f-cc9f-44d5-9199-463cd9de8272
    string spring:session:sessions:expires:e3d4d84f-cc9f-44d5-9199-463cd9de8272
    set spring:session:expirations:1577615340000
    

    第一个 hash 结构存储了 session 的一些基本信息和用户设置的一些属性信息

    creationTime 创建时间

    lastAccessedTime 最后访问时间

    maxInactiveInterval 过期时长,默认是 30 分钟,这里保存的秒值

    sessionAttr:user 这是我通过 session.setAttribute 设置进去的属性

    第二个 string 结构,它没有值,只有一个 ttl 信息,标识这组 key 还能活多久,可以用 ttl 查看

    第三个 set 结构,保存了所以需要过期的 key

    实现原理

    说明:这个实现没多少难度,我就照着源码念一遍了,就是一个过滤器的应用而已。

    首先从网上了解到,它是使用过滤器来实现把 session 存储到 redis 的,然后每次请求都是从 redis 拿到 session 的,所以目标就是看它的过滤器是哪个,是怎么存储的,又是怎么获取的。

    我们可以从它唯一的入口 @EnableRedisHttpSession 进入查看,它引入了一个 RedisHttpSessionConfiguration 开启了一个定时器,继承自 SpringHttpSessionConfiguration ,可以留意到 RedisHttpSessionConfiguration 创建一个 Bean RedisOperationsSessionRepository repository 是仓库的意思,所以它就是核心类了,用于存储 session ;那过滤器在哪呢,查看SpringHttpSessionConfiguration 它属于 spring-session-core 包,这是一个 spring 用来管理 session 的包,是一个抽象的概念,具体的实现由 spring-session-data-redis 来完成 ,那过滤器肯定在这里创建的,果然可以看到它创建一个 SessionRepositoryFilter 的过滤器,下面分别看过滤器和存储。

    SessionRepositoryFilter

    过滤器一定是有 doFilter 方法,查看 doFilter 方法,spring 使用 OncePerRequestFilter 把 doFilter 包装了一层,最终是调用 doFilterInternal 来实现的,查看 doFilterInternal 方法

    实现方式为使用了包装者设计把 request 和 response 响应进行了包装,我们一般拿 session 一般是从 request.getSession() ,所以包装的 request 肯定要重写 getSession ,所以可以看 getSession 方法来看是如何从 redis 获取 session ;

    前面都是已经存在 session 的判断相关,关键信息在这里

    S session = SessionRepositoryFilter.this.sessionRepository.createSession();
    

    这里的 sessionRepository 就是我们用来存取 session 的 RedisOperationsSessionRepository 查看 createSession 方法

    RedisOperationsSessionRepository

    // 这里保存了在 redis 中 hash 结构能看到的数据
    RedisSession redisSession = new RedisSession();
    
    this(new MapSession());
    this.delta.put(CREATION_TIME_ATTR, getCreationTime().toEpochMilli());
    this.delta.put(MAX_INACTIVE_ATTR, (int) getMaxInactiveInterval().getSeconds());
    this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime().toEpochMilli());
    this.isNew = true;
    this.flushImmediateIfNecessary();
    

    在 flushImmediateIfNecessary 方法中,如果 redisFlushMode 是 IMMEDIATE 模式,则会立即保存 session 进 redis ,但默认配置的是 ON_SAVE ,那是在哪里保存进 redis 的呢,我们回到最开始的过滤器 doFilterInternal 方法中,在 finally 中有一句

    wrappedRequest.commitSession();
    

    就是在这里将 session 存储进 redis 的 ,我们跟进去看看,核心语句为这句

    SessionRepositoryFilter.this.sessionRepository.save(session);
    
    session.saveDelta();
    if (session.isNew()) {
        String sessionCreatedKey = getSessionCreatedChannel(session.getId());
        this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
        session.setNew(false);
    }
    

    进入 saveDelta ,在这里进行了 hash 结构的设置

    getSessionBoundHashOperations(sessionId).putAll(this.delta);
    

    最后一行进行了过期时间的设置和把当前 key 加入 set ,读者自行查看

    RedisOperationsSessionRepository.this.expirationPolicy
    					.onExpirationUpdated(originalExpiration, this);
    

    修改一些参数

    实际业务中,可能需要修改一些参数才能达到我们业务的需求,最常见的需求就是修改 session 的过期时间了,在 EnableRedisHttpSession 注解中,已经提供了一些基本的配置如

    maxInactiveIntervalInSeconds 最大过期时间,默认 30 分钟 
    redisNamespace 插入到 redis 的 session 命名空间,默认是 spring:session 
    cleanupCron 过期 session 清理任务,默认是 1 分钟清理一次
    redisFlushMode 刷新方式 ,其实在上面原理的 flushImmediateIfNecessary 方法中有用到,默认是 ON_SAVE
    

    redisNamespace 是一定要修改的,这个不修改会影响别的项目,一般使用我们项目的名称加关键字 session 做 key ,表明这是这个项目的 session 信息。

    不过这样的配置明显不够,对于最大过期时间来说,有可能需要加到配置文件中去,而不是写在代码中,但是这里没有提供占位符的功能,回到 RedisOperationsSessionRepository 的创建,最终配置的 maxInactiveIntervalInSeconds 还是要设置到这个 bean 中去的,我们可以把这个 bean 的创建过程覆盖,重写 maxInactiveIntervalInSeconds 的获取过程,就解决了,代码如下

    @Autowired
    RedisTemplate sessionRedisTemplate;
    
    @Autowired
    ApplicationEventPublisher applicationEventPublisher;
    
    @Value("${server.session.timeout}")
    private int sessionTimeout = 1800;
    
    @Primary		// 使用 Primary 来覆盖默认的 Bean 
    @Bean
    public RedisOperationsSessionRepository sessionRepository() {
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
        // 这里要把原来的属性引用过来,避免出错 ,可以引用原来的类并复制属性 ;像 redisNamespace,redisFlushMode 都要复制过来
        return sessionRepository;
    }
    

    还有一个就是 redis 的序列化问题,默认是使用的 jdk 的对象序列化,很容易出现加一个字段或减少一个字段出现不能反序列化,所以序列化方式是需要换的,如果项目中的缓存就已经使用了对象序列化的话,那就面要为其单独写一个 redisTemplate 并设置进去,在构建 RedisOperationsSessionRepository 的时候设置 redisTemplate

    还有一个很重要的问题就是登录踢出问题,有时候只允许一个端登录,其它的端都要退出,这个需要在登录成功后才能把其它端踢出,我尝试过很多方法去修改存入 redis 的 key ,但最终都是失败的,因为操作不了 RedisSession ,就算建一个同包的类去修改它,SessionRepositoryRequestWrapper 也是无法修改的,需要在 commitSession 的时候把 sessionId 写入前端,这里在之前就已经写了原来的 UUID 值,所以我们能做的就是在登录成功后,把当前登录的 sessionId 和当前用户对应起来存入 redis ,下次登录之前做一次检测,如果已经登录,则把之前的 key 删除,但作者尝试过使用 SessionRepository 的 deleteById 删除 redis 的 session 记录,但是无效,删不干净,看源码发现那里是直接使用值来删除 set 中内容的,可能是序列化问题,希望有成功的大神可以指导下。

    一点小推广

    创作不易,希望可以支持下我的开源软件,及我的小工具,欢迎来 gitee 点星,fork ,提 bug 。

    Excel 通用导入导出,支持 Excel 公式
    博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
    gitee:https://gitee.com/sanri/sanri-excel-poi

    使用模板代码 ,从数据库生成代码 ,及一些项目中经常可以用到的小工具
    博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
    gitee:https://gitee.com/sanri/sanri-tools-maven

  • 相关阅读:
    LR学习笔记八 之 初识LR动态链接库
    LR学习笔记七 之 LR录制sql脚本
    LR学习笔记三 之 界面分析
    LoadRunner学习笔记一
    LR学习笔记四 之 调试、日志、动态运行
    协调世界时间 UTC
    InnerText和InnerXml的区别
    避免 showModalDialog 弹出新窗体
    嵌套SELECT语句
    XmlElement和XmlNode的区别
  • 原文地址:https://www.cnblogs.com/sanri1993/p/12120486.html
Copyright © 2020-2023  润新知