1、使用场景java
1)一台服务器上的软负载均衡应用mysql
2)分布式应用web
2、实现方式redis
1)session数据存cookiespring
将session存储至cookie中,每次请求从cookie中读取session,缺点:不安全,大小有限制sql
2)粘性sessionexpress
粘性session是指Ngnix每次都将同一用户的全部请求转发至同一台服务器上,即将用户与服务器绑定,缺点:某台服务器不可用时,获取不到session数据浏览器
3)session复制安全
每次session发生变化时,建立或者修改,就广播给全部集群中的服务器,使全部的服务器上的session相同,缺点:副本数据都同样,数据冗余,占用空间服务器
4)session共享
使用redis、mysql等存储session
3、使用配置
1)pom.xml引入jar包
2)web.xml配置filter
3)application.xml启用spring-session
4、流程图
步骤:
1)请求被filter过滤器拦截,其实是被SessionRepositoryFilter拦截器处理
2)生成request、response包装类,后续操做中跟request、response相关的操做都是调用包装类的方法
3)业务代码中调用request.getSession()时,实际调用的是SessionRepositoryRequestWrapper类的方法
4)SessionRepositoryRequestWrapper的getSession()方法会获取当前域中的cookie,获取sessionID
5)根据sessionID到redis中查找与之对应的RedisSession对象
6)当无RedisSession返回时,建立RedisSession对象,以后调用setAttribute和getAttribute方法时,分别是往对象中到map存放和获取值
7)将RedisSession对象放入request中,供后续使用,如SessionRepositoryFilter$SessionRepositoryRequestWrapper. commitSession()
8)将数据保存至redis,实际上保存到是RedisSession对象中的delta属性,该属性的数据类型为Map,对应的redis数据结构为hash
9)将cookie写入浏览器,cookie包括sessionID
5、源码解析
1)加载SessionRepositoryFilter过滤器
web.xml的过滤器为DelegatingFilterProxy,过滤器实现了InitializingBean接口,故会调用afterPropertiesSet()方法,最终对应的是SessionRepositoryFilter
为何最后对应的filter是SessionRepositoryFilter?因为DelegatingFilterProxy类中的targetBeanName值为springSessionRepositoryFilter,而initDelegate()方法是在spring容器中找到id为springSessionRepositoryFilter的对象即为filter的具体实现类。回到application.xml文件,该文件中有一行配置:
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
查看RedisHttpSessionConfiguration源码发现其父类有个方法,方法中使用了@Bean注解,该注解相似<bean />,id默认为方法名称,方法参数默认依赖spring容器中id为参数名称的对象,故该代码最后会往spring容器中注入SessionRepositoryFilter对象,id=springSessionRepositoryFilter,注入的SessionRepositoryFilter对象,且参数sessionRepository依赖spring容器中的id=sessionRepository对象,具体代码以下:
@Bean public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) { SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) { sessionRepositoryFilter.setHttpSessionStrategy((MultiHttpSessionStrategy) this.httpSessionStrategy); } else { sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy); } return sessionRepositoryFilter; }
2)加载RedisOperationsSessionRepository
在application.xml中有以下配置,查看RedisHttpSessionConfiguration源码,发现Spring容器中id=springSessionRepositoryFilter的类,即为SessionRepositoryFilter,且默认的session持久化容器为redis
图中的springSessionRepositoryFilter()方法表示初始化SessionRepositoryFilter对象,注入到spring容器中,且id=springSessionRepositoryFilter,其中方法参数sessionRepository表示依赖spring容器中id=sessionRepository的对象。从图中sessionRepository()方法可知,注入spring容器的id=sessionRepository的对象为RedisOperationsSessionRepository,即默认的session持久化到redis中
3)生成request&response包装对象
spring-session 的核心思想是对HttpServletRequest和HttpServletResonse进行包装,后续全部操做request、response的方法均调用包装对象的方法,生成包装对象是在SessionRepositoryFilter中进行,通过滤器处理后,controller方法中的HttpServletRequest和HttpServletResponse对象均为SessionRepositoryRequestWrapper和MultiSessionHttpServletResponse,具体代码以下:
4)request.getSession()解析
1。获取sessionID
当调用request.getSession()方法时,会从cookie中获取sessionID,代码以下:
2。根据sessionID查找Session对象
获取到sessionID且值不为空,则须要到redis中查找与之对应的session对象
loadSession方法定义在RedisOperationsSessionRepository类中,目的是为了将redis中键为spring:session:sessions:{sessionId}到hash结构的属性值复制到MapSession中,而生成的MapSession对象作为RedisSession类的构造函数的参数,也就是说在RedisSession对象中保存了MapSession对象的引用,能够直接操做RedisSession对象获取redis保存的属性值,具体代码以下:
private MapSession loadSession(String id, Map<Object, Object> entries) { MapSession loaded = new MapSession(id); for (Map.Entry<Object, Object> entry : entries.entrySet()) { String key = (String) entry.getKey(); if (CREATION_TIME_ATTR.equals(key)) { loaded.setCreationTime((Long) entry.getValue()); } else if (MAX_INACTIVE_ATTR.equals(key)) { loaded.setMaxInactiveIntervalInSeconds((Integer) entry.getValue()); } else if (LAST_ACCESSED_ATTR.equals(key)) { loaded.setLastAccessedTime((Long) entry.getValue()); } else if (key.startsWith(SESSION_ATTR_PREFIX)) { loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue()); } } return loaded; }
3。建立Session对象
获取不到sessionID或者值为空时,则须要建立Session对象
当调用request.getSession.setAttribute(name,value)时,实际是往RedisSession对象中的delta属性中设值,具体代码在RedisOperationsSessionRepository中,以下:
public void setAttribute(String attributeName, Object attributeValue) { this.cached.setAttribute(attributeName, attributeValue); this.putAndFlush(getSessionAttrNameKey(attributeName), attributeValue); } private void putAndFlush(String a, Object v) { this.delta.put(a, v); this.flushImmediateIfNecessary(); }
4。Session对象保存至Redis
经过以上步骤获取或建立Session对象后,以后就是将Session对象保存到redis中。而保存操做是在SessionRepositoryFilter类的doFilterInternal方法的finally中执行
五、定时任务清理过时key
虽然redis自带了过时key的清理,但采用可是按期删除+懒性删除方式,若是并发量比较大的时候,redis会存在不少无效key,形成内容浪费。鉴于redis清理key方式的弊端,spring-session开启了一个定时任务,定时清理redis中过时的key,其具体思路是取得当前时间的时间戳(精确到分)做为 key,去 redis 中定位到 spring:session:expirations:{当前时间戳} ,这个 set 里面存放的即是全部过时的 key 。具体实现以下:
org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanupExpiredSessions
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }
org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanExpiredSessions
public void cleanExpiredSessions() { long now = System.currentTimeMillis(); // 获取当前时间戳对应的分 long prevMin = roundDownMinute(now); if (logger.isDebugEnabled()) { logger.debug("Cleaning up sessions expiring at " + new Date(prevMin)); } // 获取到spring:session:expirations:{当前时间戳-精确到分} String expirationKey = getExpirationKey(prevMin); // 取出当前这一分钟应当过时的session Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members(); // 删除spring:session:expirations:{当前时间戳-精确到分}键,不是删除session自己 this.redis.delete(expirationKey); // 遍历这一分钟要过时的session for (Object session : sessionsToExpire) { String sessionKey = getSessionKey((String) session); // 访问session touch(sessionKey); } } private void touch(String key) { // 并非删除key,而只是访问key this.redis.hasKey(key); }
6、客户端禁用cookie
当浏览器禁用cookie后,是没法获取到cookie数据的,也就是说没法获取到jsessionID,获取不到jessionID则没法得到对应的session对象。为了解决这个问题,能够对URL重写,使用response.encodeURL(url)便可。重写URL的目的是在url后面加上jsessonID,这样便能在请求中获取到jsessionID,进一步得到对应的session对象。测试了一下,当集成spring-session后,使用response.encodeURL(url)重写URL时,是不会在url后面加上jsessionID参数,这或许是设计spring-session时,就必需要求不能禁用cookie。