为什么共享session
- 提升用户体验:如果用户不得已中途换了一台机器可以根据用户信息回复用户断开时的主要的核心状态
- 提供高可用服务:某台服务器宕机对用户可以做到几乎无感知,提供稳定可用的服务
由于互联网时代的到来,大量的互联网用户的涌入,便出现了很多单机无法满足的场景,毕竟单机的并发与性能是有局限性的。于是便催生了分布式应用,分布式服务的出现就必然要解决一个用户登录后的所有操作对后端的分布式服务的每台机器都是可见的。如果说第一次操作请求打在了服务器A上面,第二次请求打在了服务器B上面,同一个用户的操作要对服务器AB都是可见的,而不是说第二次到了服务器B上之后要重新登录与重复操作之前已经处理过的事情。毕竟如今的互联网更是分秒必争的
共享session的实现
共享session的实现,对session进行持久化。例如常见的mysql、redis等等。那么我们来看下springboot+springmvc共享session的实现。我们以redis的实现与背景来逐步走进源码
springboot的自动配置为我们准备好了环境,用户仅需要简单的配置即可使用
- 依赖配置
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 属性配置
# 设置session刷新ON_SAVE(表示在response commit前刷新缓存),IMMEDIATE(表示只要有更新,就刷新缓存)
spring.session.redis.flush-mode=on_save
# 选择使用redis 作为session存储
spring.session.store-type=redis
没错,就是简单的两个配置,我们就做到了session的共享。那么我们来看看springboot的自动配置都为我们做了什么?
度过上篇文章的同学应该记得jetty服务的SessionHandler句柄。没错看这个名字就知道是容器用来处理session的。没看过或者忘记了的同学可以跟着传荣门走一走哈哈https://blog.csdn.net/u010597819/article/details/90745546
看过的同学可以直接跟着我们继续来看下SessionHandler句柄的作用
SessionHandler
Jetty容器启动回调handler:doStart
- handler在Server服务启动时容器同样会管理注册至服务的各种Handler与Filter的启动与销毁,SessionHandler启动doStart
- 获取Jetty Server对象
- 如果sessionCache缓存为空,初始化并缓存
- 从Server实例管理的_beans集合中获取SessionCacheFactory实例,如果为空则使用默认的实现DefaultSessionCache
- 从Server实例管理的_beans集合中获取SessionDataStoreFactory实例,如果为空则使用默认NullSessionDataStore实现
- 如果_sessionIdManager为空
- 获取Server的sessionIdManager对象赋值,如果依然为空,创建默认实现DefaultSessionIdManager
- 将DefaultSessionIdManager注册至Server管理的_beans集合中并启动DefaultSessionIdManager.start
- 将DefaultSessionIdManager添加至当前SessionHandler管理的_beans集合中
- 从Server管理的_beans集合中获取Scheduler实现,如果为空则创建默认的ScheduledExecutorScheduler并启动start
- 如果ContextHandler不为空
- 从上下文中_initParams属性map中获取org.eclipse.jetty.servlet.SessionCookie对应的value值作为cookie name,默认为:JSESSIONID
- 同样从_initParams属性中获取org.eclipse.jetty.servlet.SessionIdPathParameterName对应的value值作为_sessionIdPathParameterName、_sessionIdPathParameterNamePrefix
- 如果_maxCookieAge等于-1,同样尝试从_initParams属性中获取org.eclipse.jetty.servlet.MaxAge对应的value值作为cookie的最大时长
- 同样的方式设置_sessionDomain、_sessionPath、_checkingRemoteSessionIdEncoding
- 创建SessionContext上下文
- 初始化_sessionCache缓存initialize
protected void doStart() throws Exception
{
//check if session management is set up, if not set up HashSessions
final Server server=getServer();
...
synchronized (server)
{
//Get a SessionDataStore and a SessionDataStore, falling back to in-memory sessions only
if (_sessionCache == null)
{
SessionCacheFactory ssFactory = server.getBean(SessionCacheFactory.class);
setSessionCache(ssFactory != null?ssFactory.getSessionCache(this):new DefaultSessionCache(this));
SessionDataStore sds = null;
SessionDataStoreFactory sdsFactory = server.getBean(SessionDataStoreFactory.class);
if (sdsFactory != null)
sds = sdsFactory.getSessionDataStore(this);
else
sds = new NullSessionDataStore();
_sessionCache.setSessionDataStore(sds);
}
if (_sessionIdManager==null)
{
_sessionIdManager=server.getSessionIdManager();
if (_sessionIdManager==null)
{
//create a default SessionIdManager and set it as the shared
//SessionIdManager for the Server, being careful NOT to use
//the webapp context's classloader, otherwise if the context
//is stopped, the classloader is leaked.
ClassLoader serverLoader = server.getClass().getClassLoader();
try
{
Thread.currentThread().setContextClassLoader(serverLoader);
_sessionIdManager=new DefaultSessionIdManager(server);
server.setSessionIdManager(_sessionIdManager);
server.manage(_sessionIdManager);
_sessionIdManager.start();
}
finally
{
Thread.currentThread().setContextClassLoader(_loader);
}
}
// server session id is never managed by this manager
addBean(_sessionIdManager,false);
}
_scheduler = server.getBean(Scheduler.class);
if (_scheduler == null)
{
_scheduler = new ScheduledExecutorScheduler();
_ownScheduler = true;
_scheduler.start();
}
}
// Look for a session cookie name
if (_context!=null)
{
String tmp=_context.getInitParameter(__SessionCookieProperty);
if (tmp!=null)
_sessionCookie=tmp;
tmp=_context.getInitParameter(__SessionIdPathParameterNameProperty);
if (tmp!=null)
setSessionIdPathParameterName(tmp);
// set up the max session cookie age if it isn't already
if (_maxCookieAge==-1)
{
tmp=_context.getInitParameter(__MaxAgeProperty);
if (tmp!=null)
_maxCookieAge=Integer.parseInt(tmp.trim());
}
// set up the session domain if it isn't already
if (_sessionDomain==null)
_sessionDomain=_context.getInitParameter(__SessionDomainProperty);
// set up the sessionPath if it isn't already
if (_sessionPath==null)
_sessionPath=_context.getInitParameter(__SessionPathProperty);
tmp=_context.getInitParameter(__CheckRemoteSessionEncoding);
if (tmp!=null)
_checkingRemoteSessionIdEncoding=Boolean.parseBoolean(tmp);
}
_sessionContext = new SessionContext(_sessionIdManager.getWorkerName(), _context);
_sessionCache.initialize(_sessionContext);
super.doStart();
}
Request请求handle链回调:doScope
- 获取Request请求的SessionHandler中的session与cookie
- 如果存在子节点_nextScope以及_outerScope节点,递归调用
- doHandle调用下个handle节点
- 提交session:complete
- 如果请求是异步的并且是Request请求则建立Session异步监听SessionAsyncListener,等待session操作的回调
- 如果请求不是异步并且不是Request请求,同步提交session:complete,缓存至_sessionCache,如果存在超时配置创建_sessionInactivityTimer超时监控定时任务:SessionInactivityTimer
springboot自动配置SessionAutoConfiguration共享session
- 在DataSourceAutoConfiguration,redis等自动配置之后启动SessionAutoConfiguration自动配置
- 导入SessionConfigurationImportSelector
- 导入所有存储类型配置StoreType
@Configuration
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnClass(Session.class)
@ConditionalOnWebApplication
@EnableConfigurationProperties(SessionProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, HazelcastAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class, MongoAutoConfiguration.class,
RedisAutoConfiguration.class })
@Import({ SessionConfigurationImportSelector.class, SessionRepositoryValidator.class })
public class SessionAutoConfiguration {
/**
* {@link ImportSelector} to add {@link StoreType} configuration classes.
*/
static class SessionConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
StoreType[] types = StoreType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = SessionStoreMappings.getConfigurationClass(types[i]);
}
return imports;
}
}
/**
* Bean used to validate that a {@link SessionRepository} exists and provide a
* meaningful message if that's not the case.
*/
static class SessionRepositoryValidator {
...
}
}
RedisSessionConfiguration配置
- SpringBootRedisHttpSessionConfiguration配置,以及其父类的配置
- 注入SessionProperties配置(前缀为spring.session),配置session,redis namespace命名空间配置,flushmode配置
- SpringHttpSessionConfiguration配置
- cookie的序列化实现,默认为DefaultCookieSerializer
- SessionRepositoryFilter,session资源库过滤器配置,拦截所有SessionRepository资源库的操作,此过滤器实现了Servlet.Filter接口,正是将容器请求的session数据进行持久化操作以实现共享
- RedisHttpSessionConfiguration配置
- EnableScheduling启动类定时任务支持,如果cleanupCron没配置,默认清理表达式为:“0 * * * * *”,即每分钟的0秒时刻会清理一次
- 创建配置RedisOperationsSessionRepository资源库
- 创建配置RedisMessageListenerContainer,监听session的创建、删除、超时
- 创建配置redis连接工厂
- redis任务执行器注入springSessionRedisTaskExecutor
- 注入springSessionRedisSubscriptionExecutor
- 导入EnableRedisHttpSession注解配置的配置项并覆盖当前配置中的配置:maxInactiveIntervalInSeconds、redisNamespace、redisFlushMode、cleanupCron
- 配置定时清理任务,清理超时的session
Session创建
服务使用公司的cas服务,堆栈如下,可以看到在登录时通过cas拦截器获取session,getSession,如果session不存在则创建session
java.lang.Thread.State: RUNNABLE
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:391)
at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:217)
at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:279)
at javax.servlet.http.HttpServletRequestWrapper.getSession(HttpServletRequestWrapper.java:279)
at org.pac4j.core.context.session.J2ESessionStore.getHttpSession(J2ESessionStore.java:24)
at org.pac4j.core.context.session.J2ESessionStore.get(J2ESessionStore.java:34)
at org.pac4j.core.context.session.J2ESessionStore.get(J2ESessionStore.java:19)
at org.pac4j.core.context.WebContext.getSessionAttribute(WebContext.java:94)
at com.....authentication.filters.CasSecurityFilter.doFilter(CasSecurityFilter.java:62)
SessionRepositoryFilter.SessionRepositoryRequestWrapper.getSession
- 当前场景下baseRequest被封装为SessionRepositoryRequestWrapper,getSession(boolean create)被重写
- 如果当前currentSession不为空直接返回,否则继续
- 获取sessionId,SessionRepositoryFilter.getRequestedSessionId,按照session策略httpSessionStrategy获取
- 根据sessionId从session资源库SessionRepositoryFilter.this.sessionRepository(RedisOperationsSessionRepository)中获取session,读取redis
- 如果session为空并且创建标识为false则返回空
- 如果session为空并且创建标识为true则创建session并封装为HttpSessionWrapper返回
- session资源库创建session:RedisOperationsSessionRepository.createSession
SessionRepositoryFilter(springSessionRepositoryFilter)
父类OncePerRequestFilter的doFilter中回调该类的doFilterInternal方法
- 使用SessionRepositoryRequestWrapper包装请求
- 使用SessionRepositoryResponseWrapper包装响应及包装后的请求
- _HttpSessionStrategy_策略(例如:CookieHttpSessionStrategy)将包装后的请求与响应包装为HttpServletRequest、HttpServletResponse
- 调用过滤链
- 提交session:SessionRepositoryRequestWrapper.commitSession,使用_HttpSessionStrategy策略写入sessionId至response并持久化session_
总结
session共享:spring通过SessionRepositoryFilter该过滤器将Request请求封装为SessionRepositoryRequestWrapper类型,该类型Request创建session,由_HttpSessionStrategy策略选择session资源库_RedisOperationsSessionRepository对session进行持久化实现session的共享。当然还有我们的sessionHandler如果再没有cas等配置的情况下默认对session不做任何操作即NullSessionDataStore
jetty特性doScope、doHandle
* <p>For example if Scoped handlers A, B & C were chained together, then
* the calling order would be:</p>
* <pre>
* A.handle(...)
* A.doScope(...)
* B.doScope(...)
* C.doScope(...)
* A.doHandle(...)
* B.doHandle(...)
* C.doHandle(...)
* </pre>
*
* <p>If non scoped handler X was in the chained A, B, X & C, then
* the calling order would be:</p>
* <pre>
* A.handle(...)
* A.doScope(...)
* B.doScope(...)
* C.doScope(...)
* A.doHandle(...)
* B.doHandle(...)
* X.handle(...)
* C.handle(...)
* C.doHandle(...)
* </pre>