前言
今天我们来聊一聊session,说到session,张三同学他啪就站起来了,很快啊!不就是会话么,可以存储特定用户会话所需的属性及配置信息,并且是存储在服务端的,这个谁不知道。确实,session大家当然都知道是个什么东西,也肯定都使用过,在单机的项目中,我们大多数情况下会来存放登录人信息等等。现如今单机系统由于性能瓶颈问题,大多公司都会采用分布式的方式来开发部署,那比如说我是去另一个域名的服务器上认证,认证完后跳回来,这样会不会有问题?又比如说我们把同一个服务来部署多个副本,然后访问的时候通过网关负载均衡后路由到不同的机器,这样的话我们直接把信息存在浏览器的session还合理吗?张三同学陷入了思考,表示大意了,你不讲武德啊。
session共享存在问题
为了说了更简洁明了,我们来直接来上图,就拿一个登陆的场景来说,登陆成功后将用户信息存在session中。
假如我们将商品服务采用多副本的方式来部署,用户第一次访问通过负载均衡后路由到了1号服务上,用户进行登录后,服务端将session存储下来。然后用户又不小心刷新了下,网关又将用户的请求路由到了2号服务上,这下又变成了未登录状态。这就是多副本状态下session存在的问题。
那么是只存在这种问题吗?当然不是。
用户在浏览了一个商品页面,相中了这件商品打算购买,系统判定未登录,于是页面跳到登录服务器进行登录,登录成功后再跳转回来。那这样存在的问题想必大家从图中也看出来了,在进行登录成功后,系统将用户的信息保存在了认证服务这个域名下的session中。登录成功后跳转回商品页面,但是由于商品服务域名下并没有存在这个session,所以判断他还是一个未登录的状态。这就是所谓的不同服务间session存在的问题。
明白了所存在的问题,怎么来解决呢,毕竟武林要以和为贵。
如何解决?
session同步
显而易见,如果只存在一个服务器上,让它们相互之间同步一下session不就可以了。
优点:实现简单,web- server原生支持,只需要修改配置文件。
缺点:
1.任意一台web- server保存的数据都是所有session的总和,浪费空间内存。
2.session同步需要传输数据,占用网络带宽,降低服务器的业务处理能力。客户端存储
不如将信息保存在本地的cookie中它不香吗?是的,不香。
优点:存于本地节省服务器资源。
缺点:
1.存于本地存在信息泄露、篡改、窃取等安全隐患。
2.cookie有长度限制4k,不能保存大量信息。hash一致性
使用用户ip来进行hash运算,之后每次请求都落在同一台服务器上。
优点:
1.只需要修改负载均衡策略。
2.可支持 web-server水平扩展。
缺点:如果水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session。统一存储
将session信息统一存储于数据库中,取session也去数据库中取。优点:
1.没有安全隐患。
2.可以水平扩展,如果DB出现性能瓶颈只需扩展DB即可。
3.web- server重启或扩容都不会丢失session数据。
缺点:增加了一次网络调用,并且需要修改应用代码,将所有getSession方法替换为从DB中来获取。(此缺点可以用SpringSession完美解决)
在现在项目中使用hash的方式和统一存储方案的较多,今天就主要来了解一下SpringSession。
SpringSession
Spring Session 是 Spring 的项目之一。Spring Session 提供了一套创建和管理 Servlet HttpSession 的方案,默认采用外置的 Redis 来存储 Session 数据,以此来解决 Session 共享的问题,落地使用也非常的简单。
第一步,导入pom依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
第二步,配置yml
spring:
#配置redis
redis:
host: 192.168.189.131
port: 6379
#设置session存储位置为redis
session:
store-type: redis
#session过期时间
server:
servlet:
session:
timeout: 30m
第三步,在启动类上加上开启SpringSession的注解 @EnableRedisHttpSession。
第四步,从HttpSession中获取session对象,调用session.setAttribute("loginUser","张三");接下来对session的存取都是在redis中来操作的。
到这里就完了吗?别忘了还有不同域名间取不到session的问题。就如上面图中 prod.whynot.com商品服务域名跳到 auth.whynot.com域名登录后跳转回去取不到session的问题。就拿这两个域名来说,他们两同属于whynot.com的子域名,那我们把session存于父域名中,相当于把级别提升一级,不就可以完美解决了吗。
实现起来也是非常简单
@Configuration
public class SessionConfig {
/**
* 设置cookie的作用范围和名字
* @return
*/
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("whynot.com");
cookieSerializer.setCookieName("MYSESSION");
return cookieSerializer;
}
springSession核心源码
了解了最基本的使用,来看一下SpringSession最基本的源码实现吧,这里我只挑选了比较重要的几个地方说一下,感兴趣的朋友可以自己点一点,看一看,也不是很复杂。
源码首先要从启动注解@EnableRedisHttpSession 来看起,我们来点进去。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
}
发现它是通过@Import注解导入了一个叫RedisHttpSessionConfiguration的配置类,它是继承于SpringHttpSessionConfiguration。当前这个配置类是对SpringSession做了一些初始化的工作,其中放入了一个返回值为RedisOperationsSessionRepository的Bean。
可以看到这个Repository就是用于对redis的一系列增删改查操作的。
再来看一看它的父类 SpringHttpSessionConfiguration,也是一个配置类,它里面放入了一个很重要的Bean。
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
点进SessionRepositoryFilter,里面重写了一个叫doFilterInternal的方法,就是SpringSession最核心的源码了。
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//为request对象设置操作session的repository方法
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//包装原生request对象
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
//包装原生response对象
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
try {
//放行包装过的wrapped对象
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
可以看到,就是使用了装饰者模式,在过滤器中对原生的HttpServletRequest和HttpServletResponse进行包装,之后只放行包装过的Wrapper对象,这样repository所对session的增删改查也是使用的包装过后的 Wrapper对象,而我们使用时直接通过HttpSession所获取的session就是spring已经替换过的session。
小结
本篇对单机session在分布式中存在的问题,再到解决方法,最后通过SpringSession来解决分布式session的问题,并对SpringSession的源码做了一个大致的入门,不知道张三你明白了。我们年轻人还是很讲武德滴!
完!
都看到着了,点个赞点个关注再走呗~
关注我的公众号,我们一起学习,一起进步。
每天都要变得比昨天更好一点.