首页 文章资讯内容详情

Spring系列.@EnableRedisHttpSession原理简析

2026-06-01 4 花语

本文内容纲要:

-什么是Session -分布式Session的解决方案 -SpringSession使用方式 -SpringSessionRedis的原理简析 -简单总结 -遗留问题 -参考

在集群系统中,经常需要将Session进行共享。不然会出现这样一个问题:用户在系统A上登陆以后,假如后续的一些操作被负载均衡到系统B上面,系统B发现本机上没有这个用户的Session,会强制让用户重新登陆。此时用户会很疑惑,自己明明登陆过了,为什么还要自己重新登陆?

什么是Session

这边再普及下Session的概念:Session是服务器端的一个Key-Value的数据结构,经常和Cookie配合,保持用户的登陆会话。客户端在第一次访问服务端的时候,服务端会响应一个SessionId并且将它存入到本地Cookie中,在之后的访问中浏览器会将Cookie中的sessionId放入到请求头中去访问服务器,如果通过这个SessionId没有找到对应的数据那么服务器会创建一个新的SessionId并且响应给客户端。

分布式Session的解决方案

使用Cookie来完成(很明显这种不安全的操作并不可靠,用户信息全都暴露在浏览器端); 使用Nginx中的IP绑定策略(Ip_Hash),同一个IP只能在指定的同一个机器访问(单台机器的负载可能很高,水平添加机器后,请求可能会被重新定位到一台机器上还是会导致Session不能顺利共享); 利用数据库同步Session(本质上和本文推荐的存在Redis中是一样的,但是效率没有存放在Redis中高); 使用Tomcat内置的Session同步(同步可能会产生延迟); 使用Token代替Session(也是比较推荐的方案,但不是本文的重点); 本文推荐使用Spring-Session集成好的解决方案,将Session存放在Redis中进行共享

最后一种方案是本文要介绍的重点。

SpringSession使用方式

添加依赖

<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) publicclassRedisSessionConfig{ }

maxInactiveIntervalInSeconds:设置Session失效时间,使用RedisSession之后,原SpringBoot的server.session.timeout属性不再生效。

经过上面的配置后,Session调用就会自动去Redis存取。另外,想要达到Session共享的目的,只需要在其他的系统上做同样的配置即可。

SpringSessionRedis的原理简析

看了上面的配置,我们知道开启RedisSession的“秘密”在@EnableRedisHttpSession这个注解上。打开@EnableRedisHttpSession的源码:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(RedisHttpSessionConfiguration.class) @Configuration public@interfaceEnableRedisHttpSession{ //Session默认过期时间,秒为单位,默认30分钟 intmaxInactiveIntervalInSeconds()defaultMapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; //配置key的namespace,默认的是spring:session,如果不同的应用共用一个redis,应该为应用配置不同的namespace,这样才能区分这个Session是来自哪个应用的 StringredisNamespace()defaultRedisOperationsSessionRepository.DEFAULT_NAMESPACE; //配置刷新Redis中Session的方式,默认是ON_SAVE模式,只有当Response提交后才会将Session提交到Redis //这个模式也可以配置成IMMEDIATE模式,这样的话所有对Session的更改会立即更新到Redis RedisFlushModeredisFlushMode()defaultRedisFlushMode.ON_SAVE; //清理过期Session的定时任务默认一分钟一次。 StringcleanupCron()defaultRedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON; }

这个注解的主要作用是注册一个SessionRepositoryFilter,这个Filter会拦截所有的请求,对Session进行操作,具体的操作细节会在后面讲解,这边主要了解这个注解的作用是注册SessionRepositoryFilter就行了。注入SessionRepositoryFilter的代码在RedisHttpSessionConfiguration这个类中。

@Configuration @EnableScheduling publicclassRedisHttpSessionConfigurationextendsSpringHttpSessionConfiguration implementsBeanClassLoaderAware,EmbeddedValueResolverAware,ImportAware, SchedulingConfigurer{ ... }

RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration,SpringHttpSessionConfiguration中注册了SessionRepositoryFilter。见下面代码。

@Configuration publicclassSpringHttpSessionConfigurationimplementsApplicationContextAware{ ... @Bean public<SextendsSession>SessionRepositoryFilter<?extendsSession>springSessionRepositoryFilter( SessionRepository<S>sessionRepository){ SessionRepositoryFilter<S>sessionRepositoryFilter=newSessionRepositoryFilter<>(sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver); returnsessionRepositoryFilter; } ... }

我们发现注册SessionRepositoryFilter时需要一个SessionRepository参数,这个参数是在RedisHttpSessionConfiguration中被注入进入的。

@Configuration @EnableScheduling publicclassRedisHttpSessionConfigurationextendsSpringHttpSessionConfiguration implementsBeanClassLoaderAware,EmbeddedValueResolverAware,ImportAware,SchedulingConfigurer{ @Bean publicRedisOperationsSessionRepositorysessionRepository(){ RedisTemplate<Object,Object>redisTemplate=createRedisTemplate(); RedisOperationsSessionRepositorysessionRepository=newRedisOperationsSessionRepository(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); intdatabase=resolveDatabase(); sessionRepository.setDatabase(database); returnsessionRepository; } }

上面主要讲的就是Spring-Session会自动注册一个SessionRepositoryFilter,这个过滤器会拦截所有的请求。下面就具体看下这个过滤器对拦截下来的请求做了哪些操作。

SessionRepositoryFilter拦截到请求后,会先将request和response对象转换成Spring内部的包装类SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper对象。SessionRepositoryRequestWrapper类重写了原生的getSession方法。代码如下:

@Override publicHttpSessionWrappergetSession(booleancreate){ //通过request的getAttribue方法查找CURRENT_SESSION属性,有直接返回 HttpSessionWrappercurrentSession=getCurrentSession(); if(currentSession!=null){ returncurrentSession; } //查找客户端中一个叫SESSION的cookie,通过sessionRepository对象根据SESSIONID去Redis中查找Session SrequestedSession=getRequestedSession(); if(requestedSession!=null){ if(getAttribute(INVALID_SESSION_ID_ATTR)==null){ requestedSession.setLastAccessedTime(Instant.now()); this.requestedSessionIdValid=true; currentSession=newHttpSessionWrapper(requestedSession,getServletContext()); currentSession.setNew(false); //将Session设置到request属性中 setCurrentSession(currentSession); //返回Session returncurrentSession; } } else{ //Thisisaninvalidsessionid.Noneedtoaskagainif //request.getSessionisinvokedforthedurationofthisrequest if(SESSION_LOGGER.isDebugEnabled()){ SESSION_LOGGER.debug( "Nosessionfoundbyid:CachingresultforgetSession(false)forthisHttpServletRequest."); } setAttribute(INVALID_SESSION_ID_ATTR,"true"); } //不创建Session就直接返回null if(!create){ returnnull; } if(SESSION_LOGGER.isDebugEnabled()){ SESSION_LOGGER.debug( "Anewsessionwascreated.TohelpyoutroubleshootwherethesessionwascreatedweprovidedaStackTrace(thisisnotanerror).YoucanpreventthisfromappearingbydisablingDEBUGloggingfor" +SESSION_LOGGER_NAME, newRuntimeException( "Fordebuggingpurposesonly(notanerror)")); } //通过sessionRepository创建RedisSession这个对象,可以看下这个类的源代码,如果 //@EnableRedisHttpSession这个注解中的redisFlushMode模式配置为IMMEDIATE模式,会立即 //将创建的RedisSession同步到Redis中去。默认是不会立即同步的。 Ssession=SessionRepositoryFilter.this.sessionRepository.createSession(); session.setLastAccessedTime(Instant.now()); currentSession=newHttpSessionWrapper(session,getServletContext()); setCurrentSession(currentSession); returncurrentSession; }

当调用SessionRepositoryRequestWrapper对象的getSession方法拿Session的时候,会先从当前请求的属性中查找CURRENT_SESSION属性,如果能拿到直接返回,这样操作能减少Redis操作,提升性能。

到现在为止我们发现如果redisFlushMode配置为ON_SAVE模式的话,Session信息还没被保存到Redis中,那么这个同步操作到底是在哪里执行的呢?

仔细看代码,我们发现SessionRepositoryFilter的doFilterInternal方法最后有一个finally代码块,这个代码块的功能就是将Session同步到Redis。

@Override protectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{ request.setAttribute(SESSION_REPOSITORY_ATTR,this.sessionRepository); SessionRepositoryRequestWrapperwrappedRequest=newSessionRepositoryRequestWrapper( request,response,this.servletContext); SessionRepositoryResponseWrapperwrappedResponse=newSessionRepositoryResponseWrapper( wrappedRequest,response); try{ filterChain.doFilter(wrappedRequest,wrappedResponse); } finally{ //将Session同步到Redis,同时这个方法还会将当前的SESSIONID写到cookie中去,同时还会发布一 //SESSION创建事件到队列里面去 wrappedRequest.commitSession(); } }

简单总结

主要的核心类有:

@EnableRedisHttpSession:开启Session共享功能; RedisHttpSessionConfiguration:配置类,一般不需要我们自己配置,主要功能是配置SessionRepositoryFilter和RedisOperationsSessionRepository这两个Bean; SessionRepositoryFilter:拦截器,Spring-Session框架的核心; 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中。

说的简单点就是:拦截请求,将之前在服务器内存中进行Session创建销毁的动作,改成在Redis中创建。

遗留问题

清理过期Session的功能怎么实现的 自定义HttpSessionStrategy

参考

https://www.cnblogs.com/SimpleWu/p/10118674.html

本文内容总结:什么是Session,分布式Session的解决方案,SpringSession使用方式,SpringSessionRedis的原理简析,简单总结,遗留问题,参考,

原文链接:https://www.cnblogs.com/54chensongxia/p/12096493.html