在集羣系統中,常常會須要將Session進行共享。否則會出現這樣一個問題:用戶在系統A上登錄之後,假如後續的一些操做被負載均衡到系統B上面,系統B發現本機上沒有這個用戶的Session,會強制讓用戶從新登錄。此時用戶會很疑惑,本身明明登錄過了,爲何還要本身從新登錄。html
這邊再普及下Session的概念:Session是服務器端的一個key-value的數據結構,常常被用戶和cookie配合,保持用戶的登錄回話。客戶端在第一次訪問服務端的時候,服務端會響應一個sessionId而且將它存入到本地cookie中,在以後的訪問會將cookie中的sessionId放入到請求頭中去訪問服務器,若是經過這個sessionid沒有找到對應的數據那麼服務器會建立一個新的sessionid而且響應給客戶端。java
最後一種方案是本文要介紹的重點。redis
添加依賴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共享的目的,只須要在其餘的系統上作一樣的配置便可。安全
看了上面的配置,咱們知道開啓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(); } }
主要的核心類有:
原理簡要總結:
當請求進來的時候,SessionRepositoryFilter會先攔截到請求,將request和Response對象轉換成SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。後續當第一次調用request的getSession方法時,會調用到SessionRepositoryRequestWrapper的getSession方法。這個方法的邏輯是先從request的屬性中查找,若是找不到;再查找一個key值是"SESSION"的cookie,經過這個cookie拿到sessionId去redis中查找,若是查不到,就直接建立一個RedisSession對象,同步到Redis中(同步的時機根據配置來)。