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


    为什么要用springsession

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

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

    使用spring-session

    pom

    
    <dependency>
        <groupId>org.springframework.sessiongroupId>
        <artifactId>spring-session-data-redisartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    配置

    spring:
      session:
        store-type: redis
        redis:
          host: 192.168.1.13
          port: 6379
    server:
      servlet:
        session:
          timeout: 30m
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    要配置redis的连接信息

    开启springsession

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

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

    使用session

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

    session.setAttribute(key,data);
    
    • 1

    修改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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    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);
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    该接口主要负责 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

  • 相关阅读:
    VScode 基础使用教程
    【21】面向流水线的指令设计(下):奔腾4是怎么失败的?
    【每日一题】矩形重叠个数
    SaaSBase:什么是涂色scrm?
    1785. 构成特定和需要添加的最少元素
    喜报!易基因“同源基因特异性甲基化时序数据分析方法”获专利授权
    著名数字音频工作站FL Studio 21.0.3.3517中文破解安装图文激活教程
    UE5 使用Mesh Editor 查看骨骼相对于root的坐标系
    [移动通讯]【Carrier Aggregation-4】【LTE-5】
    谷粒商城——分布式基础(全栈开发篇第一部分)
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/125892342