Spring Session - raymond-zhao/cat-mall GitHub Wiki

在分布式环境下是如何解决Session共享问题的?

Spring Session Documentation

Session工作原理

Session工作原理

Session 共享问题

  • 同一个服务,复制多份,Session 不同步;
  • 不同服务,Session不共享。

负载均衡后 Session 共享解决方案

  • Session复制

Session复制

  • 客户端存储

客户端存储

  • Nginx负载均衡之 ip_hash

Nginx负载均衡之 ip_hash

  • 统一存储

统一存储

本系统 Session 共享解决方案:利用 Redis 统一存储,当以后 Session 数据量较大的时候可以选择扩充 Redis 集群,主从、哨兵等。另外,由于 Redis 是纯内存操作,速度极快。

Spring Session核心原理

  • 第一次使用 Session,浏览器将会保存相应信息 JSESSIONID 这个 Cookie;
  • 以后浏览器访问网站时将会带上这个网站的 Cookie;
  • 在子域时进行作用域放大,同时 JSON 序列化对象存储到 Redis;

导入依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

配置

spring.session.store-type=redis
@EnableRedisHttpSession
@Configuration
public class MallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        // 放大作用域
        cookieSerializer.setDomainName("catmall.com");
        cookieSerializer.setCookieName("MALLSESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        // JSON 序列化
        return new GenericJackson2JsonRedisSerializer();
    }
}

原理分析

  • @EnableRedisHttpSession注解导入了RedisHttpSessionConfiguration配置
  • RedisHttpSessionConfiguration给容器中添加了一个组件 RedisIndexedSessionRepository,这个类封装了Redis对Session的大量操作
  • RedisHttpSessionConfiguration继承的SpringHttpSessionConfiguration中有一个类型为SessionRepositoryFilterBean,这个Bean最上层的接口就是javax.servlet下的Filter
  • SessionRepositoryFilter继承了抽象类OncePerRequestFilter,这个类下有一个名为doFilterInternal的受保护的抽象方法,SessionRepositoryFilter重写了这个方法,它是Session存储过滤器,每个请求都必须经过filter,它
    • 创建的时候,自动从容器中获取SessionRepository
    • 原始的request被包装成SessionRepositoryRequestWrapper
    • 原始的response被包装成SessionRepositoryResponseWrapper
    • 以后再获取Session,使用wrappedRequest.getSession()SessionRepository中获取(从Redis中获取),而不用从HttpServletRequest.getSession()获取
    • 使用了装饰者Decorator模式
  • 使用Spring Session时,Redis中存储的Session有默认时间,但是如果页面有活动的话也会自动续期。
// 1. 查看入口注解
@EnableRedisHttpSession
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class) // 2. 引入一个配置类
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession { ... }
// 3. RedisHttpSessionConfiguration 向容器中加入了一个 Bean
// RedisIndexedSessionRepository 是 Redis 对 Session 增删改查的工具类
// 4. 这个类还继承了 SpringHttpSessionConfiguration
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
 	@Bean
    public RedisIndexedSessionRepository sessionRepository() {
        RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
        RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
        sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
        if (this.indexResolver != null) {
            sessionRepository.setIndexResolver(this.indexResolver);
        }
        if (this.defaultRedisSerializer != null) {
            sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
        }
        sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
        if (StringUtils.hasText(this.redisNamespace)) {
            sessionRepository.setRedisKeyNamespace(this.redisNamespace);
        }
        sessionRepository.setFlushMode(this.flushMode);
        sessionRepository.setSaveMode(this.saveMode);
        int database = resolveDatabase();
        sessionRepository.setDatabase(database);
        this.sessionRepositoryCustomizers
            .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
        return sessionRepository;
    }   
}
// SpringHttpSessionConfiguration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
    // ...
    
    // 5. Session过滤器,这个类又继承了 SessionRepositoryFilter
    @Bean
	public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
			SessionRepository<S> sessionRepository) {
		SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
		sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
		return sessionRepositoryFilter;
	}
    
    // ...
}
// SessionRepositoryFilter 继承了 OncePerRequestFilter
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
    // ...
}

// 实现了 Filter 接口
abstract class OncePerRequestFilter implements Filter {
    // ...
}

// 这个其实就是位于 javax.servlet 下的原生 Filter
public interface Filter {
    // ...
}
@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);
    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
                                                                                            response);

    try {
        // 包装后的对象应用到了后面的整个执行链
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    }
    finally {
        wrappedRequest.commitSession();
    }
}
⚠️ **GitHub.com Fallback** ⚠️