重新认识分布式架构下的session,spring实现方案的详解
背景
近期我们的一个产品在客户环境做部署实施工作时,客户方要求接入他们的SSO。集成过程中,他们给了一个集成的SDK,其中的登录状态认证是基于session的。而我们产品是基于springcloud框架,前后端分离的。
借此机会我们重新再系统的熟悉下session,以及在分布式架构下的session处理策略。
session正在被淘汰吗?
当我们搜索“分布式部署的session方案”时,你可能会先看到类似:
“session正在被淘汰吗”、“分布式部署下,Session使用变少的原因”。
我们是不是对session的理解还停留在大学课堂
我们以前所认识的,是狭义上的session概念,一般指的是网站设计,大概率是指的HTTP。
Http是无状态的,为了在Http协议中,保持会话状态,比如用户的登录状态、购物车等,需要有一种方案,能够把用户这一次次的请求(无状态的Http请求)关联起来。
这种技术叫session。
理论上,只要我们能够用一种方案,实现把客户这一次次的无状态Http请求关联起来,都应该算作session的一种实现。
集群/分布式环境下session处理策略
- 在Cookie里放个JESSIONID,在服务器中存上状态,用户请求来了,根据JESSIONID去服务器里查状态,这是Tomacat的实现方法
- 把所有状态都存在Cookie里,服务器给个签名防止伪造,每次请求来了,直接从Cookie里提取状态,这是JWT的实现方法
- 在Cookie里放个token,状态存储基于Redis来实现
- 根据sessionId将session信息存储到数据库
- Spring sesssion的集成方案,spring session本身提供了一种透明的方式来进行session数据的管理,支持多种存储方式,也包括redis。
分布式下不适用或者被淘汰的是基于tomcat等web中间件的方案
在负载均衡的集群、分布式部署下:
1.浏览器发出请求,服务器访问nginx时,他会做负载均衡,选择哪个服务器主要看策略。一旦策略路由到了新的服务器,就会导致session失效。
粘性session
一开始有采用这种方案的,即标记浏览器第一次发起请求的路径,保证下一次请求,分配到相同的服务器,但是这种方案,很难保证服务器是负载均衡的
同步session
当一个请求到服务器后,会把session同步到其他的服务器,来保证所有服务器的session是统一的,但是在合众对服务器的性能会产生影响,且服务器器之间会增加耦合度
session共享
单独配备一台服务器,专门创建和获取session,所有服务器都通过这台服务器处理session,但是这样就增加了故障点。
所以我们一般主流上都会采用redis集中管理session,其读写效率高,且可以做redis的集群高可用。
Spring Boot/Cloud实现多服务session共享详解
基于以上我们的讨论,我们再来重点讨论下这个方案。实际应用中,结合我们产品的技术实现方案和客户给的SDK包,我们不想重写客户的sdk实现方法,基于spring session来实现透明方式的session管理是最好的实践路径。
实现方法详解
- 导入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- Redis存储类型配置
spring:
redis:
host: 127.0.0.1
port: 6379
session:
store-type: redis # Session存储类型为REDIS
这里store-type提供多种Session Store实现:
- RedisStore:将Session存储在Redis中,多台服务器共用一个Redis实现Session共享。这是默认推荐的方案。
- HazelcastStore:将Session存储在Hazelcast缓存中,多台服务器使用同一个Hazelcast集群实现Session共享。
- MongoDBStore:将Session存储在MongoDB文档数据库中,多台服务器使用同一个MongoDB实现Session共享。
- JDBCStore:将Session存储在关系型数据库中,多台服务器使用同一个数据库实现Session共享。
- 创建config类,实现spring session的支持
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericFastJsonRedisSerializer();
}
}
还可以把该注解@@EnableRedisHttpSession注解加在启动类的上面。
spring session的生命周期
这跟我们之前理解的狭义的session生命周期是有区别的:
1、创建Session:当用户第一次访问网站时,一个全新的Session会被创建,同时也会在配置的后端存储(比如Redis)中创建一个key,value就是新创建的Session对象。
2、会话活动:每当用户发送一个请求,如果还在会话超时时间内,会话就会变为活动状态。这将更新在Redis中的键的过期时间戳,确保它不会过期。
3、会话非活动:如果用户在一定时间内没有发送任何请求,会话变为非活动状态。这不会更新Redis key的过期时间戳。如果key在过期时间内没有再次变为活动状态,它最终会过期,session会被销毁。
4、显式销毁:当调用HttpServletRequest#logout() 或 HttpSession#invalidate()时,Session会被销毁。这也会导致Redis中的key被删除。
5、key过期:如果在会话超时时间内Session没有变为活动状态,Redis key会过期并被删除。这也会销毁与之对应的Session。
Spring Session引入了“非活动”状态,并且真正的Session销毁时间也推迟到了Redis key的过期时间。这带来的好处是可以配置一个较长的全局session过期时间,而通过续期使活动Session保持有效。非活动的Session会在较短的时间内过期,这有利于节省服务器资源。
另外,使用Spring Session还需要注意的地方是:必须配置Session超时时间与Redis key过期时间保持一致,否则会出现Session被错误销毁或未及时销毁的问题。
在application.properties中配置方法:
spring.session.timeout=30m
# 设置redis的过期时间为30分钟
spring.session.redis.timeout=1800
原理详解
请求链路
tips:引用自:www.cnblogs.com/crazymakerc…
原理图
tips:引用自;www.cnblogs.com/h--d/p/1485…
spring session是如何工作的
以redis作为会话存储为例,我们进行代码的跟踪,都做了哪些事情:
从注解@EnableRedisHttpSession开始,我们看到注解import了RedisHttpSessionConfiguration类,向Spring容器提供了RedisOperationsSessionRepository,其继承了SpringHttpSessionConfiguration类,在SpringHttpSessionConfiguration中,该配置类通过@Bean注解,向Spring容器中注册了一个SessionRepositoryFilter。
看到filter,我们凭借程序员的直觉应该就是跟踪到核心了,我们看到过滤器主要作用是拦所有的请求,接管创建和管理Session数据。


SessionRepository被注入
我们看到注册这个filter时需要一个SessionRepository参数,在SpringHttpSessionConfiguration的继承类RedisHttpSessionConfiguration中,SessionRepository被注入。
这里,RedisTemplate依赖一个RedisConnectionFactory是需要我们进行配置的。以springboot为例,只需要指定application.properties的spring.redis.cluster.nodes即可配置一个redis集群JedisConnectionFactory。
SessionRepositoryFilter如何接管创建和管理Session数据
它使用了一个SessionRepositoryRequestWrapper类接管了Http Session的创建和管理工作。
每当有请求进入时,过滤器会首先将ServletRequest 和ServletResponse 这两个对象转换成Spring内部的包装类SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper对象。
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//使用HttpServletRequest 、HttpServletResponse和servletContext创建一个SessionRepositoryRequestWrapper
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
//保存session信息
wrappedRequest.commitSession();
}
}
}
SessionRepositoryRequestWrapper类
将Sesison对象包装成了HttpSessionWrapper,目的是当Session失效时可以从sessionRepository删除。
这里重写了getSession方法,也就是为什么每当执行HttpServletRequest执行.getSession()方法后就会刷新session的过期时间。
redis如何保存session数据
接下来我们再来看另一个关键的节点,session数据又是怎么存放到redis中的呢:
从上述代码中看到,在SessionRepositoryFilter的doFilterInternal方法最后有一个finally中执行了:
wrappedRequest.commitSession();
这里就是保存session数据到redis。commitSession()方法还会在过滤器结束后调用,用来更新Session。
至此,我们就了解到了从注解@EnableRedisHttpSession开始的各依赖、创建和管理Session数据,以及session数据是如何存储到redis中的。
总结
本文我们在分布式、集群架构发展的场景下,重新探讨了对于session的定义和理解;在分布式、集群架构下,session的实现方案;spring session的使用方法和原理详解。
转载自:https://juejin.cn/post/7363193808521740327