在集群系统中, 经常会需要将 Session 进行共享. 不然会出现这样一个问题: 用户在系统 A 上登陆以后, 假如后续的一些操作被负载均衡到系统 B 上面, 系统 B 发现本机上没有这个用户的 Session, 会强制让用户重新登陆. 此时用户会很疑惑, 自己明明登陆过了, 为什么还要自己重新登陆.
什么是 Session
这边再普及下 Session 的概念: Session 是服务器端的一个 key-value 的数据结构, 经常被用户和 cookie 配合, 保持用户的登陆回话. 客户端在第一次访问服务端的时候, 服务端会响应一个 sessionId 并且将它存入到本地 cookie 中, 在之后的访问会将 cookie 中的 sessionId 放入到请求头中去访问服务器, 如果通过这个 sessionid 没有找到对应的数据那么服务器会创建一个新的 sessionid 并且响应给客户端.
分布式 Session 的解决方案
使用 cookie 来完成 (很明显这种不安全的操作并不可靠)
使用 Nginx 中的 ip 绑定策略, 同一个 ip 只能在指定的同一个机器访问 (不支持负载均衡)
利用数据库同步 session(效率不高)
使用 tomcat 内置的 session 同步 (同步可能会产生延迟)
使用 token 代替 session
我们使用 spring-session 以及集成好的解决方案, 存放在 Redis 中
最后一种方案是本文要介绍的重点.
Spring Session 使用方式
添加依赖
- <dependency>
- <groupId>org.springframework.session</groupId>
- <artifactId>spring-session-data-Redis</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.session</groupId>
- <artifactId>spring-session</artifactId>
- </dependency>
添加注解 @EnableRedisHttpSession
- @Configuration
- @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
- public class RedisSessionConfig {
- }
maxInactiveIntervalInSeconds: 设置 Session 失效时间, 使用 Redis Session 之后, 原 Spring Boot 的 server.session.timeout 属性不再生效.
经过上面的配置后, Session 调用就会自动去 Redis 存取. 另外, 想要达到 Session 共享的目的, 只需要在其他的系统上做同样的配置即可.
4. Spring Session Redis 的原理简析
看了上面的配置, 我们知道开启 Redis Session 的 "秘密" 在 @EnableRedisHttpSession 这个注解上. 打开 @EnableRedisHttpSession 的源码:
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- @Documented
- @Import(RedisHttpSessionConfiguration.class)
- @Configuration
- public @interface EnableRedisHttpSession {
- //Session 默认过期时间, 秒为单位, 默认 30 分钟
- int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
- // 配置 key 的 namespace, 默认的是 spring:session, 如果不同的应用共用一个 Redis, 应该为应用配置不同的 namespace, 这样才能区分这个 Session 是来自哪个应用的
- String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
- // 配置刷新 Redis 中 Session 的方式, 默认是 ON_SAVE 模式, 只有当 Response 提交后才会将 Session 提交到 Redis
- // 这个模式也可以配置成 IMMEDIATE 模式, 这样的话所有对 Session 的更改会立即更新到 Redis
- RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
- // 清理过期 Session 的定时任务默认一分钟一次.
- String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
- }
这个注解的主要作用是注册一个 SessionRepositoryFilter, 这个 Filter 会拦截到所有的请求, 对 Session 进行操作, 具体的操作细节会在后面讲解, 这边主要了解这个注解的作用是注册 SessionRepositoryFilter 就行了. 注入 SessionRepositoryFilter 的代码在 RedisHttpSessionConfiguration 这个类中.
- @Configuration
- @EnableScheduling
- public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
- implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
- SchedulingConfigurer {
- ...
- }
RedisHttpSessionConfiguration 继承了 SpringHttpSessionConfiguration,SpringHttpSessionConfiguration 中注册了 SessionRepositoryFilter. 见下面代码.
- @Configuration
- public class SpringHttpSessionConfiguration implements ApplicationContextAware {
- ...
- @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 时需要一个 SessionRepository 参数, 这个参数是在 RedisHttpSessionConfiguration 中被注入进入的.
- @Configuration
- @EnableScheduling
- public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
- implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
- SchedulingConfigurer {
- ...
- @Bean
- public RedisOperationsSessionRepository sessionRepository() {
- RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
- RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
- redisTemplate);
- sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
- if (this.defaultRedisSerializer != null) {
- sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
- }
- sessionRepository
- .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
- if (StringUtils.hasText(this.redisNamespace)) {
- sessionRepository.setRedisKeyNamespace(this.redisNamespace);
- }
- sessionRepository.setRedisFlushMode(this.redisFlushMode);
- int database = resolveDatabase();
- sessionRepository.setDatabase(database);
- return sessionRepository;
- }
- ...
- }
请求进来的时候拦截器会先将 request 和 response 拦截住, 然后将这两个对象转换成 Spring 内部的包装类 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 对象. SessionRepositoryRequestWrapper 类重写了原生的 getSession 方法. 代码如下:
- @Override
- public HttpSessionWrapper getSession(boolean create) {
- // 通过 request 的 getAttribue 方法查找 CURRENT_SESSION 属性, 有直接返回
- HttpSessionWrapper currentSession = getCurrentSession();
- if (currentSession != null) {
- return currentSession;
- }
- // 查找客户端中一个叫 SESSION 的 cookie, 通过 sessionRepository 对象根据 SESSIONID 去 Redis 中查找 Session
- S requestedSession = getRequestedSession();
- if (requestedSession != null) {
- if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
- requestedSession.setLastAccessedTime(Instant.now());
- this.requestedSessionIdValid = true;
- currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
- currentSession.setNew(false);
- // 将 Session 设置到 request 属性中
- setCurrentSession(currentSession);
- // 返回 Session
- return currentSession;
- }
- }
- else {
- // This is an invalid session id. No need to ask again if
- // request.getSession is invoked for the duration of this request
- if (SESSION_LOGGER.isDebugEnabled()) {
- SESSION_LOGGER.debug(
- "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
- }
- setAttribute(INVALID_SESSION_ID_ATTR, "true");
- }
- // 不创建 Session 就直接返回 null
- if (!create) {
- return null;
- }
- if (SESSION_LOGGER.isDebugEnabled()) {
- SESSION_LOGGER.debug(
- "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for"
- + SESSION_LOGGER_NAME,
- new RuntimeException(
- "For debugging purposes only (not an error)"));
- }
- // 通过 sessionRepository 创建 RedisSession 这个对象, 可以看下这个类的源代码, 如果
- //@EnableRedisHttpSession 这个注解中的 redisFlushMode 模式配置为 IMMEDIATE 模式, 会立即
- // 将创建的 RedisSession 同步到 Redis 中去. 默认是不会立即同步的.
- S session = SessionRepositoryFilter.this.sessionRepository.createSession();
- session.setLastAccessedTime(Instant.now());
- currentSession = new HttpSessionWrapper(session, getServletContext());
- setCurrentSession(currentSession);
- return currentSession;
- }
当调用 SessionRepositoryRequestWrapper 对象的 getSession 方法拿 Session 的时候, 会先从当前请求的属性中查找. CURRENT_SESSION 属性, 如果能拿到直接返回, 这样操作能减少 Redis 操作, 提升性能.
到现在为止我们发现如果 redisFlushMode 配置为 ON_SAVE 模式的话, Session 信息还没被保存到 Redis 中, 那么这个同步操作到底是在哪里执行的呢? 我们发现 SessionRepositoryFilter 的 doFilterInternal 方法最后有一个 finally 代码块, 这个代码块的功能就是将 Session 同步到 Redis.
- @Override
- protected void doFilterInternal(HttpServletRequest request,
- HttpServletResponse response, FilterChain filterChain)
- throws ServletException, IOException {
- request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
- SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
- request, response, this.servletContext);
- SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
- wrappedRequest, response);
- try {
- filterChain.doFilter(wrappedRequest, wrappedResponse);
- }
- finally {
- // 将 Session 同步到 Redis, 同时这个方法还会将当前的 SESSIONID 写到 cookie 中去, 同时还会发布一
- //SESSION 创建事件到队列里面去
- wrappedRequest.commitSession();
- }
- }
总结
主要的核心类有:
@EnableRedisHttpSession: 开启 Session 共享功能
RedisHttpSessionConfiguration: 配置类, 一般不需要我们自己配置. 主要功能是配置 SessionRepositoryFilter 和 RedisOperationsSessionRepository 这两个 Bean
SessionRepositoryFilter: 拦截器
RedisOperationsSessionRepository: 可以认为是一个 Redis 操作的客户端, 有在 Redis 中增删改查 Session 的功能
SessionRepositoryRequestWrapper:Request 的包装类, 主要是重写了 getSession 方法
SessionRepositoryResponseWrapper:Response 的包装类.
原理简要总结:
当请求进来的时候, SessionRepositoryFilter 会先拦截到请求, 将 request 和 Response 对象转换成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper. 后续当第一次调用 request 的 getSession 方法时, 会调用到 SessionRepositoryRequestWrapper 的 getSession 方法. 这个方法的逻辑是先从 request 的属性中查找, 如果找不到; 再查找一个 key 值是 "SESSION" 的 cookie, 通过这个 cookie 拿到 sessionId 去 Redis 中查找, 如果查不到, 就直接创建一个 RedisSession 对象, 同步到 Redis 中 (同步的时机根据配置来).
遗留问题
什么时候写的 cookie
清理过期 Session 的功能怎么实现的
自定义 HttpSessionStrategy
参考
https://www.cnblogs.com/SimpleWu/p/10118674.html
来源: https://www.cnblogs.com/54chensongxia/p/12096493.html