本文借鉴于: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 中进行。