使用spring-session-data-redis实现HTTP Session集中管理原理

在使用spring-session-data-redis时只需要在@Configuration类上加上@EnableSpringHttpSession即可实现使用Redis集中存储Session。将此批注添加到@Configuration类,以将SessionRepositoryFilter公开为名为“springSessionRepositoryFilter”的bean,并由用户提供的SessionRepository实现提供支持。 为了利用注释,必须提供单个SessionRepository bean。 例如:

@Configuration
@EnableSpringHttpSession
public class SpringHttpSessionConfig {
    @Bean
    public SessionRepository sessionRepository() {
        return new MapSessionRepository();
    }
}

1、导入SpringHttpSessionConfiguration

@Import(SpringHttpSessionConfiguration.class)
@Configuration
public @interface EnableSpringHttpSession {
}

@EnableSpringHttpSession的目的就是为了导入一个Configuration Class,这个配置类定义了一个核心过滤器SessionRepositoryFilter。

@Bean
public  SessionRepositoryFilter springSessionRepositoryFilter(
      SessionRepository sessionRepository) {
   SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(
         sessionRepository);
   sessionRepositoryFilter.setServletContext(this.servletContext);
   if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
      sessionRepositoryFilter.setHttpSessionStrategy(
            (MultiHttpSessionStrategy) this.httpSessionStrategy);
   }
   else {
      sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
   }
   return sessionRepositoryFilter;
}

2、SessionRepositoryFilter原理分析

这个SessionRepositoryFilter的构造方法需要一个SessionRepository,所以需要我们配置一个SessionRepository实例注册到Spring容器中。SessionRepository是管理Session的接口,Spring为我们提供了许多默认的实现稍后讲解。

SessionRepositoryFilter默认会拦截所有的Http请求,然后使用SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper分别包装原始HttpServletRequest对象和HttpServletResponse对象,重写与Session相关的方法。SessionRepositoryFilter有一个成员变量httpSessionStrategy默认是CookieHttpSessionStrategy,SessionRepositoryRequestWrapper有关session的方法会使用到它,因为它的session id会以cookie的形式存储到客户端。

2.1、CookieHttpSessionStrategy

使用spring-session-data-redis实现HTTP Session集中管理原理_第1张图片

2.1.1、简介

一个HttpSessionStrategy实现类,它使用cookie从中获取会话。具体来说,此实现将允许使用setCookieName(String)指定cookie名称。默认为“SESSION”。创建会话时,HTTP响应将具有包含指定cookie名称和会话ID值的cookie。 cookie将被标记为会话cookie,使用cookie路径的上下文路径,标记为HTTPOnly,如果HttpServletRequest.isSecure()返回true,则cookie将被标记为Secure。例如:

HTTP/1.1 200 OK
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly

客户端现在应该通过在请求中指定相同的cookie来在每个请求中包含会话。例如:

GET /messages/ HTTP/1.1
Host: example.com
Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6 

当会话失效时,服务器将发送一个使cookie过期的HTTP响应。例如:

HTTP/1.1 200 OK
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly

支持多个同时会话
默认情况下,还支持多个会话。与浏览器建立会话后,可以通过为setSessionAliasParamName(String)指定唯一值来启动另一个会话。例如,请求: 

GET /messages/?_s=1416195761178 HTTP/1.1
Host: example.com
Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6

将导致以下响应:

HTTP/1.1 200 OK
Set-Cookie: SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d"; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly

要使用原始会话,可以进行不带HTTP参数的请求。要使用新会话,可以使用HTTP参数_s=1416195761178的请求。默认情况下,URL将被重写以包含当前选定的会话。 

2.1.2、getSessionIds方法实现

getSessionIds方法是HttpSessionManager接口定义的用于获取会话别名到会话ID的映射,CookieHttpSessionStrategy是支持多回话的,每个会话都对应一个别名。

public Map getSessionIds(HttpServletRequest request) {
   //读取同名的cookieName(默认是SESSION)的所有cookie value值
   List cookieValues = this.cookieSerializer.readCookieValues(request);
   String sessionCookieValue = cookieValues.isEmpty() ? ""
         : cookieValues.iterator().next();
   Map result = new LinkedHashMap();
   //以空格符分割session 格式为 alias1 sessionId1 alias2 sessionId2 ...
   StringTokenizer tokens = new StringTokenizer(sessionCookieValue,
         this.deserializationDelimiter);
   if (tokens.countTokens() == 1) {
      result.put(DEFAULT_ALIAS, tokens.nextToken());
      return result;
   }
   while (tokens.hasMoreTokens()) {
      String alias = tokens.nextToken();
      if (!tokens.hasMoreTokens()) {
         break;
      }
      String id = tokens.nextToken();
      result.put(alias, id);
   }
   return result;
}

2.1.3、getCurrentSessionAlias方法实现

HttpSessionManager接口方法,从HttpServletRequest取得当前Session的别名,CookieHttpSessionStrategy实现是从request.getParameter("_s")中获取。

public String getCurrentSessionAlias(HttpServletRequest request) {
   //sessionParam默认值是"_s"
   if (this.sessionParam == null) {
      return DEFAULT_ALIAS;
   }
   String u = request.getParameter(this.sessionParam);
   if (u == null) {
      //"0"
      return DEFAULT_ALIAS;
   }
   if (!ALIAS_PATTERN.matcher(u).matches()) {
      return DEFAULT_ALIAS;
   }
   return u;
}

2.1.4、getRequestedSessionId方法实现

 HttpSessionStrategy接口方法,从HttpServletRequest获取请求的会话ID。 例如,会话ID可能来自cookie或请求标头,CookieHttpSessionStrategy实现是从Cookie中获取。

public String getRequestedSessionId(HttpServletRequest request) {
   Map sessionIds = getSessionIds(request);
   String sessionAlias = getCurrentSessionAlias(request);
   return sessionIds.get(sessionAlias);
}

2.1.5、onNewSession方法实现

HttpSessionStrategy接口方法,在创建新会话时调用此方法,并且应该通知客户端新会话ID是什么。 

public void onNewSession(Session session, HttpServletRequest request,
      HttpServletResponse response) {
   //这个集合包含所有已经写入的seesionID
   Set sessionIdsWritten = getSessionIdsWritten(request);
   if (sessionIdsWritten.contains(session.getId())) {
      return;
   }
   //不包含说明是个新Session 加入集合
   sessionIdsWritten.add(session.getId());
   //更新别名与sessionId的映射
   Map sessionIds = getSessionIds(request);
   String sessionAlias = getCurrentSessionAlias(request);
   sessionIds.put(sessionAlias, session.getId());
   //alias1 value1 alias2 value2 ..的格式
   String cookieValue = createSessionCookieValue(sessionIds);
   //写入Cookie
   this.cookieSerializer
         .writeCookieValue(new CookieValue(request, response, cookieValue));
}

@SuppressWarnings("unchecked")
private Set getSessionIdsWritten(HttpServletRequest request) {
   Set sessionsWritten = (Set) request
         .getAttribute(SESSION_IDS_WRITTEN_ATTR);
   if (sessionsWritten == null) {
      sessionsWritten = new HashSet();
      request.setAttribute(SESSION_IDS_WRITTEN_ATTR, sessionsWritten);
   }
   return sessionsWritten;
}

2.1.6、onInvalidateSession方法实现

HttpSessionStrategy接口方法,当会话无效时调用此方法,并且应该通知客户端会话ID不再有效。 例如,它可能会删除其中包含会话ID的cookie,或者设置一个带有空值的HTTP响应标头,指示客户端不再提交该会话ID。

public void onInvalidateSession(HttpServletRequest request,
      HttpServletResponse response) {
   Map sessionIds = getSessionIds(request);
   String requestedAlias = getCurrentSessionAlias(request);
   sessionIds.remove(requestedAlias);
   //Session失效删它后更新Cookie
   String cookieValue = createSessionCookieValue(sessionIds);
   this.cookieSerializer
         .writeCookieValue(new CookieValue(request, response, cookieValue));
}

2.2、SessionRepositoryRequestWrapper

此类主要重写了HttpServletRequest的几个方法:

  1. isRequestedSessionIdValid()
  2. getSession(boolean)
  3. getSession()
  4. getRequestedSessionId()

重写后再调用HttpServletRequest的这些方法后将不再调用我们的Servlet容器的HttpServletRequest是实现了,下面看看重写后的行为。

2.2.1、getRequestedSessionId方法实现

@Override
public String getRequestedSessionId() {
   return SessionRepositoryFilter.this.httpSessionStrategy
         .getRequestedSessionId(this);
}

默认委托给CookieHttpSessionStrategy.getRequestedSessionId(this)实现,从Cookie读取SessionID。

2.2.2、isRequestedSessionIdValid方法实现

@Override
public boolean isRequestedSessionIdValid() {
   if (this.requestedSessionIdValid == null) {
      String sessionId = getRequestedSessionId();
      S session = sessionId == null ? null : getSession(sessionId);
      return isRequestedSessionIdValid(session);
   }

   return this.requestedSessionIdValid;
}

private boolean isRequestedSessionIdValid(S session) {
   if (this.requestedSessionIdValid == null) {
      this.requestedSessionIdValid = session != null;
   }
   return this.requestedSessionIdValid;
}

2.2.3、getSession(boolean create)方法实现

@Override
public HttpSessionWrapper getSession(boolean create) {
   //获取当前Request作用域中代表Session的属性,缓存作用避免每次都从sessionRepository获取
   HttpSessionWrapper currentSession = getCurrentSession();
   if (currentSession != null) {
      return currentSession;
   }
   String requestedSessionId = getRequestedSessionId();
   //客户端存在sessionId 并且未过期
   if (requestedSessionId != null
         && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
      //使用sessionRepository.getSession(sessionId)获取Session
      S session = getSession(requestedSessionId);
      if (session != null) {
         this.requestedSessionIdValid = true;
         currentSession = new HttpSessionWrapper(session, getServletContext());
         currentSession.setNew(false);
         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 (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(System.currentTimeMillis());
   currentSession = new HttpSessionWrapper(session, getServletContext());
   setCurrentSession(currentSession);
   return currentSession;
}

上面有一点需要注意就是将Sesison对象包装成了HttpSessionWrapper,目的是当Session失效时可以从sessionRepository删除。

private final class HttpSessionWrapper extends ExpiringSessionHttpSession {
   HttpSessionWrapper(S session, ServletContext servletContext) {
      super(session, servletContext);
   }
   @Override
   public void invalidate() {
      super.invalidate();
      SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
      setCurrentSession(null);
      SessionRepositoryFilter.this.sessionRepository.delete(getId());
   }
}

2.2.4、getSession()方法实现

@Override
public HttpSessionWrapper getSession() {
   //不存在直接创建
   return getSession(true);
}

2.3、SessionRepositoryResponseWrapper

SessionRepositoryResponseWrapper继承于OnCommittedResponseWrapper实现了抽象方法onResponseCommitted()

private final class SessionRepositoryResponseWrapper
      extends OnCommittedResponseWrapper {
   private final SessionRepositoryRequestWrapper request;
   SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
         HttpServletResponse response) {
      super(response);
      if (request == null) {
         throw new IllegalArgumentException("request cannot be null");
      }
      this.request = request;
   }
   @Override
   protected void onResponseCommitted() {
      this.request.commitSession();
   }
}

那么这个方法何时会被调用呢?

使用spring-session-data-redis实现HTTP Session集中管理原理_第2张图片

当调用SessionRepositoryResponseWrapper的以上方法就会触发onResponseCommitted方法,然后紧接着调用原始父类的相应方法。而onResponseCommitted方法内部是调用this.request.commitSession();它的作用是什么呢?

private void commitSession() {
   HttpSessionWrapper wrappedSession = getCurrentSession();
   if (wrappedSession == null) {
      if (isInvalidateClientSession()) {
         SessionRepositoryFilter.this.httpSessionStrategy
               .onInvalidateSession(this, this.response);
      }
   }
   else {
      S session = wrappedSession.getSession();
      SessionRepositoryFilter.this.sessionRepository.save(session);
      if (!isRequestedSessionIdValid()
            || !session.getId().equals(getRequestedSessionId())) {
         SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
               this, this.response);
      }
   }
}

这个方法的作用就是当前Session存在则使用sessionRepository保存(可能是新Session)或更新(老Session则更新一下避免过期)Session。如果Session不存在并且isInvalidateClientSession()为true说明Session已过期调用httpSessionStrategy .onInvalidateSession(this, this.response);更新Cookie。

commitSession()方法还会在过滤器结束后调用,用来更新Session。

使用spring-session-data-redis实现HTTP Session集中管理原理_第3张图片

3、SessionRepository的实现

通过分析SessionRepositoryFilter的源码知道了对于Session增删改查都是通过SessionRepository实现的,下面就来看一看spring-data提供了哪些实现类。

public interface SessionRepository {
   S createSession();
   void save(S session);
   S getSession(String id);
   void delete(String id);
}

使用spring-session-data-redis实现HTTP Session集中管理原理_第4张图片

3.1、MapSessionRepository

由Map支持并使用MapSession的SessionRepository。 默认情况下使用ConcurrentHashMap,但可以注入自定义Map以使用Redis和Hazelcast等NoSQL厂商提供的分布式Map。该实现不支持触发SessionDeletedEvent或SessionExpiredEvent。

public class MapSessionRepository implements SessionRepository {
   private Integer defaultMaxInactiveInterval;
   private final Map sessions;
   public MapSessionRepository() {
      this(new ConcurrentHashMap());
   }
   public MapSessionRepository(Map sessions) {
      if (sessions == null) {
         throw new IllegalArgumentException("sessions cannot be null");
      }
      this.sessions = sessions;
   }
   public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
      this.defaultMaxInactiveInterval = Integer.valueOf(defaultMaxInactiveInterval);
   }

   public void save(ExpiringSession session) {
      this.sessions.put(session.getId(), new MapSession(session));
   }

   public ExpiringSession getSession(String id) {
      ExpiringSession saved = this.sessions.get(id);
      if (saved == null) {
         return null;
      }
      if (saved.isExpired()) {
         delete(saved.getId());
         return null;
      }
      return new MapSession(saved);
   }

   public void delete(String id) {
      this.sessions.remove(id);
   }

   public ExpiringSession createSession() {
      ExpiringSession result = new MapSession();
      if (this.defaultMaxInactiveInterval != null) {
         result.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);
      }
      return result;
   }
}

3.2、FindByIndexNameSessionRepository

扩展基本SessionRepository以允许通过主体名称查找会话ID。 主体名称由Session属性定义,名称为PRINCIPAL_NAME_INDEX_NAME=FindByIndexNameSessionRepository.class.getName() .concat(".PRINCIPAL_NAME_INDEX_NAME")

public interface FindByIndexNameSessionRepository
      extends SessionRepository {

   //包含当前主体名称(即用户名)的公共会话属性。开发人员有责任确保填充属性,
   //因为Spring Session不知道正在使用的身份验证机制。
   String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()
         .concat(".PRINCIPAL_NAME_INDEX_NAME");

   //查找会话ID的映射到包含名称为PRINCIPAL_NAME_INDEX_NAME的会话属性的所有会话的会话以及指定主体名称的值
   Map findByIndexNameAndIndexValue(String indexName, String indexValue);
}

3.3、RedisOperationsSessionRepository

3.3.1、简介

使用Spring Data的RedisOperations实现的SessionRepository。在Web环境中,这通常与SessionRepositoryFilter结合使用。此实现通过实现MessageListener来支持SessionDeletedEvent和SessionExpiredEvent。
创建一个新实例,下面是一个如何创建新实例的典型示例:

JedisConnectionFactory factory = new JedisConnectionFactory();
RedisOperationsSessionRepository redisSessionRepository = new RedisOperationsSessionRepository(factory);

存储细节
以下部分概述了如何针对每个操作更新Redis。可以在下面找到创建新会话的示例。后续部分描述了详细信息。

HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr2:attrName someAttrValue2
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations1439245080000 2100

保存会话
每个会话都作为哈希存储在Redis中。使用HMSET命令设置和更新每个会话。下面将介绍如何存储每个会话的示例。

HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2

在此示例中,会话后续语句对于会话是真实的:
Session ID为33fdd1b6-b496-4b33-9f7d-df96679d32fe
Session创建自自格林威治标准时间1970年1月1日午夜起,1404360000000毫秒。
Session将在1800秒(30分钟)后到期。
Session最后一次访问时间为自1970年1月1日格林威治标准时间午夜起1404360000000毫秒。
该Session有两个属性。第一个是“attrName”,其值为“someAttrValue”。第二个会话属性名为“attrName2”,其值为“someAttrValue2”。

优化的写入
RedisOperationsSessionRepository.RedisSession会跟踪已更改的属性,并仅更新这些属性。这意味着如果一个属性被写入一次并且多次读取,我们只需要写一次该属性。例如,假设更新了之前的会话属性“sessionAttr2”。保存后将执行以下操作:

HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue

SessionCreatedEvent
创建会话时,会向Redis发送一个事件,其key为"spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe",以“33fdd1b6-b496-4b33-9f7d-df96679d32fe”为sesion ID。事件的主体将是创建的会话。
如果注册了MessageListener,则RedisOperationsSessionRepository将Redis消息转换为SessionCreatedEvent。

过期
根据RedisOperationsSessionRepository.RedisSession.getMaxInactiveIntervalInSeconds(),使用EXPIRE命令将到期与每个会话相关联。例如:
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
您将注意到,设置的到期时间是会话实际到期后的5分钟。这是必要的,以便在会话到期时可以访问会话的值。会话本身在实际到期后五分钟设置到期,以确保它被清理,但仅在我们执行任何必要的处理之后。
注意:getSession(String)方法确保不返回过期的会话。这意味着在使用会话之前无需检查过期时间Spring Session依赖于Redis的过期和删除键空间通知来触发SessionDestroyedEvent。 SessionDestroyedEvent确保清除与Session关联的资源。例如,当使用Spring Session的WebSocket支持时,Redis过期或删除事件会触发与要关闭的会话关联的任何WebSocket连接。
不会直接在会话密钥本身上跟踪到期,因为这意味着会话数据将不再可用。而是使用特殊会话到期密钥。在我们的示例中,expires键是:
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
当会话到期密钥被删除或过期时,密钥空间通知会触发查找实际会话并触发SessionDestroyedEvent。
依赖于Redis过期的一个问题是,如果没有访问密钥,Redis不保证何时将触发过期事件。具体而言,Redis用于清除过期密钥的后台任务是低优先级任务,可能不会触发密钥过期。有关其他详细信息,请参阅Redis文档中的过期事件的时间部分。
为了避免过期事件无法保证发生这一事实,我们可以确保在预期到期时访问每个密钥。这意味着如果密钥上的TTL过期,当我们尝试访问密钥时,Redis将删除密钥并触发过期事件。
因此,每个会话到期也会跟踪到最近的分钟。这允许后台任务访问可能已过期的会话,以确保以更确定的方式触发Redis过期事件。例如:
 SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
 EXPIRE spring:session:expirations1439245080000 2100
然后,后台任务将使用这些映射来显式请求每个会话到期密钥。通过访问密钥而不是删除密钥,我们确保只有在TTL过期时Redis才会删除密钥。
注意:我们没有明确删除密钥,因为在某些情况下,可能存在竞争条件错误地将密钥标识为过期而未过期。如果没有使用分布式锁(这会破坏我们的性能),就无法确保到期映射的一致性。通过简单地访问密钥,我们确保仅在该密钥上的TTL过期时才删除密钥。

4、使用@EnableRedisHttpSession配置Redis存储Session

4.1、@EnableRedisHttpSession的使用

本文开头使用@EnableSpringHttpSession还需要配置一个SessionRepository来决定底层使用什么存储,而使用@EnableRedisHttpSession将此批注添加到@Configuration类,以将SessionRepositoryFilter公开为名为“springSessionRepositoryFilter”的bean,并由Redis提供支持。 为了利用注释,必须提供单个RedisConnectionFactory。 例如:


    org.springframework.session
    spring-session-data-redis
@Configuration
@EnableRedisHttpSession
public class RedisHttpSessionConfig {

    @Bean
    public JedisConnectionFactory connectionFactory() throws Exception {
        return new JedisConnectionFactory();
    }

}

4.2、@EnableSpringHttpSession源码分析

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
   int maxInactiveIntervalInSeconds() default 1800;
   //为键定义唯一的命名空间。 
   //该值用于通过将前缀从"spring:session:"更改为"spring:session::"来隔离会话。 
   //默认值为"",以便所有Redis键以"spring:session"开头。
   //例如,如果您有一个名为“Application A”的应用程序需要将会话与“Application B”隔离,则可以为应用程序设置两个不同的值,它们可以在同一个Redis实例中运行。
   String redisNamespace() default "";

   //设置Redis会话的刷新模式。 
   //默认值为ON_SAVE,仅在调用SessionRepository.save(Session)时更新后备Redis。 
   //在Web环境中,这发生在提交HTTP响应之前。
   //将值设置为IMMEDIATE将确保会话的任何更新立即写入Redis实例。
   RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
}

@EnableRedisHttpSession的主要作用就是导入一个Configuration类RedisHttpSessionConfiguration。

4.3、RedisHttpSessionConfiguration源码分析

RedisHttpSessionConfiguration继承于SpringHttpSessionConfiguration所以就已经配置了SessionRepositoryFilter,而此类本身提供SessionRepository的bean配置。

@Bean
public RedisOperationsSessionRepository sessionRepository(
      @Qualifier("sessionRedisTemplate") RedisOperations sessionRedisTemplate,
      ApplicationEventPublisher applicationEventPublisher) {
   RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
         sessionRedisTemplate);
   sessionRepository.setApplicationEventPublisher(applicationEventPublisher);
   sessionRepository
         .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
   if (this.defaultRedisSerializer != null) {
      sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
   }

   String redisNamespace = getRedisNamespace();
   if (StringUtils.hasText(redisNamespace)) {
      sessionRepository.setRedisKeyNamespace(redisNamespace);
   }

   sessionRepository.setRedisFlushMode(this.redisFlushMode);
   return sessionRepository;
}

这样一来就不需要开发人员主动配置一个RedisOperationsSessionRepository,但是这个配置需要一个RedisOperations,而这个RedisOperations也是定义在这个类中的。

@Bean
public RedisTemplate sessionRedisTemplate(
      RedisConnectionFactory connectionFactory) {
   RedisTemplate template = new RedisTemplate();
   template.setKeySerializer(new StringRedisSerializer());
   template.setHashKeySerializer(new StringRedisSerializer());
   if (this.defaultRedisSerializer != null) {
      template.setDefaultSerializer(this.defaultRedisSerializer);
   }
   template.setConnectionFactory(connectionFactory);
   return template;
}

而这个RedisTemplate依赖一个RedisConnectionFactory是需要开发人员配置的。如果我们使用spring-boot,只需要指定application.properties的spring.redis.cluster.nodes即可为我配置一个redis集群JedisConnectionFactory。具体请参考org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.RedisConnectionConfiguration

 

你可能感兴趣的:(集群,Spring-Data)