你问我分布式session,我劝你耗子尾汁

1,027 阅读7分钟

前言

今天我们来聊一聊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的源码做了一个大致的入门,不知道张三你明白了。我们年轻人还是很讲武德滴!

完!

都看到着了,点个赞点个关注再走呗~

关注我的公众号,我们一起学习,一起进步。
每天都要变得比昨天更好一点.