SpringBoot结合Redis作为Session管理浅析

doMore 34 2024-11-27

依赖

<dependencies>
	<dependency>  
	  <groupId>org.springframework.boot</groupId>  
	  <artifactId>spring-boot-autoconfigure</artifactId>  
	  <version>2.6.8</version>  
	</dependency>
	<dependency>
		<groupId>org.springframework.session</groupId>  
		<artifactId>spring-session-data-redis</artifactId>  
		<version>2.6.3</version>
	</dependency>
</dependencies>

启动过程

  1. 加载 redis session 相关配置
// org.springframework.boot.autoconfigure.session.RedisSessionConfiguration
@Configuration(proxyBeanMethods = false)  
@ConditionalOnClass({ RedisTemplate.class, RedisIndexedSessionRepository.class })  
@ConditionalOnMissingBean(SessionRepository.class)  
@ConditionalOnBean(RedisConnectionFactory.class)  
@Conditional(ServletSessionCondition.class)  
@EnableConfigurationProperties(RedisSessionProperties.class)  
class RedisSessionConfiguration {  
  
    @Bean  
    @ConditionalOnMissingBean    
    ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {  
       switch (redisSessionProperties.getConfigureAction()) {  
       case NOTIFY_KEYSPACE_EVENTS:  
          return new ConfigureNotifyKeyspaceEventsAction();  
       case NONE:  
          return ConfigureRedisAction.NO_OP;  
       }  
       throw new IllegalStateException(  
             "Unsupported redis configure action '" + redisSessionProperties.getConfigureAction() + "'.");  
    }  
  
    @Configuration(proxyBeanMethods = false)  
    public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {  
  
       @Autowired  
       public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,  
             ServerProperties serverProperties) {  
          Duration timeout = sessionProperties  
                .determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());  
          if (timeout != null) {  
             setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());  
          }  
          setRedisNamespace(redisSessionProperties.getNamespace());  
          setFlushMode(redisSessionProperties.getFlushMode());  
          setSaveMode(redisSessionProperties.getSaveMode());  
          setCleanupCron(redisSessionProperties.getCleanupCron());  
       }  
    }
}

  1. Redis Session 配置加载
// org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration
// 实际用来创建 session 和 管理session的对象
// 这个类 并不能由我们自己去创建, 方法上只加了 @Bean,并没有条件注解,我们能做的处理就是修改 序列化类和在增加 org.springframework.session.config.SessionRepositoryCustomizer 定制化 操作。
@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;  
}

// 父类(org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration)中初始化  SessionRepositoryFilter,这个会在拦截每一次请求,请求完成之后,都回去提交 session,session过期或创建一个新的session 返回去

@Bean  
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(  
       SessionRepository<S> sessionRepository) {  
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);  
    sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);  
    return sessionRepositoryFilter;  
}


  1. 实现了 org.springframework.session.Session 的 RedisSession 和 持有RedisSession 的包装类org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper
// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession
// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper


// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper#getSession(boolean)
@Override  
public HttpSessionWrapper getSession(boolean create) {  
// 当前请求中的 Attribute 属性中是否保存了session实体,避免与redis的多次交互,增加性能
    HttpSessionWrapper currentSession = getCurrentSession();  
    if (currentSession != null) {  
       return currentSession;  
    }  
    // 通过请求Header Cookie 中 的session id 获取 session,获取到之后转换为包装类,放入 attribute 中
    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)"));  
    }  
    // 创建新的session
    S session = SessionRepositoryFilter.this.sessionRepository.createSession();  
    session.setLastAccessedTime(Instant.now());  
    currentSession = new HttpSessionWrapper(session, getServletContext());  
    setCurrentSession(currentSession);  
    return currentSession;  
}

  1. HttpSessionWrapper 持有 RedisSession ,重点看一下 提交session的方法
// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper#commitSession

private void commitSession() {  
    HttpSessionWrapper wrappedSession = getCurrentSession();  
    if (wrappedSession == null) {  
       if (isInvalidateClientSession()) {  
          SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);  
       }  
    }  
    else {  
       S session = wrappedSession.getSession();  
       clearRequestedSessionCache();  
       // sessionRepository 就是 RedisIndexedSessionRepository
       SessionRepositoryFilter.this.sessionRepository.save(session);  
       String sessionId = session.getId();  
       if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {  
          SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);  
       }  
    }  
}


//  org.springframework.session.data.redis.RedisIndexedSessionRepository#save
@Override  
public void save(RedisSession session) {  
    session.save();  
    if (session.isNew) {  
       String sessionCreatedKey = getSessionCreatedChannel(session.getId());  
       this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);  
       session.isNew = false;  
    }  
}

// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession#save
private void save() {
// 该方法主要是看是不是要更换 session ID,
    saveChangeSessionId();  
    // 主要是更新 session 失效时间,根据最后访问时间 lastAccessedTime
    saveDelta();  
}

// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession#saveDelta
private void saveDelta() {  
    if (this.delta.isEmpty()) {  
       return;  
    }  
    String sessionId = getId();  
    getSessionBoundHashOperations(sessionId).putAll(this.delta);  
    // 省略部分代码
    ......
  
    this.delta = new HashMap<>(this.delta.size());  
// 根据在 org.springframework.session.data.redis.RedisIndexedSessionRepository#getSession 中设置的 originalLastAccessTime 去更新缓存失效时间
    Long originalExpiration = (this.originalLastAccessTime != null)  
          ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;  
    RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);  
}


// org.springframework.session.data.redis.RedisSessionExpirationPolicy#onExpirationUpdated
// RedisSessionExpirationPolicy 过期策略,只有这一个实现
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {  
    String keyToExpire = SESSION_EXPIRES_PREFIX + session.getId();  
    long toExpire = roundUpToNextMinute(expiresInMillis(session));  
  
    if (originalExpirationTimeInMilli != null) {  
       long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);  
       if (toExpire != originalRoundedUp) {  
          String expireKey = getExpirationKey(originalRoundedUp);  
          this.redis.boundSetOps(expireKey).remove(keyToExpire);  
       }  
    }  
  
    long sessionExpireInSeconds = session.getMaxInactiveInterval().getSeconds();  
    String sessionKey = getSessionKey(keyToExpire);  
  
    if (sessionExpireInSeconds < 0) {  
       this.redis.boundValueOps(sessionKey).append("");  
       this.redis.boundValueOps(sessionKey).persist();  
       this.redis.boundHashOps(getSessionKey(session.getId())).persist();  
       return;  
    }  
  
    String expireKey = getExpirationKey(toExpire);  
    BoundSetOperations<Object, Object> expireOperations = this.redis.boundSetOps(expireKey);  
    expireOperations.add(keyToExpire);  
  
    long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);  
  
    expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);  
    if (sessionExpireInSeconds == 0) {  
       this.redis.delete(sessionKey);  
    }  
    else {  
       this.redis.boundValueOps(sessionKey).append("");  
       this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);  
    }  
// 刷新 session 失效时间,在原有基础上还加了 5分钟
this.redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);  
}

结语

到这里,从请求开始 获取 session,到最后刷新session 失效时间,一整个流程就结束了。根据研究以及天马行空的想法,有一个骚操作,记录如下:


/**  
 * 项目中很多情况下都会有站内信功能,实时刷新的站内信 通常是轮询或者websocket,而在项目中又一般会设置 一定时间内不操作,自动登出(也就是session失效了)。
 * 此处的骚操作就是,将这些方法在某次请求中不去刷新缓存失效的时间。
 * 例如: 我在项目中设置session过期时间是 30分钟,但是由于我的项目在不停的请求消息列表接口,导致永远不会登出,以下是结合 spring-redis 作为session 的定制。
 * 不刷新session的过期时间,可以用来过滤一些 轮询请求的接口  
 */  
public static void noFlushSessionExpireTime() {  
    try {  
        HttpSession session = getHttpServletRequest().getSession();  
        Field f = session.getClass().getSuperclass().getDeclaredField("session");  
        f.setAccessible(true);  
        Session realSession = (Session) f.get(session);  
        Field delta = realSession.getClass().getDeclaredField("delta");  
        delta.setAccessible(true);  
        delta.set(realSession, new HashMap<>());  
    } catch (Exception e) {  
        // 捕获抛出的异常,不影响原有的业务逻辑  
    }  
}