Spring系列.@EnableRedisHttpSession原理簡析


在集羣系統中,常常會須要將Session進行共享。否則會出現這樣一個問題:用戶在系統A上登錄之後,假如後續的一些操做被負載均衡到系統B上面,系統B發現本機上沒有這個用戶的Session,會強制讓用戶從新登錄。此時用戶會很疑惑,本身明明登錄過了,爲何還要本身從新登錄。html


什麼是Session

這邊再普及下Session的概念:Session是服務器端的一個key-value的數據結構,常常被用戶和cookie配合,保持用戶的登錄回話。客戶端在第一次訪問服務端的時候,服務端會響應一個sessionId而且將它存入到本地cookie中,在以後的訪問會將cookie中的sessionId放入到請求頭中去訪問服務器,若是經過這個sessionid沒有找到對應的數據那麼服務器會建立一個新的sessionid而且響應給客戶端。java

分佈式Session的解決方案

  • 使用cookie來完成(很明顯這種不安全的操做並不可靠)
  • 使用Nginx中的ip綁定策略,同一個ip只能在指定的同一個機器訪問(不支持負載均衡)
  • 利用數據庫同步session(效率不高)
  • 使用tomcat內置的session同步(同步可能會產生延遲)
  • 使用token代替session
  • 咱們使用spring-session以及集成好的解決方案,存放在Redis中

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

Spring Session使用方式

添加依賴spring

<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 屬性再也不生效。tomcat

通過上面的配置後,Session調用就會自動去Redis存取。另外,想要達到Session共享的目的,只須要在其餘的系統上作一樣的配置便可。安全

4. 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這個類中。cookie

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

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

@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;
	}    
          ...
        }

請求進來的時候攔截器會先將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:攔截器
  • 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中(同步的時機根據配置來)。

遺留問題

  • 何時寫的cookie
  • 清理過時Session的功能怎麼實現的
  • 自定義HttpSessionStrategy

參考

相關文章
相關標籤/搜索