Session共享

Session共享及Session保持或者叫做Session⼀致性

session共享的问题:

1. Session问题原因分析

出现这个问题的原因,从根本上来说是因为Http协议是⽆状态的协议。客户端和服务端在某次会话中产⽣的数据不会被保留下来,所以第⼆次请求服务端⽆法认识到你曾经来过, Http为什么要设计为⽆状态协议?早期都是静态⻚⾯⽆所谓有⽆状态,后来有动态的内容更丰富,就需要有状态,出现了两种⽤于保持Http状态的技术,那就是Cookie和Session。

2. 解决Session⼀致性的⽅案

  • Nginx的 IP_Hash 策略(可以使⽤) 同⼀个客户端IP的请求都会被路由到同⼀个⽬标服务器,也叫做会话粘滞 优点:

    • 配置简单,不⼊侵应⽤,不需要额外修改代码 缺点:
    • 服务器重启Session丢失
    • 存在单点负载⾼的⻛险
    • 单点故障问题
  • Session复制(不推荐) 多个tomcat之间通过修改配置⽂件,达到Session之间的复制

优点: - 不⼊侵应⽤ - 便于服务器⽔平扩展 - 能适应各种负载均衡策略 - 服务器重启或者宕机不会造成Session丢失

缺点: - 性能低 - 内存消耗 - 不能存储太多数据,否则数据越多越影响性能 - 延迟性

  • Session共享,Session集中存储

优点: - 能适应各种负载均衡策略 - 服务器重启或者宕机不会造成Session丢失 - 扩展能⼒强 - 适合⼤集群数量使⽤

缺点: - 对应⽤有⼊侵,引⼊了和Redis的交互代码

3. Spring Session

Spring Session使得基于Redis的Session共享应⽤起来⾮常之简单

3.1 引入依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>
3.2 配置redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
3.3 添加注解

该注解可以创建⼀个过滤器使得SpringSession替代HttpSession发挥作⽤,找到那个过滤器!

@SpringBootApplication
@EnableRedisHttpSession
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
3.4 EnableRedisHttpSession
3.4.1 EnableRedisHttpSession
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession {

	/**
	 * The session timeout in seconds. By default, it is set to 1800 seconds (30 minutes).
	 * This should be a non-negative integer.
	 * @return the seconds a session can be inactive before expiring
	 */
	int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

	/**
	 * Defines a unique namespace for keys. The value is used to isolate sessions by
	 * changing the prefix from default {@code spring:session:} to
	 * {@code <redisNamespace>:}.
	 * <p>
	 * For example, if you had an application named "Application A" that needed to keep
	 * the sessions isolated from "Application B" you could set two different values for
	 * the applications and they could function within the same Redis instance.
	 * @return the unique namespace for keys
	 */
	String redisNamespace() default RedisIndexedSessionRepository.DEFAULT_NAMESPACE;

	/**
	 * Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only
	 * updates the backing Redis when {@link SessionRepository#save(Session)} is invoked.
	 * In a web environment this happens just before the HTTP response is committed.
	 * <p>
	 * Setting the value to {@code IMMEDIATE} will ensure that the any updates to the
	 * Session are immediately written to the Redis instance.
	 * @return the {@link RedisFlushMode} to use
	 * @since 1.1
	 * @deprecated since 2.2.0 in favor of {@link #flushMode()}
	 */
	@Deprecated
	RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;

	/**
	 * Flush mode for the Redis sessions. The default is {@code ON_SAVE} which only
	 * updates the backing Redis when {@link SessionRepository#save(Session)} is invoked.
	 * In a web environment this happens just before the HTTP response is committed.
	 * <p>
	 * Setting the value to {@code IMMEDIATE} will ensure that the any updates to the
	 * Session are immediately written to the Redis instance.
	 * @return the {@link FlushMode} to use
	 * @since 2.2.0
	 */
	FlushMode flushMode() default FlushMode.ON_SAVE;

	/**
	 * The cron expression for expired session cleanup job. By default runs every minute.
	 * @return the session cleanup cron expression
	 * @since 2.0.0
	 */
	String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;

	/**
	 * Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which
	 * only saves changes made to session.
	 * @return the save mode
	 * @since 2.2.0
	 */
	SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;

}

通过Import({RedisHttpSessionConfiguration.class}) 导入了 RedisHttpSessionConfiguration 类

3.4.2 RedisHttpSessionConfiguration

3.4.3 SpringHttpSessionConfiguration
@Bean
	public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
			SessionRepository<S> sessionRepository) {
		SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
		sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
		return sessionRepositoryFilter;
	}
3.4.4 SessionRepositoryFilter
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
	......
}

SessionRepositoryFilter既然是个Filter 那就一定有核心方法 doFilter()

@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

		// 对HttPRequest和HttpResponse进行包装
		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
				response);

		try {
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
			// 保存session
			wrappedRequest.commitSession();
		}
	}
3.4.5 SessionRepositoryFilter 包装类中的 getSession()
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 (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;
		}

方法中先从redis中获取session,如果没有就创建一个 RedisSession

3.4.6 doFilterInternal中的保存session wrappedRequest.commitSession();
finally {
	wrappedRequest.commitSession();
}
private void commitSession() {
			HttpSessionWrapper wrappedSession = getCurrentSession();
			if (wrappedSession == null) {
				if (isInvalidateClientSession()) {
					SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
				}
			}
			else {
				S session = wrappedSession.getSession();
				clearRequestedSessionCache();
				// 保存
				SessionRepositoryFilter.this.sessionRepository.save(session);
				String sessionId = session.getId();
				if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
					SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
				}
			}
		}

org.springframework.session.data.redis.RedisSessionRepository#save

@Override
	public void save(RedisSession session) {
		if (!session.isNew) {
			String key = getSessionKey(session.hasChangedSessionId() ? session.originalSessionId : session.getId());
			Boolean sessionExists = this.sessionRedisOperations.hasKey(key);
			if (sessionExists == null || !sessionExists) {
				throw new IllegalStateException("Session was invalidated");
			}
		}
		// 保存
		session.save();
	}
private void save() {
			saveChangeSessionId();
			saveDelta();
			if (this.isNew) {
				this.isNew = false;
			}
		}