源码篇|Spring Session 的session读写

409 阅读3分钟

前言

http 是无状态的,我们可以使用客户端的 cookie 和服务器端的 session 来保持会话信息。但是在分布式架构下,涉及到 session 同步的问题。客户端向服务器集群A和B发送请求,首先向A发送请求,生成一个 session 存储在服务器A,再次发送请求打到服务器B,就没有原先的会话信息,因为服务器A和B的 session 并没有同步。Spring Session 是解决这种问题的一种方法。

Spring Session 提供了和 HttpSession 的集成,可以选择不同的存储容器,一般选择 redis 作为存储容器,将会话信息存储到 redis 中来解决 session 同步的问题。首先设想下如果是让我们自己来实现Spring Session,存储到 redis 中,我们会怎么做呢?对于 session 的写操作,先写到本地服务器,再写到 redis 服务器;对于读操作,先在本地读,如果没有,再去 redis 读,如果有的话,同步到本地,下次就不用去 redis 读了;对于删除和修改操作,如果本地有该 session 就先在本地操作,再到 redis 操作。session 是有过期时间的,所以同样要对存在 redis 里的值设置过期时间。

源码分析

本文是基于使用 @EnableRedisHttpSession 注解分析的源码。

入口

经过断点调试,并查看调用堆栈,可以看到入口是 SessionRepositoryFilter 类的doFilterInternal 方法。在调用到下面的 filterChain.doFilter 方法之后,最后会调用到SessionRepositoryFilter 的 getSession 方法。

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
        SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            wrappedRequest.commitSession();
        }

    }

getSession()

public HttpSessionWrapper getSession(boolean create) {
                        //currentSession 存储在本地服务器
			HttpSessionWrapper currentSession = getCurrentSession();
            /           /如果有,直接返回
			if (currentSession != null) {
				return currentSession;
			}
                       //根据cookie或者header中的sessionId获取存储在数据库的session
			S requestedSession = getRequestedSession();
                        //如果从数据库中获取session不为null
			if (requestedSession != null) {
                                //没有session失效标志
				if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
					requestedSession.setLastAccessedTime(Instant.now());
					this.requestedSessionIdValid = true;
                                        //将session存储在本地服务器,方便下次获取
					currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
					currentSession.markNotNew();
					setCurrentSession(currentSession);
					return currentSession;
				}
			}
                        //否则,设置session失效标志为true
			else {
				setAttribute(INVALID_SESSION_ID_ATTR, "true");
			}
                        //不需要创建,返回null
			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");
			}
			//创建session
			S session = SessionRepositoryFilter.this.sessionRepository.createSession();
                        //设置session上一次访问时间是现在
			session.setLastAccessedTime(Instant.now());
                        //将session存储在本地服务器
			currentSession = new HttpSessionWrapper(session, getServletContext());
			setCurrentSession(currentSession);
			return currentSession;
		}

可以看到,获取 session 的时候首先从 currentSession 获取。currentSession 存储在request 的 attribute 中,request 的 attribute 是一个 ConcurrentHashMap 的结构,存储时 key 是 .CURRENT_SESSION,value 是HttpSessionWrapper 类型的 currentSession。

如果本地服务器获取不到,调用 getRequestSession() 方法。首先会获取sessionId,有两种策略,从 cookie 中获取和从 header 中获取,默认不设置的话是从 cookie 中获取 name 为Session的cookie值。如果不是第一次访问,是有 sessionId 的,根据这个 sessionId 到数据库查找得到对应的 session 返回。

如果数据库中有 session ,会将 session 同步到本地服务器,如果没有且需要创建,会在本地服务器创建 session。

commitSession()

从 getSession() 中看到,如果是第一次访问服务器,会创建 session 并保存到本地服务器,但在 getSession() 里并未写入到数据库。看回入口中的方法,在执行完 filter.doFilter 方法后, finally 块中有一个 commitSession() 方法,这个方法才会将 session 同步到数据库。

private void commitSession() {
                        //获取本地服务器的session
			HttpSessionWrapper wrappedSession = getCurrentSession();
                        //没有直接返回
			if (wrappedSession == null) {
				if (isInvalidateClientSession()) {
					SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
				}
			}
			else {
				S session = wrappedSession.getSession();
				clearRequestedSessionCache();
				//将session保存到数据库中               
                                SessionRepositoryFilter.this.sessionRepository.save(session);
				String sessionId = session.getId();
				if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
					SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
				}
			}
		}

在将 session 保存到数据库时,先判断session的delta属性是否为空。如果是第一次访问服务器,默认会创建一个session,其中delta属性是一个map,存储了 session 的创建时间、失效时间、上一次访问时间。delta属性不为空说明有数据需要同步到数据库,才连接数据库进行写操作。spring session在redis中以hash的数据结构保存。

参考