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 中进行。

相关推荐
DKPT12 分钟前
如何确定MySQL中哪些列适合做索引
数据库·spring boot·spring·spring cloud·缓存
小哇6661 小时前
SpringBoot整合Minio
java·spring boot·spring
行动π技术博客1 小时前
spring中IOC相关介绍
java·spring·rpc
Architect_Lee2 小时前
Redis 客户端命令大全
redis
一只猿Hou2 小时前
【logback-spring配置不生效,开发环境和生产环境配置不同输出级别】
linux·spring·logback
Mero技术博客2 小时前
第三节:如何理解Spring的两个特性IOC和AOP(自学Spring boot 3.x第一天)
java·spring boot·spring
菠菜很好吃2 小时前
Java知识点大纲
java·jvm·spring
hummhumm3 小时前
数据结构第06节:栈
java·开发语言·数据结构·spring boot·spring·java-ee·intellij-idea
petaexpress4 小时前
K8S学习教程(二):在 PetaExpress KubeSphere容器平台部署高可用 Redis 集群
redis·学习·kubernetes
飞翔的佩奇5 小时前
Java项目:基于SSM框架实现的网上医院预约挂号系统【ssm+B/S架构+源码+数据库+毕业论文】
java·数据库·mysql·spring·架构·ssm框架·预约挂号