技术背景
超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应
HTTP协议是无状态的协议,一次浏览器和服务器的交互过程就是一次会话,对话完成后,这次会话就结束了,服务器端并不能记住这个人,下次再对话时,服务器端并不知道是上一次的这个人,所以服务端需要记录用户的状态时,就需要用某种机制来识别具体的用户,这个机制就是Session , 当客户端访问服务器的时候, 服务器端先获取session,如果不存在则创建一个session保存到服务器端, 并将session作为reponse的head返回给客户端浏览器,客户端浏览器下次访问的时候,服务器就可以通过请求中的session识别具体用户了
随着网站的演变,技术的发展,网络提供的功能也越来越丰富,分布式和集群成逐渐流程成为了设计趋势.一个已经登录过用户发送请求,可能负载均衡到不同的服务器上,或者访问服务器A时会再访问服务器B, 传统的session管理方式试用于单例模式,对于分布式的后端服务,系统没办法确定此次请求是否来自一个已登录的用户或是未登录的用户
基于这样的现状,我们需要新的可靠的集群分布式/集群session的解决方案来解决这个问题, 这个时候就需要了解另一个概念Session共享
共享Session
, session共享就是是指在一个浏览器访问多个 Web 服务时,服务端的 Session 数据需要共享
1. 基于session复制
服务器间同步,定时同步各个服务器的session信息。 例如通过Ehcache RMI集群, 然后通过其集群同步的方式 ,将session拷贝到其他服务器的当中,这样其他服务器节点获取请求后从请求中得到session 后依然可以从其缓存中找到对应的session,
优点:即使其中1台机器宕机, 不影响其访问其他节点并通过session验证
缺点:可能有一定延时,即还没有完成同步就访问另一个节点,业务会被终止。 当服务器很多时(几十台),这些节点之前互相同步,容易引起网络风暴,
2. 基于绑定
一般是利用hash算法,将同一个IP请求分发到同一台服务器上, 例如nginx 的IP-hash策略,见 Nginx 学习总结 中的负载策略
缺点:这种方式不符合高可用的要求,某台机器宕机了比如节点A, 那么session也就丢失,即使切换到其他节点的机器后也没有session
3. 基于数据库的
优点:使用内存表Heap,提高session操作的读写效率。这个方案的实用性比较强。
缺点:session的并发读写能力取决于数据库的性能,同时需要自己实现session淘汰逻辑,以便定时从数据表中更新、删除 session记录,当并发过高时容易出现表锁。虽然我们可以选择行级锁的表引擎,但不得不否认使用数据库存储Session还是有些杀鸡用牛刀的架势。
4. 基于缓存服务器
session数据保存到Redis等数据库中,设计一个Filter,利用HttpServletRequestWrapper,实现自己的 getSession()方法,替换掉Servlet容器创建和管理HttpSession的实现。Servlet容器启动时加载。
优点: 从缓存中直接获取session,速度比查询更快,提高了读写能力
缺思:自己实现session淘汰逻辑,占用内存,访问量级越大,开销也就越大
Spring-Session
Spring-session是spring旗下的一个项目,把servlet容器实现的httpSession替换为spring-session,专注于解决 session管理问题。可简单快速且无缝的集成到我们的应用中。(Spring Session)
Spring-session的优势:在于开箱即用,具有较强的设计模式。且支持多种持久化方式,其中RedisSession较为成熟,很容易与spring-data-redis整合,同时也支持springboot等spring框 架
spring--session | |
信息存储 | Session的状态是存储在服务器端,客户端只有session id |
资源占用 | 服务器保存认证信息,占用内存,数量级越高开销越大, |
session共享 | 数据库保存session 解决共享问题,Redis、Database、MogonDB等 |
特点 | 遵循servlet规范,同样方式获取session,对应用代码无侵入 |
应用实例
Redis安装: Redis 安装_=PNZ=BeijingL的博客-CSDN博客
SpringBoot+SpringSession实例:Redis 安装_=PNZ=BeijingL的博客-CSDN博客
技术原理
滤器SessionRepositoryFilter 包装HttpServletRequest和HttpServletResponse,利用HttpServletRequestWrapper,实现自己的 getSession()方法,接管创建和管理Session数据的工作
- SessionRepositoryFilter:Servlet规范中Filter接口的实现,用来切换HttpSession至Spring Session,包装HttpServletRequest和HttpServletResponse
- HttpServerletRequest/HttpServletResponse/HttpSessionWrapper包装器:包装原有的HttpServletRequest、HttpServletResponse和Spring Session,实现切换Session和透明继承HttpSession的关键之所在
- 通过request.getSession方法创建session
- Session:Spring Session模块,
- SessionRepository:管理Spring Session的模块,实现创建保存session以及记录session的过期时间
- Storage: session 可以保存到Redis, MogonDB等数据库中
Spring-session的核心思想在于:将session从web容器中剥离,存储在独立的存储服务器中。目前支持多种形式的session存储器:Redis、Database、MogonDB等。session的管理责任委托给spring-session承担。当request进入web容器,根据request获取session时,由spring-session负责存存储器中获取session,如果存在则返回,如果不存在则创建并持久化至存储器中。
源码分析
替换HttpSession
之前提到SpringBoot会创建一个SessionRepositroyFilter 的Bean,这个Bean实现了Filter接口,通过这个接口实现了替换HttpSession, 先看看这个对象是如何实现的,
//1. 每一个请求都会被SessionRepositoryFilter拦截器拦截
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//处理中 HttpServletRequest和HttpServletResponse 被包装成新对象
// SessionRepositoryRequestWrapper 继承了HttpServletRequestWrapper
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
//SessionRepositoryResponseWrapper 继承了HttpServletResponseWrapper
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
//后续处理使用的是包装后的对象实现了替换
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
//请求结束的时候将session保存到数据库里
//其实spring.session.redis.flush-mode配置来决定了是创建session保存还是通过这里保存
//这个配置后续会提到
wrappedRequest.commitSession();
}
}
//....
}
再看看SessionRepositoryRequestWrapper对象, HttpServletRequestWrapper可以创建并得到请求中的session,SessionRepositoryRequestWrapper继承了HttpServletRequestWrapper类并重写了getsession和getsession(boolean create)方法,这样httpsession被替换掉 ,SpringSesion来创建并管理请求中的session信息
public class HttpServletRequestWrapper extends ServletRequestWrapper implements
HttpServletRequest {
/**
* The default behavior of this method is to return getSession(boolean
* create) on the wrapped request object.
*/
@Override
public HttpSession getSession(boolean create) {
return this._getHttpServletRequest().getSession(create);
}
/**
* The default behavior of this method is to return getSession() on the
* wrapped request object.
*/
@Override
public HttpSession getSession() {
return this._getHttpServletRequest().getSession();
}
}
创建和保存Session
看到这里就有新问题了接口SessionRepository 是如何实现创建session?当使用request.getSession()方法的时,这个request其实已经是之前被替换的SessionRepositoryRequestWrapper 对象。 看看SessionRepositoryRequestWrapper对象通过getsession方法来创建session
// HttpServletRequestWrapper 继承了ServletRequestWrapper 并实现了接口HttpServletRequest
// SessionRepositoryRequestWrapper 通过实现HttpServletRequest 接口 getSession方法,
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper{
private SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response) {
super(request);
this.response = response;
}
//重写了HttpServletRequestWrapper#getSession 方法
@Override
public HttpSessionWrapper getSession() {
return getSession(true);//
}
//重写HttpServletRequestWrapper# getSession(boolean create)方法
public HttpSessionWrapper getSession(boolean create) {
//1. 尝试获取HttpSessionWrapper属性中的session
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
//当前session已经纯在则直接返回当前的session
return currentSession;
}
// 2. 从请求中获取session
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 (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)"));
}
//3. 开始创建session
//从这里可以看到通过sessionRepository创建的session
//例如 :RedisSessionRepository 对象实现sessionRepository ,通过其依赖的MapSession 对象产生UUID当做sessionId
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
// 设置当前的session并返回
setCurrentSession(currentSession);
return currentSession;
}
}
通过上面的源码看到sessionRepository$#createSession()实现session创建,具体是如何创建的
Navigate->Type Hyerarchy查看类接口层次会发现RedisSessionRepository实现了这个接口
当配置spring.session.store-type=redis 的时候,使用RedisSessionRepository对象管理session,实现创建和保存
public class RedisSessionRepository implements SessionRepository<RedisSessionRepository.RedisSession> {
//properties配置项spring.session.redis.namespace为空的时候会使用的默认配置项
//这样默认的redis保存的key格式为spring:session:生成的sessionId
private static final String DEFAULT_KEY_NAMESPACE = "spring:session:";
//ON_SAVE表示请求结束finally方法中commitSession的时候保存(默认保存session的方式)
//IMMEDIATE表示创建sessionId后就直接保存到redis中
//也可以通过properties配置项spring.session.redis.flush-mode修改
private FlushMode flushMode = FlushMode.ON_SAVE;
@Override
public RedisSession createSession() {
//MapSession对象主要是保存session属性,通过map方式保存
//新建时,会通过UUID获取一个随机编码
MapSession cached = new MapSession();
cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);
//创建RedisSession
RedisSession session = new RedisSession(cached, true);
//如果FlushMode配置的是IMMEDIATE 调用save方法就保存session信息到redis中
session.flushIfRequired();
return session;
}
private void save() {
//如果sessionId发生变化的的时候reman
saveChangeSessionId();
//通过getSessionKey获取sessionId,然后保存session信息到redis中
saveDelta();
if (this.isNew) {
this.isNew = false;
}
}
//SessionKey的格式keyNamespace + "sessions:" + sessionId(之前生成的UUID号)
private String getSessionKey(String sessionId) {
return this.keyNamespace + "sessions:" + sessionId;
}
//...
}
文章总结
当FlushMode.ON_SAVE的时候getSession创建Session,请求结束时SessionRepositoryFilter 执行保存session到redis中
当FlushMode.IMMEDIATE的时候,getSession创建session后直接就保存到redis中
参考: