Spring系列.@EnableRedisHttpSession原理簡析


在集群系統中,經常需要將 Session 進行共享。不然會出現這樣一個問題:用戶在系統A上登陸以后,假如后續的一些操作被負載均衡到系統B上面,系統B發現本機上沒有這個用戶的 Session ,會強制讓用戶重新登陸。此時用戶會很疑惑,自己明明登陸過了,為什么還要自己重新登陸?

什么是 Session

這邊再普及下 Session 的概念:Session 是服務器端的一個 Key-Value 的數據結構,經常和 Cookie 配合,保持用戶的登陸會話。客戶端在第一次訪問服務端的時候,服務端會響應一個 SessionId 並且將它存入到本地 Cookie 中,在之后的訪問中瀏覽器會將 Cookie 中的 sessionId 放入到請求頭中去訪問服務器,如果通過這個 SessionId 沒有找到對應的數據那么服務器會創建一個新的SessionId並且響應給客戶端。

分布式 Session 的解決方案

  • 使用 Cookie 來完成(很明顯這種不安全的操作並不可靠,用戶信息全都暴露在瀏覽器端);
  • 使用 Nginx 中的 IP 綁定策略(Ip_Hash),同一個 IP 只能在指定的同一個機器訪問(單台機器的負載可能很高,水平添加機器后,請求可能會被重新定位到一台機器上還是會導致 Session 不能順利共享);
  • 利用數據庫同步 Session(本質上和本文推薦的存在 Redis 中是一樣的,但是效率沒有存放在 Redis 中高);
  • 使用 Tomcat 內置的 Session 同步(同步可能會產生延遲);
  • 使用 Token 代替 Session(也是比較推薦的方案,但不是本文的重點);
  • 本文推薦使用 Spring-Session 集成好的解決方案,將Session存放在Redis中進行共享

最后一種方案是本文要介紹的重點。

Spring Session使用方式

添加依賴

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
</dependency>

添加注解@EnableRedisHttpSession

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class RedisSessionConfig {
}

maxInactiveIntervalInSeconds: 設置 Session 失效時間,使用 Redis Session 之后,原 Spring Boot 的 server.session.timeout 屬性不再生效。

經過上面的配置后,Session 調用就會自動去Redis存取。另外,想要達到 Session 共享的目的,只需要在其他的系統上做同樣的配置即可。

Spring Session Redis 的原理簡析

看了上面的配置,我們知道開啟 Redis Session 的“秘密”在 @EnableRedisHttpSession 這個注解上。打開 @EnableRedisHttpSession 的源碼:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
    //Session默認過期時間,秒為單位,默認30分鍾
    int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
    //配置key的namespace,默認的是spring:session,如果不同的應用共用一個redis,應該為應用配置不同的namespace,這樣才能區分這個Session是來自哪個應用的
    String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
    //配置刷新Redis中Session的方式,默認是ON_SAVE模式,只有當Response提交后才會將Session提交到Redis
    //這個模式也可以配置成IMMEDIATE模式,這樣的話所有對Session的更改會立即更新到Redis
    RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
    //清理過期Session的定時任務默認一分鍾一次。
   String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
}

這個注解的主要作用是注冊一個 SessionRepositoryFilter,這個 Filter 會攔截所有的請求,對 Session 進行操作,具體的操作細節會在后面講解,這邊主要了解這個注解的作用是注冊 SessionRepositoryFilter 就行了。注入 SessionRepositoryFilter 的代碼在 RedisHttpSessionConfiguration 這個類中。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
		SchedulingConfigurer {
            ...
}

RedisHttpSessionConfiguration 繼承了 SpringHttpSessionConfiguration,SpringHttpSessionConfiguration 中注冊了 SessionRepositoryFilter。見下面代碼。

@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
	...
    @Bean
    public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
			SessionRepository<S> sessionRepository) {
		SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
		sessionRepositoryFilter.setServletContext(this.servletContext);
		sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
		return sessionRepositoryFilter;
	}
     ...
}

我們發現注冊 SessionRepositoryFilter 時需要一個 SessionRepository 參數,這個參數是在 RedisHttpSessionConfiguration 中被注入進入的。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,SchedulingConfigurer {
            
  @Bean
	public RedisOperationsSessionRepository sessionRepository() {
		RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
		RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
		sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
		if (this.defaultRedisSerializer != null) {
			sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
		}
		sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
		if (StringUtils.hasText(this.redisNamespace)) {
			sessionRepository.setRedisKeyNamespace(this.redisNamespace);
		}
		sessionRepository.setRedisFlushMode(this.redisFlushMode);
		int database = resolveDatabase();
		sessionRepository.setDatabase(database);
		return sessionRepository;
	}    
}

上面主要講的就是 Spring-Session 會自動注冊一個 SessionRepositoryFilter ,這個過濾器會攔截所有的請求。下面就具體看下這個過濾器對攔截下來的請求做了哪些操作。

SessionRepositoryFilter 攔截到請求后,會先將 request 和 response 對象轉換成 Spring 內部的包裝類 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 對象。SessionRepositoryRequestWrapper 類重寫了原生的getSession方法。代碼如下:

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

@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 中創建。

遺留問題

  • 清理過期 Session 的功能怎么實現的
  • 自定義 HttpSessionStrategy

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM