SpringSession原理简析

本文借鉴于:Spring-Session 原理简析 - 知乎 (zhihu.com)

目录

概述

使用方式

原理

总结


概述

Session的原理

Session是存在服务器的一种用来存放用户数据的类哈希表结构,当浏览器第一次发送请求的时候服务器会生成一个hashtable和一个sessionid,sessionid来唯一标识这个hashtable,响应的时候会通过一个响应头set-cookie返回给浏览器,浏览器再将这个sessionid存储在一个名为JESSIONID的cookie中。 接着浏览器在发送第二次请求时,就会带上这个cookie,这个cookie会存储sessionid,并发送给服务器,服务器根据这个sessionid找到对应的用户信息。

分布式下Session共享的问题

如果在不同域名环境下需要进行session共享,比如在auth.gulimall.com登录成功之后要将登陆成功的用户信息共享给gulimall.com服务,由于域名不同,普通的session就会不起作用。并且如果是同一个服务,复制多份,session也不会共享。

SpringSession

SpringSession 是一个 Spring 项目中用于管理和跟踪会话的框架。它提供了一种抽象层,使得会话数据可以存储在不同的后端数据结构中(例如内存、数据库、Redis 等),并且支持跨多个请求的会话管理。

Spring Session 的核心功能包括:

(1)跨多个请求共享会话数据:SpringSession 使用一个唯一的会话标识符来跟踪用户的会话,并且可以在不同的请求中共享会话数据,无论是在同一个应用程序的不同服务器节点之间,还是在不同的应用程序之间。

(2)多种后端存储支持:SpringSession 支持多种后端存储,包括内存、数据库和缓存系统(如 Redis)。你可以根据应用程序的需求选择合适的存储方式。

(3)会话过期管理:SpringSession 提供了会话过期管理的功能,可以根据一定的策略自动清理过期的会话数据。 会话事件监听:Spring Session 允许开发人员注册会话事件监听器,以便在会话创建、销毁或属性更改时执行一些自定义逻辑。

(4)使用 SpringSession 可以使得在分布式环境中管理会话变得更加简单和灵活,同时也提供了更多的扩展性和可定制性。

SpringSession 可以在多个微服务之间共享 session 数据。

使用方式

添加依赖

java 复制代码
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>

添加注解@EnableRedisHttpSession

java 复制代码
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class RedisSessionConfig {


}

maxInactiveIntervalInSeconds: 设置 Session 失效时间,使用 Redis Session 之后,原 Spring Boot 的 server.session.timeout 属性不再生效。也可以在yml文件上配置

java 复制代码
spring:  
  session:
    store-type: redis
    timeout: 30m #这个过期时间是在浏览器关闭之后才开始计时

经过上面的配置后,Session 调用就会自动去Redis存取。另外,想要达到 Session 共享的目的,只需要在其他的系统上做同样的配置即可。

原理

看了上面的配置,我们知道开启 Redis Session 的"秘密"在 @EnableRedisHttpSession 这个注解上。打开 @EnableRedisHttpSession 的源码(shift+shift):

java 复制代码
/**
 * 用于启用基于Redis的HTTP会话管理的配置注解。
 * 该注解会将RedisHttpSessionConfiguration配置类导入到Spring配置中,
 * 从而支持将会话信息存储在Redis中。
 *
 * @author <NAME>
 * @see RedisHttpSessionConfiguration
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({RedisHttpSessionConfiguration.class})
@Configuration(
    proxyBeanMethods = false
)
public @interface EnableRedisHttpSession {
    
    /**
     * 会话的最大不活动间隔时间(秒)。默认值为1800秒(30分钟)。
     * 该属性定义了会话在Redis中存储的有效期。
     * 
     * @return 最大不活动间隔时间(秒)
     */
    int maxInactiveIntervalInSeconds() default 1800;

    /**
     * Redis中用于存储会话数据的命名空间。默认值为"spring:session"。
     * 通过设置不同的命名空间,可以实现多个应用会话的隔离。
     * 
     * @return Redis存储会话数据的命名空间
     */
    String redisNamespace() default "spring:session";

    /**
     * 已弃用的Redis刷新模式设置。建议使用{@link #flushMode()}替代。
     * 
     * @return Redis刷新模式,默认为ON_SAVE
     * @deprecated 使用{@link #flushMode()}替代
     */
    @Deprecated
    RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;

    /**
     * 定义Redis中数据的刷新模式。可配置为在每次保存属性时刷新(ON_SAVE),
     * 或者在请求结束时刷新(ON_COMMIT)。
     * 
     * @return 刷新模式,默认为ON_SAVE
     */
    FlushMode flushMode() default FlushMode.ON_SAVE;

    /**
     * 定义清理过期会话的CRON表达式。默认值为"0 * * * * *",即每分钟执行一次。
     * 通过调整该表达式,可以控制清理过期会话的频率。
     * 
     * @return 清理过期会话的CRON表达式
     */
    String cleanupCron() default "0 * * * * *";

    /**
     * 定义会话属性保存的模式。可配置为仅在设置属性时保存(ON_SET_ATTRIBUTE),
     * 或者在每次请求结束时保存(ALWAYS)。
     * 
     * @return 会话属性保存模式,默认为ON_SET_ATTRIBUTE
     */
    SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
}

这个注解的主要作用是注册一个 SessionRepositoryFilter,这个 Filter 会拦截所有的请求,对 Session 进行操作。

SessionRepositoryFilter 拦截到请求后,会先将 request 和 response 对象转换成 Spring 内部的包装类 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 对象。SessionRepositoryRequestWrapper 类重写了原生的getSession方法。代码如下:

java 复制代码
@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。

java 复制代码
@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:拦截器,Spring-Session 框架的核心;
  • 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 中。

说的简单点就是:拦截请求,将之前在服务器内存中进行 Session 创建、销毁的动作,改成在 Redis 中进行。

相关推荐
韩立学长3 小时前
【开题答辩实录分享】以《自助游网站的设计与实现》为例进行选题答辩实录分享
java·mysql·spring
不像程序员的程序媛3 小时前
Spring的cacheEvict
java·后端·spring
谷哥的小弟4 小时前
Spring Framework源码解析——PropertiesLoaderUtils
java·后端·spring·框架·源码
可爱又迷人的反派角色“yang”6 小时前
redis知识点总集
linux·运维·数据库·redis·缓存
BullSmall6 小时前
Redis 性能调优(二)
数据库·redis·缓存
gugugu.6 小时前
Redis ZSet类型深度解析:有序集合的原理与实战应用
网络·windows·redis
学习编程的Kitty6 小时前
Redis(1)——持久化
数据库·redis·mybatis
即将进化成人机6 小时前
验证码生成 + Redis 暂存 + JWT 认证
数据库·redis·笔记
岳轩子7 小时前
DDD领域驱动设计:核心概念、实践结构与框架对比
java·spring