Spring Session Redis的原理解析(通过Redis实现session共享)

1,055 阅读2分钟

前言

在集群系统中,经常会需要session共享,不然会出现这样一个问题:用户通过机器A登陆系统以后,假如后续的一些操作被负载均衡到机器B上面,机器B发现本机上没有这个用户的session,会强制让用户重新登陆。此时用户会很疑惑,自己明明登陆过了,为什么还要自己重新登陆。 实现session共享的方式有多种,本文主要介绍redis存储session实现共享。

Spring Session使用方式

添加依赖

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

添加注解

@EnableRedisHttpSession(redisNamespace = "demo:spring:session")
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

使用@EnableRedisHttpSession注解就不需要配置RedisSessionConfig。

Demo

@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping("/setValue")
    public void setValue() {
        SessionUtil.getSession().setAttribute("demo", "test1");
    }
    @GetMapping("/getValue")
    public String getValue() {
        return (String) SessionUtil.getSession().getAttribute("demo");
    }
}
public class SessionUtil {
    private static ThreadLocal<HttpServletRequest> threadLocal = new ThreadLocal<>();
    public static void setRequest(HttpServletRequest request) {
        threadLocal.set(request);
    }
    public static HttpServletRequest getRequest() {
        return threadLocal.get();
    }
    public static HttpSession getSession() {
        return getRequest().getSession();
    }
}
@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object obj) throws Exception {
        SessionUtil.setRequest(request);
        return true;
    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Resource
    private TokenInterceptor tokenInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 自定义拦截器,添加拦截路径和排除拦截路径
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
    }
}
#Redis
spring.redis.timeout=3000
spring.redis.host=127.0.0.1
spring.redis.port=6379

测试时,启动两个端口8080和8081,端口8080调用setValue实现将信息存放在session中,端口8081调用getValue从输出test1则说明seesion在两个tomcat之间共享。

Spring Session Redis的原理简析(以版本 2.6.0为例)

看了上面的配置,我们知道了开启Redis Session的关键在于@EnableRedisHttpSession这个注解上。打开@EnableRedisHttpSession的源码:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {
  ...
}

这个@EnableRedisHttpSession注解Import了RedisHttpSessionConfiguration.class这个类,而这个类的作用则是注册一个SessionRepositoryFilter这个bean,该bean由RedisHttpSessionConfiguration子类继承父类SpringHttpSessionConfiguration注入,同时SessionRepositoryFilter的入参是SessionRepository<s>,这个入参由子类RedisHttpSessionConfiguration注入。

@Configuration(proxyBeanMethods = false)
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
      implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
   ...
   @Bean
   public RedisIndexedSessionRepository sessionRepository() {
      RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
      RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
      ...
      return sessionRepository;
   }
   ...
}
@Configuration(proxyBeanMethods = false)
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
   ...
   @Bean
   public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
         SessionRepository<S> sessionRepository) {
      SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
      sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
      return sessionRepositoryFilter;
   }
   ...
}

那么注册SessionRepositoryFilter这个bean有什么用呢,查看这个类的源码,就会发现这个 类是为了将HttpServletRequest和HttpServletResponse这两个对象封装成SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper

@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
   public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class.getName();
   private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR + ".CURRENT_SESSION";
   ...
   @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);
      SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response);
      try {
         filterChain.doFilter(wrappedRequest, wrappedResponse);
      }
      finally {
         wrappedRequest.commitSession();
      }
   }
   ...
}
   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();
      }
   }
   private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
      private final HttpServletResponse response;
      private S requestedSession;
      private boolean requestedSessionCached;
      private String requestedSessionId;
      private Boolean requestedSessionIdValid;
      private boolean requestedSessionInvalidated;
      private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response) {
         super(request);
         this.response = response;
      }
      @Override
      public HttpSessionWrapper getSession(boolean create) {
         HttpSessionWrapper currentSession = getCurrentSession();
         if (currentSession != null) {
            return currentSession;
         }
         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)"));
         }
         S session = SessionRepositoryFilter.this.sessionRepository.createSession();
         session.setLastAccessedTime(Instant.now());
         currentSession = new HttpSessionWrapper(session, getServletContext());
         setCurrentSession(currentSession);
         return currentSession;
      }
      @Override
      public HttpSessionWrapper getSession() {
         return getSession(true);
      }
      private HttpSessionWrapper getCurrentSession() {
        return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
      }
      ...
   }

我们发现SessionRepositoryRequestWrapper重写了getSession方法,这个方法的逻辑是先从request的属性中查找,如果找不到;再查找一个key值是"SESSION"的cookie,通过这个cookie拿到sessionId去redis中查找,如果查不到,就直接创建一个RedisSession对象,同步到Redis中。

参考