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;
}
}