2019独角兽企业重金招聘Python工程师标准>>>
一、使用场景
1)一台服务器上的软负载均衡应用
2)分布式应用
二、实现方式
1)session数据存cookie
将session存储至cookie中,每次请求从cookie中读取session,缺点:不安全,大小有限制
2)粘性session
粘性session是指Ngnix每次都将同一用户的所有请求转发至同一台服务器上,即将用户与服务器绑定,缺点:某台服务器不可用时,获取不到session数据
3)session复制
每次session发生变化时,创建或者修改,就广播给所有集群中的服务器,使所有的服务器上的session相同,缺点:副本数据都一样,数据冗余,占用空间
4)session共享
使用redis、mysql等存储session
三、使用配置
1)pom.xml引入jar包
2)web.xml配置filter
3)application.xml启用spring-session
四、流程图
步骤:
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
五、源码解析
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中执行
5、定时任务清理过期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);
}
六、客户端禁用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。