对于分布式应用来讲,最开始遇到的问题就是 session 的存储了,解决方案大体有以下几种java
本文内容主要说 spring-session 使用 redis 来存储 session ,实现原理,修改过时时间,自定义 key 等mysql
spring-session 对于内部系统来讲仍是能够的,使用方便,但若是用户量上来了的话,会使 redis 有很大的 session 存储开销,不太划算。git
使用起来比较简单,简单说一下,引包,配置,加注解 。以下面三步,就配置好了使用 redis-sessionredis
<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(); }
它的存储结果以下spring
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 的一些基本信息和用户设置的一些属性信息sql
creationTime 建立时间数据库
lastAccessedTime 最后访问时间缓存
maxInactiveInterval 过时时长,默认是 30 分钟,这里保存的秒值session
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 值都是 uuid 的形式,根本没办法知道当前这个 key 是哪一个用户在哪里登陆的,咱们其实能够修改它的 key 为 userId_ip_time 的形式,用来代表这个用户什么时间在哪一个 ip 有登陆过,我是这么玩的(没有在实际中使用过,虽然能改,但可能有坑):
通过前面的源码分析,建立 session 并保存到 redis 的是 RedisOperationsSessionRepository
的 createSession 方法,可是这里写死了 RedisSession 使用空的构造,并且 RedisSession 是 final 的内部类,访问权限为默认,构造的时候 new MapSession 也是默认的,最终那个 id 为使用 UUID ,看起来一点办法都没有,其实在这里建立完 session ,用户不必定是登陆成功的状态,咱们应该在登陆成功才能修改 session 的 key ,好在 RedisOperationsSessionRepository
提供了一个方法 findById ,咱们能够在这个上面作文章,先把 RedisSession 查出来,而后用反射获得 MapSession
,而后留意到 MapSession
是能够修改 id 的,它本身也提供了方法 changeSessionId ,咱们彻底能够在登陆成功调用 setId 修改 sessionId ,而后再写回去,这个代码必定要和 RedisSession 在同包 代码以下:
package org.springframework.session.data.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.session.MapSession; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; import java.lang.reflect.Field; @Component public class SessionOperation { @Autowired private RedisOperationsSessionRepository redisOperationsSessionRepository; public void loginSuccess(String userId){ String sessionId = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession().getId(); RedisOperationsSessionRepository.RedisSession redisSession = redisOperationsSessionRepository.findById(sessionId); Field cached = ReflectionUtils.findField(RedisOperationsSessionRepository.RedisSession.class, "cached"); ReflectionUtils.makeAccessible(cached); MapSession mapSession = (MapSession) ReflectionUtils.getField(cached, redisSession); mapSession.setId("userId:1"); redisOperationsSessionRepository.save(redisSession); } }
源码地址: https://gitee.com/sanri/example/tree/master/test-redis-session
创做不易,但愿能够支持下个人开源软件,及个人小工具,欢迎来 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