session在分布式环境下存在的问题

由于HTTP协议是无状态的,在开发中我们可以将用户的信息存储在服务器的session中,并生成与之相对应的JSESSIONID通过cookie返回给浏览器。浏览器下次访问,cookie会自动携带上次请求存储的数据(JSESSIONID)到服务器中,服务器根据JSESSIONID找到对应的session,从而获取用户的信息。

该机制在单体应用中是没有问题的,但是如果在分布式环境下,会产生session共享问题,即session的数据在服务1中存在,但是在服务2中不存在。

就会出现下面的问题:
在这里插入图片描述
假设用户第一次访问的是会员服务1,会员服务1将用户的信息记录在自己的session中,但是当用户第二次访问的是会员服务2时,就会找不到用户信息

session共享解决方案

session复制

服务器将自己的session数据传送给其他服务器,使得每个服务器都拥有全量的数据。
在这里插入图片描述

优点:tomcat原生支持,只需要修改配置文件即可
缺点:

  • session同步需要数据传输,会占用大量带宽,降低服务器集群的业务处理能力
  • 任意一台web-server保存的都是所有web-server的session总和,浪费了大量的空间,且受内存限制无法水平扩展更多的web-server
  • 大型分布式集群情况下,由于所有web-server都要全量保存数据,所以此方案不可取。

客户端存储

用户的信息不再保存在服务器中,而是保存在客户端(浏览器)中。
在这里插入图片描述
优点:服务器不需要保存用户信息,节省服务器资源
缺点:

  1. 每次http请求,携带用户在cookie中的完整信息,浪费网络带宽
  2. 用户信息存放在cookie中,cookie有长度4k限制,不能存放大量信息
  3. 用户信息存储在cookie中,存在泄漏、篡改、窃取等安全隐患

一般情况下不会使用这种方案。

hash一致性

nginx负载均衡的时候采用ip-hash策略,这样同一个客户端每次的请求都会被同一个服务器处理。
在这里插入图片描述
优点:

  • 只需要修改nginx配置,不需要修改应用程序代码
  • 可以支持web-server水平扩展

缺点:

  • session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录
  • 如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确session。

但以上缺点其实问题不大,因为session本来也是有有效期的,所以这个方案也经常被采用。

统一存储

jsessionid这个cookie默认是系统域名。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案。

将用户的信息存储在第三方中间件上,做到统一存储,如redis中,所有的服务都到redis中获取用户信息,从而实现session共享。

优点

  • 没有安全隐患
  • 可以水平扩展
  • 服务器重启或扩容都不会造成session的丢失

不足:

  • 增加了一次网络调用,速度有所下降
  • 需要修改应用程序代码,如将所有的getSession方法替换为Redis查数据的方式。但这个问题可以通过spring session完美解决

整合spring session

现在我们知道,我们可以将session的信息存储在第三方数据库中,比如redis。但如果是我们自己去写这个逻辑的话太过麻烦。而spring session可以很简单的帮助我们实现这个功能。

官网地址:spring session官网地址
在这里插入图片描述
1、添加依赖

<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>

2、配置文件添加配置

spring.session.store-type=redis

3、配置redis连接

spring.redis.host=localhost # Redis server host.
spring.redis.password= # Login password of the redis server.
spring.redis.port=6379 # Redis server port.

4、使用json序列化机制

@Configuration
public class SessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericFastJsonRedisSerializer();
    }
}

spring session默认是jdk序列化机制,要求类需要实现Serializable接口,序列化后是二进制,人看不懂。使用json序列化机制就没有这些问题。

5、在springboot启动类中添加@EnableRedisHttpSession注解

这样就OK了


扩展

session不能跨不同域名共享

当我们的认证微服务以及其他微服务使用的是俩个不同的域名时,即使使用了spring session也会存在不同域名的共享问题。

比如,认证服务的域名为auth.fcpmall.com,订单服务的域名为
order.fcpmall.com,这种情况下,即使在认证服务登录成功,将用户的信息保存在redis中,订单服务也无法查询到。

session不能跨不同域名共享的原因

先回顾一下正常的session流程:

  • session依赖于cookie的,服务器会将JSESSIONID放到cookie中,并返回给服务器。
  • 浏览器下次访问时,携带的cookie信息中含有JSESSIONID,所以服务器可以根据JSESSIONID找到对应的session

在不同域名下会发生什么?

  • 首先你需要知道浏览器在发送http请求时,只会携带domain为当前域名以及父域名cookie信息。也就是从order.fcpmall.com发出的http请求只会携带domain为order.fcpmall.com和fcpmall.com的域名信息。
  • 浏览器在设置域名的时候默认使用的是当前的域名。即认证服务的JSESSIONID会被保存在domain为auth.fcpmall.com的cookie中
  • 综上,订单服务在发送请求的时候,没有携带含有JSESSIONID的cookie信息。所以找不到对应的session信息

在这里插入图片描述

知道了原因后,解决请来就很简单了,只需要在设置cookie的时候,指定domain为父域名fcpmall.com即可。

@Configuration
public class SessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainName("fcpmall.com");
        return serializer;
    }
}

这篇博客的知识点总结:
在这里插入图片描述
脑图链接地址

下面的部分由于我水平有限,写得不太好,所以选看即可

spring session核心原理

为什么spring session可以在不修改应用程序代码的前提下,将getSession方法替换为Redis查询数据的方式?

原理很简单,在我们添加@EnableRedisHttpSession注解的时候,它会为我们创建一个名为springSessionRepositoryFilter的bean,这个bean实现了Filter接口,在过滤器中将原先的HttpSession替换掉了,采用了装饰者模式。

下面进行初浅的源码分析(源码这一块虽然我现在还很弱,源码也很难读,但我认为这一块还是必要去锻炼的,所以慢慢来吧)

//在EnableRedisHttpSession中导入RedisHttpSessionConfiguration配置类
@Import({RedisHttpSessionConfiguration.class})
@Configuration(
    proxyBeanMethods = false
)
public @interface EnableRedisHttpSession{
	...
}


@Configuration(
    proxyBeanMethods = false
)
//RedisHttpSessionConfiguration继承SpringHttpSessionConfiguration
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
	//注入sessionRepository,用来对redis进行增删操作的类
	@Bean
    public RedisIndexedSessionRepository sessionRepository() {
    	。。。
    }
	...
}

//看看SpringHttpSessionConfiguration做了些什么
@Configuration(
    proxyBeanMethods = false
)
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
    //在容器中注入SessionRepositoryFilter,该类继承了Filter(关键)
    @Bean
    public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
        sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
        return sessionRepositoryFilter;
    }
   	...
}

//这个过滤器中实现了狸猫换太子
@Order(-2147483598)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
        SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
        	//注意了,传入下一个过滤器的request和response已经被换成了wrappedRequest,wrappedResponse。这里使用了装饰者模式
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            wrappedRequest.commitSession();
        }

    }

}

到这里知道了,当我们使用spring session的时候,在经过spring session过滤器的时候HttpServletRequest已经被换成了SessionRepositoryResponseWrapper,接下来我们就看一下这个类对getSession动了什么手脚。

    private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
	  public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
            SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
            if (currentSession != null) {
                return currentSession;
            } else {
            	//获取session
                S requestedSession = this.getRequestedSession();
    		.....
     }

	  private S getRequestedSession() {
            if (!this.requestedSessionCached) {
                List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
                Iterator var2 = sessionIds.iterator();

                while(var2.hasNext()) {
                    String sessionId = (String)var2.next();
                    if (this.requestedSessionId == null) {
                        this.requestedSessionId = sessionId;
                    }

					//从sessionRepository获取session
                    S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
                    if (session != null) {
                        this.requestedSession = session;
                        this.requestedSessionId = sessionId;
                        break;
                    }
                }

                this.requestedSessionCached = true;
            }

            return this.requestedSession;
        }
}

还记得前面RedisHttpSessionConfiguration配置的RedisIndexedSessionRepository吗?被spring session狸猫换太子后,我们后面对HttpSession的操作其实都是由这个类完成的。也就是说对session的增删操作实际上已经换成了对redis的增删操作了。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐