一篇文章认清Spring-Redis-Session机制

3,831 阅读5分钟

1.传统Session与Spring Session对比

传统容器session与应用绑定,保存在应用内存中,与容器形成一对一关系,如果多应用时无法实现session共享,比如session中保存用户信息,Spring Session通过巧妙的方式将session保存到一个公共的区域,支持可配置化方式,实现SessionRepository接口,可将session保存到Redis、Jdbc、Mongo等,图1表示两者的区别

image.png (图1)

2. 先看下如何使用

2.1 在pom.xml加入依赖并安装redis服务
   <!--spring-session-core  和 spring-data-redis集成包 用于分布式session管理-->
    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    <!-- spring boot redis starter 用于自动装配-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
2.2 在SpringBoot启动类加上自动装配参数
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60*30)
@SpringBootApplication
public class RedisTestApplication{
   public static void main(String[] args) {
        SpringApplication.run(RedisTestApplication.class, args);
    }
}
2.3 在application.yml上加上redis连接信息
spring:
  redis:
    database: 0
    host: 192.168.1.123
    port: 6379
    password: *******

3. 开始揭密

3.1 对类的继承关系有个了解

SessionRepositoryRequestWrapper 与 SessionRepositoryResponseWrapper 是SessionRepositoryFilter的内部类, SessionRepositoryRequestWrapper 通过继承HttpServletRequestWrapper,采用装饰者模式,来扩展已有功能

HttpServletRequestWrapper中持有一个HttpServletRequest对象,然后实现HttpServletRequest接口的所有方法,所有方法实现中都是调用持有的HttpServletRequest对象的相应的方法。继承HttpServletRequestWrapper 可以对其重写。SessionRepositoryRequestWrapper继承HttpServletRequestWrapper,在构造方法中将原有的HttpServletRequest通过调用super完成对HttpServletRequestWrapper中持有的HttpServletRequest初始化赋值,然后重写和session相关的方法。这样就保证SessionRepositoryRequestWrapper的其他方法调用都是使用原有的HttpServletRequest的数据,只有session相关的是重写的逻辑。

Request相关类图 image.png Response相关类图

image.png

3.2 开启自动装配
@EnableRedisHttpSession
3.3 开启RedisHttpSessionConfiguration
#导入RedisHttpSessionConfiguration这个 Spring Bean
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession

通过 RedisHttpSessionConfiguration 自动装配 RedisHttpSessionConfiguration

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration{
    #定义sessionRepository,些Bean实现了SessionRepository,这个就是管理session 贮藏的接口,不同的贮藏方式会对应不同的方式
    @Bean
    public RedisOperationsSessionRepository sessionRepository() {
        #创建内置RedisTemplate,如我们业务操作Redis,可配置这个Bean
        RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
        #调用构造法创建对象
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(redisTemplate);
        sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
        if (this.defaultRedisSerializer != null) {
          sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
        }
        #设置默认session失效时间
        sessionRepository
                     .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
        if (StringUtils.hasText(this.redisNamespace)) {
                sessionRepository.setRedisKeyNamespace(this.redisNamespace);
        }
        #设置redis刷新模式
        sessionRepository.setRedisFlushMode(this.redisFlushMode);
        #设置redis操作的数据库槽,默认为0
        int database = resolveDatabase();
        sessionRepository.setDatabase(database);
        return sessionRepository;
    }
  
     //设置连接工厂采用 lettuce 还是 jedis,默认是 lettuce
    @Autowired
    public void setRedisConnectionFactory(
                    @SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory> springSessionRedisConnectionFactory,
                    ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
        RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory
                        .getIfAvailable();
        if (redisConnectionFactoryToUse == null) {
                redisConnectionFactoryToUse = redisConnectionFactory.getObject();
        }
        this.redisConnectionFactory = redisConnectionFactoryToUse;
    }
}
3.4 session 贮藏对应的接口层定义,分别对应session的增删改查,由不同贮藏方式自我实现
public interface SessionRepository<S extends Session> {
    S createSession();
    void save(S session);
    S findById(String id);
    void deleteById(String id);
}
3.5 通过redis方式实现的存储 RedisOperationsSessionRepository
public class RedisOperationsSessionRepository  {
           
     private final RedisOperations<Object, Object> sessionRedisOperations; 
     public RedisOperationsSessionRepository(
			RedisOperations<Object, Object> sessionRedisOperations) {
            //构造sessionTemplate         
            this.sessionRedisOperations = sessionRedisOperations;
            //创建失效策略
            this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations,
                            this::getExpirationsKey, this::getSessionKey);
            configureSessionChannels();
	}
                
   
}
3.6 通过Servlet Api既可拿到redis中session,而不是原来的HttpSession,使用方式不变,这就是Spring-session 采用适配器模式来达到目地
 HttpSession session = request.getSession(false);

3.7 其中的执行原理

image.png

3.8 分析 SessionRepositoryFilter,这是进行请求拦截改写HttpSession的入口,这是一个标准的Filter,通过责任链方式进行请求和响应处理,在处理请求前对httpServletRequest与httpServletResponse包装
@Override
protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
    //保存sessionRepository
    request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
    //构建Request与Response的包装类
    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                    request, response, this.servletContext);
    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                    wrappedRequest, response);

    try {
        //filter链式调用
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    }
    finally {
        //确保resonse在返回给client之前,提交session
        wrappedRequest.commitSession();
    }
}

request.getSession(false) 会调用 SessionRepositoryRequestWrapper. getSession()

@Override
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.setNew(false);
            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)"));
    }
    //调用sessionRepository创建session
    S session = SessionRepositoryFilter.this.sessionRepository.createSession();
    //设置最后访问时间
    session.setLastAccessedTime(Instant.now());
    //对redis进行包装
    currentSession = new HttpSessionWrapper(session, getServletContext());
    setCurrentSession(currentSession);
    return currentSession;
}

看下 session 结构图

image.png

看doc文档,Session接口是对所有Session相关类的一种定义, RedisSession内部 通过适配器模式重写HttpSession相关的方法,使用方法与原来一样,实际是操作RedisSession, RedisSession是MapSession是装饰器模式,MapSession是对Session的默认实现

/**
 * Provides a way to identify a user in an agnostic way. This allows the session to be
 * used by an HttpSession, WebSocket Session, or even non web related sessions.
 *
 * @author Rob Winch
 * @author Vedran Pavic
 * @since 1.0
 */
public interface Session {
    ...
}

了解下RedisSession的三种key, 创建一个RedisSession,会同时创建三个key-value

spring:session:sessions:sff1b336-dd96-4b33-11133d-df9424vgsdffe spring:session:sessions:expires:sff1b336-dd96-4b33-11133d-df9424vgsdffe spring:session:expirations:21233245080000


/**
 * The default namespace for each key and channel in Redis used by Spring Session.
 */
public static final String DEFAULT_NAMESPACE = "spring:session";
        
private String namespace = DEFAULT_NAMESPACE + ":";

/**
 * Gets the Hash key for this session by prefixing it appropriately.
 *
 * @param sessionId the session id
 * @return the Hash key for this session by prefixing it appropriately.
 */
String getSessionKey(String sessionId) {
    return this.namespace + "sessions:" + sessionId;
}

String getExpirationsKey(long expiration) {
    return this.namespace + "expirations:" + expiration;
}

private String getExpiredKey(String sessionId) {
        return getExpiredKeyPrefix() + sessionId;
}
private String getExpiredKeyPrefix() {
    return this.namespace + "sessions:expires:";
}

在filter finally中调用 SessionRepositoryRequestWrapper提交commitSession

private void commitSession() {
    HttpSessionWrapper wrappedSession = getCurrentSession();
    if (wrappedSession == null) {
        if (isInvalidateClientSession()) {
                SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
                                this.response);
        }
    }
    else {
        S session = wrappedSession.getSession();
        //清除当前request对象中session
        clearRequestedSessionCache();
        //调用sessionRepository来保存session
        SessionRepositoryFilter.this.sessionRepository.save(session);
        String sessionId = session.getId();
        if (!isRequestedSessionIdValid()
                        || !sessionId.equals(getRequestedSessionId())) {
                    SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
                                this.response, sessionId);
        }
    }
}

SessionRepositoryRequestWrapper.save()保存 session

 //保存session到redis            
    @Override
    public void save(RedisSession session) {
        session.save();
        if (session.isNew()) {
                String sessionCreatedKey = getSessionCreatedChannel(session.getId());
                this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
                session.setNew(false);
        }
    }
    
    //内部类,通过装饰模式包装MapSession,实现对MapSession操作
    final class RedisSession implements Session {
        private final MapSession cached;
        private Instant originalLastAccessTime;
        private Map<String, Object> delta = new HashMap<>();
        private boolean isNew;
        private String originalPrincipalName;
        private String originalSessionId;
        ...
        private void save() {
            saveChangeSessionId();
            saveDelta();
        }
        
        //开始保存session 中delta map数据到redis
        private void saveDelta() {
            if (this.delta.isEmpty()) {
                return;
            }
            String sessionId = getId();
            //调用底层redis connection 保存hashmap值
            getSessionBoundHashOperations(sessionId).putAll(this.delta);
            ...
        }
    
    }