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

为什么要用springsession

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

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

使用spring-session

pom

<!-- 整合springsession -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

配置

spring:
  session:
    store-type: redis
    redis:
      host: 192.168.1.13
      port: 6379
server:
  servlet:
    session:
      timeout: 30m

要配置redis的连接信息

开启springsession

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

@EnableRedisHttpSession     //整合Redis作为session存储

使用session

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

session.setAttribute(key,data);

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

@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各种增删改查的类。

@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了。

@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类中实现的。

@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

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 中创建。

站在巨人的肩膀上

https://blog.csdn.net/weixin_42189048/article/details/123727819

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃了也弱了。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值