前言
在集群系统中,经常会需要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中。