【精选】spring-session的使用及其原理——分布式session解决方案

文章目录

为什么要用springsession

为了解决分布式系统下session无法共享的问题。

spring-session是spring旗下的一个项目,把servlet容器实现的httpSession替换为spring-session,专注于解决 session管理问题。可简单快速且无缝的集成到我们的应用中。

使用spring-session

pom

xml 复制代码
<!-- 整合springsession -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

配置

java 复制代码
spring:
  session:
    store-type: redis
    redis:
      host: 192.168.1.13
      port: 6379
server:
  servlet:
    session:
      timeout: 30m

要配置redis的连接信息

开启springsession

在配置类或者启动类中开启session共享

java 复制代码
@EnableRedisHttpSession     //整合Redis作为session存储

使用session

正常使用session,就会将session存入redis(data要实现Serializable)

java 复制代码
session.setAttribute(key,data);

修改json方式序列化和作用域

java 复制代码
@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        //放大作用域
        cookieSerializer.setDomainName("cxf.com");
        cookieSerializer.setCookieName("CXFSESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

springsession核心原理(2.6.0版本的springsession,与之前版本可能有出入,但是原理相同)

RedisIndexedSessionRepository

使用@EnableRedisHttpSession注解就会导入RedisHttpSessionConfiguration配置类,其中有一个Bean,就是用来操作session各种增删改查的类。

java 复制代码
@Bean
public RedisIndexedSessionRepository sessionRepository() {
	RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
	RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
	sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
	if (this.indexResolver != null) {
		sessionRepository.setIndexResolver(this.indexResolver);
	}
	if (this.defaultRedisSerializer != null) {
		sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
	}
	sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
	if (StringUtils.hasText(this.redisNamespace)) {
		sessionRepository.setRedisKeyNamespace(this.redisNamespace);
	}
	sessionRepository.setFlushMode(this.flushMode);
	sessionRepository.setSaveMode(this.saveMode);
	int database = resolveDatabase();
	sessionRepository.setDatabase(database);
	this.sessionRepositoryCustomizers
			.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
	return sessionRepository;
}

SessionRepositoryFilter

Spring Session 的核心就是这个 SessionRepositoryFilter。

它包装request和request,后续使用的request就不是原生的request了,而是包装好的,此时调用getSession方法返回的也是包装好的session,就可以直接操作Redis了。

java 复制代码
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
		throws ServletException, IOException {
	request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
	// 把 HttpServletRequest 和 HttpServletResponse 装饰成 SessionRepositoryRequestWrapper
	// 和 SessionRepositoryResponseWrapper,让它们走接下来的 filterChain,后续使用的request和response就是包装好的了
	SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
	SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
			response);

	try {
		filterChain.doFilter(wrappedRequest, wrappedResponse);
	}
	finally {
		wrappedRequest.commitSession();
	}
}

SessionRepositoryRequestWrapper

我们可以看到,使用包装的request获取的session,也是一个包装好的session------HttpSessionWrapper,最终实现对session的增删改查是在RedisIndexedSessionRepository类中实现的。

java 复制代码
@Override
public HttpSessionWrapper getSession(boolean create) {
	// // 先从 request attribute 获取,同一个 request 的 session 会放在这
	HttpSessionWrapper currentSession = getCurrentSession();
	if (currentSession != null) {
		return currentSession;
	}
	// // 获取不到,就解析 sessionId,然后基于此从 repository 获取
	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.markNotNew();
			setCurrentSession(currentSession);
			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");
	}
	if (!create) {
		return null;
	}
	if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
			&& this.response.isCommitted()) {
		throw new IllegalStateException("Cannot create a session after the response has been committed");
	}
	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)"));
	}
	S session = SessionRepositoryFilter.this.sessionRepository.createSession();
	session.setLastAccessedTime(Instant.now());
	currentSession = new HttpSessionWrapper(session, getServletContext());
	setCurrentSession(currentSession);
	return currentSession;
}

HttpSessionIdResolver

java 复制代码
public interface HttpSessionIdResolver {

	// 从客户端请求解析 sessionId
	List<String> resolveSessionIds(HttpServletRequest request);

	// 告诉客户端新创建 session 的 sessionId,比如放到 Cookie 里
	void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId);

	// 告知客户端 session 过期,比如从 Cookie 里移除对应的 sessionId
	void expireSession(HttpServletRequest request, HttpServletResponse response);

}

该接口主要负责 sessionId 的解析、处理工作

resolveSessionIds 方法解析客户端请求对应的 sessionId,比如从 Cookie 解析、 RequestHeader 解析

setSessionId 方法将创建的 sessionId 返回给客户端,比如放到 Cookie 、 ResponseHeader 里

expireSession 方法告知客户端 session 已过期,比如从 Cookie 移除、删除对应的 ResponseHeader

Spring 提供的实现有 CookieHttpSessionIdResolver 和 HeaderHttpSessionIdResolver,前者是基于 Cookie 后者基于 Header,默认的是 CookieHttpSessionIdResolver

原理简要总结

当请求进来的时候,SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的getSession方法。这个方法是被从写过的,逻辑是先从 request 的属性中查找,如果找不到;再查找一个key值是"SESSION"的 Cookie,通过这个 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接创建一个RedisSession 对象,同步到 Redis 中。

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

站在巨人的肩膀上

blog.csdn.net/weixin_4218...

相关推荐
zopple3 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001115 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本6 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34166 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan6 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer7 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor3567 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3568 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer8 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP9 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪