对于分布式应用来说, 最开始遇到的问题就是 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 公式
博客地址:
gitee: https://gitee.com/sanri/sanri-excel-poi
使用模板代码 , 从数据库生成代码 , 及一些项目中经常可以用到的小工具
博客地址:
gitee: https://gitee.com/sanri/sanri-tools-maven
来源: https://www.cnblogs.com/sanri1993/p/12120486.html