Session(Spring-session-data-redis)

https://www.jianshu.com/p/97c714e2ca7d

1、在pom中添加如下依赖

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

2、在yml中配置redis

:此处配置完全是spring-boot-starter-data-redis相关配置参数,跟spring-session-data-redis无关。后者依赖前者

#redis
#spring.redis.host=localhost
#spring.redis.port=6379
#spring.redis.sentinel.master = sentinel-name
#spring.redis.sentinel.nodes = 192.168.1.2:6389,192.168.1.3:6390,192.168.1.4:6387
##公共配置
#spring.redis.password = 123456
#  ; 连接池最大连接数,默认8个,(使用负值表示没有限制)
#spring.redis.pool.max-active = 21
#  ; 连接池最大阻塞等待时间(使用负值表示没有限制)
#spring.redis.pool.max-wait = -1
#  ; 连接池中的最大空闲连接
#spring.redis.pool.max-idle = 8
#  ; 连接池中的最小空闲连接
#spring.redis.pool.min-idle = 0
#  ; 连接超时时间(毫秒)
#spring.redis.timeout = 1000

spring:
    redis:
        password: 123456
        pool:
            max-active: 21
            max-idle: 8
            max-wait: -1
            min-idle: 0
        sentinel:
            master: sentinel-name
            nodes: 192.168.1.2:6389,192.168.1.3:6390,192.168.1.4:6387
        timeout: 1000

3、在项目启动类上添加如下注解

@EnableRedisHttpSession

EnableRedisHttpSession源码解析

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
    //Session默认过期时间,秒为单位,默认30分钟
    int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
    //配置key的namespace,默认的是spring:session,如果不同的应用共用一个redis,应该为应用配置不同的namespace,这样才能区分这个Session是来自哪个应用的
    String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
    //配置刷新Redis中Session的方式,默认是ON_SAVE模式,只有当Response提交后才会将Session提交到Redis
    //这个模式也可以配置成IMMEDIATE模式,这样的话所有对Session的更改会立即更新到Redis
    RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
    //清理过期Session的定时任务默认一分钟一次。
    String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
}

原理

上面的注解的主要作用是注册一个SessionRepositoryFilter,这个Filter会拦截到所有的请求,对Session进行操作,具体的操作细节会在后面讲解,这边主要了解这个注解的作用是注册SessionRepositoryFilter就行了。注入SessionRepositoryFilter的代码在RedisHttpSessionConfiguration这个类中。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
        SchedulingConfigurer {
            ...
        }

RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration,SpringHttpSessionConfiguration中注册了SessionRepositoryFilter。见下面代码。

@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
    ...
     @Bean
    public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
            SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(
                sessionRepository);
        sessionRepositoryFilter.setServletContext(this.servletContext);
        sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
        return sessionRepositoryFilter;
    }
     ...
}

我们发现注册SessionRepositoryFilter时需要一个SessionRepository参数,这个参数是在RedisHttpSessionConfiguration中被注入进入的。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
        SchedulingConfigurer {
            ...
            @Bean
    public RedisOperationsSessionRepository sessionRepository() {
        RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
                redisTemplate);
        sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
        if (this.defaultRedisSerializer != null) {
            sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
        }
        sessionRepository
                .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
        if (StringUtils.hasText(this.redisNamespace)) {
            sessionRepository.setRedisKeyNamespace(this.redisNamespace);
        }
        sessionRepository.setRedisFlushMode(this.redisFlushMode);
        int database = resolveDatabase();
        sessionRepository.setDatabase(database);
        return sessionRepository;
    }    
          ...
        }

请求进来的时候拦截器会先将request和response拦截住,然后将这两个对象转换成Spring内部的包装类SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper对象。SessionRepositoryRequestWrapper类重写了原生的getSession方法。代码如下:

  @Override
        public HttpSessionWrapper getSession(boolean create) {
             //通过request的getAttribue方法查找CURRENT_SESSION属性,有直接返回
            HttpSessionWrapper currentSession = getCurrentSession();
            if (currentSession != null) {
                return currentSession;
            }
             //查找客户端中一个叫SESSION的cookie,通过sessionRepository对象根据SESSIONID去Redis中查找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.setNew(false);
                      //将Session设置到request属性中
                    setCurrentSession(currentSession);
                      //返回Session
                    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");
            }
             //不创建Session就直接返回null
            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)"));
            }
             //通过sessionRepository创建RedisSession这个对象,可以看下这个类的源代码,如果
             //@EnableRedisHttpSession这个注解中的redisFlushMode模式配置为IMMEDIATE模式,会立即
             //将创建的RedisSession同步到Redis中去。默认是不会立即同步的。
            S session = SessionRepositoryFilter.this.sessionRepository.createSession();
            session.setLastAccessedTime(Instant.now());
            currentSession = new HttpSessionWrapper(session, getServletContext());
            setCurrentSession(currentSession);
            return currentSession;
        }

当调用SessionRepositoryRequestWrapper对象的getSession方法拿Session的时候,会先从当前请求的属性中查找.CURRENT_SESSION属性,如果能拿到直接返回,这样操作能减少Redis操作,提升性能。

到现在为止我们发现如果redisFlushMode配置为ON_SAVE模式的话,Session信息还没被保存到Redis中,那么这个同步操作到底是在哪里执行的呢?我们发现SessionRepositoryFilter的doFilterInternal方法最后有一个finally代码块,这个代码块的功能就是将Session同步到Redis。

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

        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                request, response, this.servletContext);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                wrappedRequest, response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        }
        finally {
             //将Session同步到Redis,同时这个方法还会将当前的SESSIONID写到cookie中去,同时还会发布一
             //SESSION创建事件到队列里面去
            wrappedRequest.commitSession();
        }
    }

总结

主要的核心类有:

  • @EnableRedisHttpSession:开启Session共享功能
  • RedisHttpSessionConfiguration:配置类,一般不需要我们自己配置。主要功能是配置SessionRepositoryFilter和RedisOperationsSessionRepository这两个Bean
  • SessionRepositoryFilter:拦截器
  • RedisOperationsSessionRepository:可以认为是一个Redis操作的客户端,有在Redis中增删改查Session的功能
  • SessionRepositoryRequestWrapper:Request的包装类,主要是重写了getSession方法
  • SessionRepositoryResponseWrapper:Response的包装类。

原理简要总结:

当请求进来的时候,SessionRepositoryFilter会先拦截到请求,将request和Response对象转换成SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。后续当第一次调用request的getSession方法时,会调用到SessionRepositoryRequestWrapper的getSession方法。这个方法的逻辑是先从request的属性中查找,如果找不到;再查找一个key值是"SESSION"的cookie,通过这个cookie拿到sessionId去redis中查找,如果查不到,就直接创建一个RedisSession对象,同步到Redis中(同步的时机根据配置来)。



作者:jackcooper
链接:https://www.jianshu.com/p/97c714e2ca7d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/qq_27088383/article/details/107767000