美文网首页FBS分布式
分布式Session(Spring-session-data-r

分布式Session(Spring-session-data-r

作者: jackcooper | 来源:发表于2020-03-03 17:20 被阅读0次

    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中(同步的时机根据配置来)。

    相关文章

      网友评论

        本文标题:分布式Session(Spring-session-data-r

        本文链接:https://www.haomeiwen.com/subject/lcihlhtx.html