# 概述

# springSession解决的问题

传统单机Web应用,用户的session由容器进行管理。当网站逐渐演变,分布式应用和集群开始成为主流,传统的web容器管理用户会话session的方式行不通;
springSession应运而生;

# springSession设计的核心思想

  • 将session从web容器剥离;
  • 与servlet规范无缝结合,仍然使用HttpServletRequest获取session,获取到的仍然是HttpSession类型(适配器模式)
  • Session不再存储在web容器内,外化存储(装饰器模式)

# 源码

# springSession装配流程设计

@Import(SpringHttpSessionConfiguration.class)
public @interface EnableSpringHttpSession {
}
@Configuration(proxyBeanMethods = false)
/*xxx: spring-session配置 */
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
    private List<HttpSessionListener> httpSessionListeners = new ArrayList<>();

    /*xxx: 默认从cookie中解析解析sessionId*/
    private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();

    /*xxx: sessionId的解析器*/
    private HttpSessionIdResolver httpSessionIdResolver = this.defaultHttpSessionIdResolver;
    
    @Bean
	/*xxx: session存储器过滤器,
	   当前的过滤器,设置了过滤器的顺序,默认为 Integer.min_value+50 */
    public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
            SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
        sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
        return sessionRepositoryFilter;
    }

    @Bean
    /*xxx: session事件监听适配器 */
    public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
        return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
    }
}
@Order(Integer.MIN_VALUE + 50)
/*xxx: session存储器过滤器*/
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
    /*xxx: session外置存储器*/
    private final SessionRepository<S> sessionRepository;

    public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        this.sessionRepository = sessionRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        /*xxx: 将当前的 request,response 请求对象进行包装*/
        /*xxx: 包装之后,主要是覆写了 获取session 的逻辑*/
        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
                response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        }
        finally {
            /*xxx: 在当前过滤器执行完成后,会自动持久化session信息,通过各自的 session存储策略 */
            wrappedRequest.commitSession();
        }
    }
}

# springSession获取session流程

/*xxx: 通过sessionRepository进行管理session的请求包装器*/
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {

    @Override
    /*xxx: 覆写了 getSession()方法,改变了 session的生成逻辑*/
    public HttpSessionWrapper getSession() {
        return getSession(true);
    }
    
    @Override
    /*xxx: 覆写了 getSession(boolean)方法*/
    /*xxx: 默认情况下,它是 通过  servlet规范的 Manager 进行操作的*/
    public HttpSessionWrapper getSession(boolean create) {
        /*xxx: 获取当前的session,通过 attribute获取: SessionRepository.CURRENT_SESSION*/
        HttpSessionWrapper currentSession = getCurrentSession();
        if (currentSession != null) {
            return currentSession;
        }

        /*xxx: 从请求中 获取 session信息*/
        S requestedSession = getRequestedSession();
        /*xxx: 获取到session后,需要验证其合法性*/
        if (requestedSession != null) {
            if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
                /*xxx: 刷新session时间*/
            }
        }else{
            setAttribute(INVALID_SESSION_ID_ATTR, "true");
        }
        /*xxx: 本次是否创建*/
        if (!create) {
            return null;
        }

        /*xxx: 通过sessionRepository创建session */
        S session = SessionRepositoryFilter.this.sessionRepository.createSession();
        session.setLastAccessedTime(Instant.now());
        currentSession = new HttpSessionWrapper(session, getServletContext());
        setCurrentSession(currentSession);
        /*xxx: 设置好相应的信息后,返回新创建的session实例*/
        return currentSession;
    }
}

# springSession保存session流程

/*xxx: session存储器过滤器*/
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        /*xxx: 将当前的 request,response 请求对象进行包装*/
        /*xxx: 包装之后,主要是覆写了 获取session 的逻辑*/
        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        }
        finally {
            /*xxx: 在当前过滤器执行完成后,会自动持久化session信息,通过各自的 session存储策略 */
            wrappedRequest.commitSession();
        }
    }
}
/*xxx: 通过sessionRepository进行管理session的请求包装器*/
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
    /*xxx: 该方法会在过滤器执行完成后,处理session的状态*/
    private void commitSession() {
        HttpSessionWrapper wrappedSession = getCurrentSession();
        
        S session = wrappedSession.getSession();
        /*xxx: 保存session至外置存储器中*/
        SessionRepositoryFilter.this.sessionRepository.save(session);
        String sessionId = session.getId();
        /*xxx: 当请求的sessionId非法,或者请求的sessionId 与当前的sessionId不一致,需要重写cookie*/
        if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
            SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
        }
    }
}
public interface HttpSessionIdResolver {
    /*xxx: 解析sessionId*/
    List<String> resolveSessionIds(HttpServletRequest request);
    /*xxx: 设置sessionId*/
    void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId);
    /*xxx:sessionId失效*/
    void expireSession(HttpServletRequest request, HttpServletResponse response);
}
/*xxx: cookieHttpSession解析器,默认依赖于cookie序列化器实现*/
public final class CookieHttpSessionIdResolver implements HttpSessionIdResolver {
    @Override
    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
        this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId));
    }
}

# springSession读写cookie

/*xxx: 默认的cookie序列化器*/
public class DefaultCookieSerializer implements CookieSerializer {

    private boolean useBase64Encoding = true;
    
    @Override
    /*xxx: 写入cookie */
    public void writeCookieValue(CookieValue cookieValue) {
        StringBuilder sb = new StringBuilder();
        sb.append(this.cookieName).append('=');
        String value = getValue(cookieValue);
        /*xxx: 省略其它抽象...*/
    }

    private String getValue(CookieValue cookieValue) {
        String requestedCookieValue = cookieValue.getCookieValue();
        String actualCookieValue = requestedCookieValue;
        /*xxx: 原生容器下的 session,没有这个限制 */
        if (this.useBase64Encoding) {
            actualCookieValue = base64Encode(actualCookieValue);
        }
        return actualCookieValue;
    }

    @Override
    /*xxx: 从cookie中,读取sessionId */
    public List<String> readCookieValues(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        /*xxx: 注意,有可能匹配到多个 sessionId*/
        List<String> matchingCookieValues = new ArrayList<>();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                /*xxx: 如果Cookie的名称为 SESSION*/
                if (this.cookieName.equals(cookie.getName())) {
                    /*xxx: 则将其值进行 base解码,作为sessionId*/
                    String sessionId = (this.useBase64Encoding ? base64Decode(cookie.getValue()) : cookie.getValue());
                    matchingCookieValues.add(sessionId);
                }
            }
            return matchingCookieValues;
        }
    }
}

# 传统容器获取session流程(比对)

//package org.apache.catalina.connector;
public class Request implements HttpServletRequest {
    protected Session session = null;
    
    @Override
    public HttpSession getSession(boolean create) {
        Session session = doGetSession(create);
        return session.getSession();
    }

    protected Session doGetSession(boolean create) {
        /*xxx: 如果之前已经获取过session,并且session是合法的,则直接返回*/
        if (session != null) {
            return session;
        }
        /*xxx: StandardManager实例是从 context中获取的,换言之,session是存在内存中的,并且以Context为单位进行隔离*/
        Manager manager = context.getManager();
        if (requestedSessionId != null) {
            /*xxx: 获取session,实际上是根据 coyoteAdaptor阶段,恢复的sessionId,通过该Session去 Manager中,寻找与之映射的 session实例 */
            session = manager.findSession(requestedSessionId);
            if (session != null) {
                /*xxx: 刷新session的最近访问时间*/
                session.access();
                return session;
            }
        }

        /*xxx: 本次是否创建session*/
        if (!create) {
            return null;
        }

        String sessionId = getRequestedSessionId();
        /*xxx: session的创建,最终是通过 session管理器完成的。 */
        session = manager.createSession(sessionId);

        /*xxx: 当创建session成功后,同时会将当前的session,写入到 response头部中 */
        if (session != null && trackModesIncludesCookie) {
            Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
                    context, session.getIdInternal(), isSecure());

            response.addSessionCookieInternal(cookie);
        }

        session.access();
        return session;
    }
}

# springSession外置存储-逻辑设计

/*xxx: 抽象session*/
public interface Session {
    String getId();

    <T> T getAttribute(String attributeName);

    void setAttribute(String attributeName, Object attributeValue);

    Instant getCreationTime();

    void setLastAccessedTime(Instant lastAccessedTime);

    /*xxx: session有效时长*/
    void setMaxInactiveInterval(Duration interval);

    boolean isExpired();
}

/*xxx: session映射,默认包含一个 id */
/*xxx: 注意,该类是最终类,不可再被扩展*/
public final class MapSession implements Session, Serializable {
    /*xxx: 默认时长为30分钟*/
    private Duration maxInactiveInterval = Duration.ofSeconds(1800);

    public MapSession() {
        this(generateId());
    }

    public MapSession(String id) {
        this.id = id;
        this.originalId = id;
    }

    /*xxx: 生成 sessionId,默认通过uuid实现*/
    private static String generateId() {
        return UUID.randomUUID().toString();
    }
}
/*xxx: jdbc技术实现的session*/
final class JdbcSession implements Session {
    private final Session delegate;

    private void save() {
        /*xxx: 如果是新增session信息,则进行保存*/
        if (this.isNew) {
            JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> {
                /*xxx: 执行两条语句:*/
                /*xxx: INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) VALUES (?, ?, ?, ?, ?, ?, ?)*/
                /*xxx: 注: 最后一个字段: principal_name为 从session中获取的 "org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME"属性的值,或者是 spring-security-context的authentication的name*/
                /*xxx: INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) SELECT PRIMARY_ID, ?, ? FROM %TABLE_NAME% WHERE SESSION_ID = ?*/
            });
        }else{
            /*xxx: 更新session,并对属性做 增删改操作*/
            /*xxx: UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? WHERE PRIMARY_ID = ?*/
            /*xxx: INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) SELECT PRIMARY_ID, ?, ? FROM %TABLE_NAME% WHERE SESSION_ID = ?*/
            /*xxx: UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? WHERE SESSION_PRIMARY_ID = ? AND ATTRIBUTE_NAME = ?*/
            /*xxx: DELETE FROM %TABLE_NAME%_ATTRIBUTES WHERE SESSION_PRIMARY_ID = ? AND ATTRIBUTE_NAME = ?*/
        }
    }

    private void flushIfRequired() {
        if (JdbcIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
            save();
        }
    }
}

final class RedisSession implements Session {
    private final MapSession cached;

    private Map<String, Object> delta = new HashMap<>();

    RedisSession(MapSession cached, boolean isNew) {
        this.cached = cached;
        this.isNew = isNew;
        /*xxx: 省略其它抽象...*/
    }

    private void flushImmediateIfNecessary() {
        if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
            save();
        }
    }

    private void save() {
        /*xxx: 保存 sessionId,以及 所有的 attributes */
        /*xxx: 在这个操作过程中,会将 信息 保存至 redis*/
        saveChangeSessionId();
        saveDelta();
    }

    private void saveChangeSessionId() {
        /*xxx: 执行更新操作*/
        if (!this.isNew) {
            /*xxx: 修改session键名:  即修改  spring:session:sessions:当前的sessionId*/
            RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
                    sessionIdKey);
            
            /*xxx: 修改过期键:  spring:session:sessions:expires:当前的sessionId*/
            RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
        }
    }

    private void saveDelta() {
        if (this.delta.isEmpty()) {
            return;
        }
        /*xxx: 保存属性时,会将所有的信息保存到 hash类型下, 键名为 namespace + sessionId  */
        getSessionBoundHashOperations(sessionId).putAll(this.delta);
        /*xxx: 存储 principalRedisKey 键   spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:当前的principalName名称*/

        /*xxx: 设置失效时间...*/
        /*xxx: 设置了三个失效时间,以30分钟为例*/
        /*xxx: sessionExpire为30分钟自动删除, expirations,session 将在35分钟自动删除*/
        RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
        /*xxx: 存储过期时长的键: spring:session:expirations:有效时长,  这个属性是用于通过  定时器进行清除session */
    }
    
}

/*xxx: session存储器,主要包括session的增、删、查、创建 操作*/
public interface SessionRepository<S extends Session> {
    /*xxx: 创建 session*/
    S createSession();

    /*xxx: 保存session */
    void save(S session);

    /*xxx: 通过id查找session */
    S findById(String id);

    /*xxx: 通过id 删除session */
    void deleteById(String id);
}

/*xxx: 通过索引名查找的session仓库*/
public interface FindByIndexNameSessionRepository<S extends Session> extends SessionRepository<S> {
    /*xxx: 通过索引名,以及索引值进行查找 */
    Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);

    default Map<String, S> findByPrincipalName(String principalName) {
        return findByIndexNameAndIndexValue("org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME", principalName);
    }
}

/*xxx: jdbc实现的 session仓库 */
/*xxx: 涉及到的表包括: SPRING_SESSION,SPRING_SESSION_ATTRIBUTES*/
public class JdbcIndexedSessionRepository
        implements FindByIndexNameSessionRepository<JdbcIndexedSessionRepository.JdbcSession> {
    
    @Override
	/*xxx: jdbc实现的 session仓库,
	   创建session实例 */
    public JdbcSession createSession() {
        /*xxx: 首先 创建 mapSession实例*/
        MapSession delegate = new MapSession();
        if (this.defaultMaxInactiveInterval != null) {
            delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
        }
        /*xxx: 通过 mapSession 创建 jdbcSession实例 */
        JdbcSession session = new JdbcSession(delegate, UUID.randomUUID().toString(), true);
        session.flushIfRequired();
        return session;
    }

    @Override
    /*xxx: 保存 jdbcSession*/
    public void save(final JdbcSession session) {
        /*xxx: 调用 jdbcSession进行保存 */
        session.save();
    }

    @Override
    public JdbcSession findById(final String id) {
        /*xxx: 执行sql查询*/
        /*xxx: SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES FROM %TABLE_NAME% S LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID WHERE S.SESSION_ID = ?*/
    }

    @Override
    /*xxx: 失效后的session剔除*/
    public void deleteById(final String id) {
        /*xxx: 执行sql更新*/
        /*xxx: DELETE FROM %TABLE_NAME% WHERE SESSION_ID = ?*/
    }
    
}

/*xxx: redis实现的 session仓库*/
public class RedisIndexedSessionRepository
        implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
    @Override
    /*xxx: 保存 RedisSession */
    public void save(RedisSession session) {
        session.save();
        /*xxx: 如果session是新建的,会同时发布session创建事件,集群下的其它节点,会收到该事件的广播*/
        if (session.isNew) {
            String sessionCreatedKey = getSessionCreatedChannel(session.getId());
            /*xxx: 发布session创建事件*/
            this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
            session.isNew = false;
        }
    }

    @Override
    public RedisSession findById(String id) {
        return getSession(id, false);
    }
    
    /*xxx: 获取session,从redis中加载属性 */
    private RedisSession getSession(String id, boolean allowExpired) {
        Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
        MapSession loaded = loadSession(id, entries);
        RedisSession result = new RedisSession(loaded, false);
        return result;
    }

    private MapSession loadSession(String id, Map<Object, Object> entries) {
        MapSession loaded = new MapSession(id);
        /*xxx: 将entries的属性进行设置*/
        return loaded;
    }

    private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
        String key = getSessionKey(sessionId);
        return this.sessionRedisOperations.boundHashOps(key);
    }

    @Override
    public void deleteById(String sessionId) {
        RedisSession session = getSession(sessionId, true);

        cleanupPrincipalIndex(session);
        this.expirationPolicy.onDelete(session);

        String expireKey = getExpiredKey(session.getId());
        this.sessionRedisOperations.delete(expireKey);

        session.setMaxInactiveInterval(Duration.ZERO);
        /*xxx: 最后保存了一个空的session*/
        save(session);
    }
}

# redisSession失效机制

# 通过定时任务剔除(属于定期删除,下文有介绍)

/*xxx: redisSession 的配置*/
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {

    private final RedisIndexedSessionRepository sessionRepository;

    /*xxx: 每分钟执行一次清理*/
    private String cleanupCron = "0 * * * * *";
    
    @EnableScheduling
    @Configuration(proxyBeanMethods = false)
    class SessionCleanupConfiguration implements SchedulingConfigurer {
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            /*xxx: 定期清理的调度任务*/
            taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
                    RedisHttpSessionConfiguration.this.cleanupCron);
        }
    }
}

# 通过redis本身机制自动剔除

final class RedisSessionExpirationPolicy {
    void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {

        /*xxx: expireKey: spring:session:expirations:有效时长*/
        String expireKey = getExpirationKey(toExpire);
        BoundSetOperations<Object, Object> expireOperations = this.redis.boundSetOps(expireKey);
        expireOperations.add(keyToExpire);

        /*xxx: 默认将在 sessionExpire失效后的五分钟才失效*/
        expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);

        /*xxx: session本身,也将在 sessionExpire失效后的五分钟,才失效*/
        this.redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
        
        /*xxx: 此时sessionKey表示的是: spring:session:sessions:expires:当前的sessionId*/
        /*xxx: sessionExpire将会在设置的时间失效 */
        this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
    }
}

# 对于session状态变更的监听

@Configuration(proxyBeanMethods = false)
/*xxx: redisSession 的配置*/
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
    @Bean
    /*xxx: 定义 基于 redis的消息监听器*/
    public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
            RedisIndexedSessionRepository sessionRepository) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(this.redisConnectionFactory);

        /*xxx: 订阅 __keyevent@0__:del, __keyevent@0__:expired, spring:session:event:0:created:* 事件 */
        container.addMessageListener(sessionRepository,
                Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
                        new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
        container.addMessageListener(sessionRepository,
                Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));

        return container;
    }
}

/*xxx: redis实现的 session仓库*/
public class RedisIndexedSessionRepository
        implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
    @Override
    /*xxx: 监听事件*/
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        
        if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
            Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
            /*xxx: 处理session创建的事件*/
            handleCreated(loaded, channel);
            return;
        }

        /*xxx: session过期,或者 session被删除,需要执行的动作*/
        boolean isDeleted = channel.equals(this.sessionDeletedChannel);
        if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
            /*xxx: 当 redis的sessionExpire 删除后,需要通知应用程序*/
            /*xxx: 正常情况下,此时还是能获取到session信息的*/
            RedisSession session = getSession(sessionId, true);
            cleanupPrincipalIndex(session);

            if (isDeleted) {
                /*xxx: 处理session删除*/
                handleDeleted(session);
            }
            else {
                /*xxx: 处理session过期*/
                handleExpired(session);
            }
        }
    }

    private void handleCreated(Map<Object, Object> loaded, String channel) {
        String id = channel.substring(channel.lastIndexOf(":") + 1);
        Session session = loadSession(id, loaded);
        /*xxx: 广播session创建事件*/
        publishEvent(new SessionCreatedEvent(this, session));
    }

    private void handleDeleted(RedisSession session) {
        /*xxx: 广播session删除事件*/
        publishEvent(new SessionDeletedEvent(this, session));
    }

    private void handleExpired(RedisSession session) {
        /*xxx: 广播session过期事件*/
        publishEvent(new SessionExpiredEvent(this, session));
    }
}

# session的更新模式

  • session的更新模式分为两种立即更新,以及保存时更新
public enum FlushMode {
    ON_SAVE,
    /*xxx: 有关session的任何变动,都会立即保存到外置存储*/
    IMMEDIATE
}

# redisSession专题

# redisSession的四个key

class Demo{
    /*xxx: sessionKey: 存储session本身,java表现为Map,redis表现为hash*/
	/*xxx: 该键本身也存在过期时间, 为 expires的时间 + 5分钟*/
	String getSessionKey(String sessionId) {
		return this.namespace + "sessions:" + sessionId;
	}

	/*xxx: principalKey: 存储principal -> sessionId的映射关系,属于控制同用户数登录个数的参数,java表现为集合,redis表现为set*/
	String getPrincipalKey(String principalName) {
		return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
				+ principalName;
	}
	/*xxx: expirationKey: 存储 时间戳(以分钟为单位) -> sessionId的映射关系, 用于定时器清除, 一个时间戳可能对应多个sessionId 定时器默认每分钟扫描一起(如果开了的话)*/
	/*xxx: 该键本身也存在过期时间, 为 expires的时间 + 5分钟*/
	String getExpirationsKey(long expiration) {
		return this.namespace + "expirations:" + expiration;
	}

	/*xxx: expiresKey: session的实际过期时间,未配置的情况下,默认为 30分钟。 也就是实际配置的session过期时间*/
	private String getExpiredKey(String sessionId) {
		return getExpiredKeyPrefix() + sessionId;
	}
}
  • sessionKey

    • 存储session本身,java表现为Map,redis表现为hash
    • 该键本身也存在过期时间, 为 expires的时间 + 5分钟
  • principalKey

    • 存储principal -> sessionId的映射关系,属于控制同用户数登录个数的参数,java表现为集合,redis表现为set
  • expirationKey

    • 存储 时间戳(以分钟为单位) -> sessionId的映射关系, 用于定时器清除, 一个时间戳可能对应多个sessionId 定时器默认每分钟扫描一起(如果开了的话)
    • 该键本身也存在过期时间, 为 expires的时间 + 5分钟
  • expiresKey

    • session的实际过期时间,未配置的情况下,默认为 30分钟。 也就是实际配置的session过期时间
    • 配置该参数的名称为:maxInactiveInterval,也就是这个 间隔 时间段没有被访问,则认为会话失效

# redisSession的key创建过程(常用三个)

/*xxx: redis实现的 session仓库*/
public class RedisIndexedSessionRepository
		implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
    
    @Override
	/*xxx: 保存 RedisSession */
	public void save(RedisSession session) {
		session.save();
		/*xxx: 如果session是新建的,会同时发布session创建事件,集群下的其它节点,会收到该事件的广播*/
		if (session.isNew) {
			String sessionCreatedKey = getSessionCreatedChannel(session.getId());
			/*xxx: 发布session创建事件*/
			this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
			session.isNew = false;
		}
	}
    final class RedisSession implements Session {
        
        private void save() {
			/*xxx: 保存 sessionId,以及 所有的 attributes */
			/*xxx: 在这个操作过程中,会将 信息 保存至 redis*/
			saveChangeSessionId();
			saveDelta();
		}
        
        private void saveDelta() {
            //....
            if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
				//...
                //xxx:如果session中,存在 org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME这个键名,则需要进行并发控制
				Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
				String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
				this.originalPrincipalName = principal;
				if (principal != null) {
                    //xxx: 并发控制键的写入
					String principalRedisKey = getPrincipalKey(principal);
					RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
							.add(sessionId);
				}
			}
            
            //xxx: 过期时间设置: 上次访问时间+有效时间间隔  即为过期时间
			Long originalExpiration = (this.originalLastAccessTime != null)
					? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
			/*xxx: 进行过期时间设置*/
			RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
        }
    }  
}
/*xxx: redis超时策略类*/
final class RedisSessionExpirationPolicy {
 	   
    void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
        //xxx: 这个超时的key,没有用统一的前缀,离谱
     	String keyToExpire = "expires:" + session.getId();   
        //xxx: 将需要超时的时间,转为分钟(时间戳)
		long toExpire = roundUpToNextMinute(expiresInMillis(session));
        //... 一个容错处理,略
        
        /*xxx: maxInactiveInterval 代表超时时间,也就是这个 间隔 时间段没有被访问,则认为会话失效 */
		long sessionExpireInSeconds = session.getMaxInactiveInterval().getSeconds();
		/*xxx: 此处是生成 expires的方法, 它没有用 统一的生成key的方法...,名字也起的模糊  离谱 */
		String actualExpiresKey = getSessionKey(keyToExpire);

		/*xxx: 如果超时时间设置为小于0,则session永不失效,则对其进行持久化*/
		if (sessionExpireInSeconds < 0) {
			this.redis.boundValueOps(actualExpiresKey).append("");
			this.redis.boundValueOps(actualExpiresKey).persist();
			this.redis.boundHashOps(getSessionKey(session.getId())).persist();
			return;
		}
        
        /*xxx: 将expiresKey 加入到 expirationKey 集合里面 */
		String expirationKey = getExpirationKey(toExpire);
		BoundSetOperations<Object, Object> expireOperations = this.redis.boundSetOps(expirationKey);
		expireOperations.add(keyToExpire);

		long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);

		/*xxx: expiration 过期 时间为 实际过期的五分钟之后 */
		expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
		//xxx: 如果超时时间设置为0 ,说明 不存储session
		if (sessionExpireInSeconds == 0) {
			this.redis.delete(actualExpiresKey);
		}
		else {
			this.redis.boundValueOps(actualExpiresKey).append("");
			/*xxx: 如果超时时间大于0 ,则以该时间作为超时时间 */
			this.redis.boundValueOps(actualExpiresKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
		}
		/*xxx: 实际session的过期时间,在该键过期时间五分钟之后*/
		this.redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
        
    }
}
  • 附: principal并发控制键的说明(即springSession与SpringSecurity集成时,都会有该属性)
public class PrincipalNameIndexResolver<S extends Session> extends SingleIndexResolver<S> {
 	public String resolveIndexValueFor(S session) {
		//xxx: 获取principal的策略,如果 键: org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME有值,则直接返回
		String principalName = session.getAttribute(getIndexName());
		if (principalName != null) {
			return principalName;
		}
		//xxx: 否则,从springSecurity的安全上下文中,获取
		Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
		if (authentication != null) {
			//xxx: 具体就是获取 securityContext.authentication.getName(); 该方法绝大部分时候,就是获取 userDetails的userName
			return expression.getValue(authentication, String.class);
		}
		return null;
	}   
}

# redisSession的失效机制

redis过期键有三种删除策略(这是对redis本身而言);redisSession采用惰性删除 和 定期删除 的策略

  • 定时删除
    • 通过定时器,过期马上删除。
    • 在设置某个key 的过期时间同时,为每个设置过期时间的key都创造一个定时器;当key过期时间到达时,由定时器任务立即执行对键的删除操作
    • 最有效,但最浪费CPU
  • 惰性删除
    • 访问的时候判断是否过期
    • 对cpu友好,对内存不友好
  • 定期删除
    • 每隔一段时间删除过期键,同时限制每次操作的执行时常和频率
    • 是一种折中方案

# 存储三个键,以及失效时间不一致的原因

  • 业务系统可能会在session失效后做业务逻辑处理,如果只有一个session键,则redis删除后,无法再获取session信息;
  • expires键最先过期,其过期后会发布过期事件,其他订阅了过期事件的节点,依然能获取到session信息

# 不可靠的清理,定时任务兜底

public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
    
 	static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
    
    private String cleanupCron = DEFAULT_CLEANUP_CRON;
    
    public void cleanupExpiredSessions() {
		this.expirationPolicy.cleanExpiredSessions();
	}
    
    @EnableScheduling
	@Configuration(proxyBeanMethods = false)
	/*xxx: redisSession自动带有该特性*/
	class SessionCleanupConfiguration implements SchedulingConfigurer {
     	@Override
		public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
			/*xxx: 定期清理的调度任务*/
			taskRegistrar.addCronTask(this.sessionRepository:: cleanupExpiredSessions,
					RedisHttpSessionConfiguration.this.cleanupCron);
		}   
    }
}
/*xxx: redis超时策略类*/
final class RedisSessionExpirationPolicy {
 	void cleanExpiredSessions() {
		long now = System.currentTimeMillis();
		long prevMin = roundDownMinute(now);

		//xxx: 从expirationKey中获取,即某个时间点,需要过去的键 信息
		String expirationKey = getExpirationKey(prevMin);
		Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
		//xxx: 将键本身删除
		this.redis.delete(expirationKey);
		for (Object session : sessionsToExpire) {
			//xxx: 主动去获取一遍相应的键,包括expires,此时expires将会失效,触发后续流程
			String sessionKey = getSessionKey((String) session);
			touch(sessionKey);
		}
	}
    
    private void touch(String key) {
		this.redis.hasKey(key);
	}
}

# redisSession过期时间更新机制

  • 只要程序内部调用了getSession方法,都会触发更新机制
  • 更新机制会更新lastAccessTime以及所有键的TTL时间

# 时间更新机制源码

/*xxx: 通过sessionRepository进行管理session的请求包装器*/
class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
    
    @Override
		/*xxx: 覆写了 getSession(boolean)方法*/
		/*xxx: 默认情况下,它是 通过  servlet规范的 Manager 进行操作的*/
		public HttpSessionWrapper getSession(boolean create) {
           /*xxx: 获取当前的session,通过 attribute获取: SessionRepository.CURRENT_SESSION*/
			HttpSessionWrapper currentSession = getCurrentSession();
			if (currentSession != null) {
				return currentSession;
			}
			/*xxx: 从请求中 获取 session信息*/
			S requestedSession = getRequestedSession();
			/*xxx: 获取到session后,需要验证其合法性*/
			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;
				}
			}
            
            //省略其它抽象...
            return null;
        }

    private void setCurrentSession(HttpSessionWrapper currentSession) {
			if (currentSession == null) {
				removeAttribute(CURRENT_SESSION_ATTR);
			}
			else {
				setAttribute(CURRENT_SESSION_ATTR, currentSession);
			}
		}
}
class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {
 	   @Override
		/*xxx: responseCommit后,会处理session状态*/
		protected void onResponseCommitted() {
			this.request.commitSession();
		}
    
        class request{
          /*xxx: 该方法会在过滤器执行完成后,处理session的状态*/
          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);
              }
            }
          }
        }
}
class RedisSession implements Session {
 	private void save() {
			/*xxx: 保存 sessionId,以及 所有的 attributes */
			/*xxx: 在这个操作过程中,会将 信息 保存至 redis*/
			saveChangeSessionId();
			saveDelta();
		}   
    
    private void saveDelta() {
		/*xxx: 进行过期时间设置,会更新所有的键的ttl,以及lastAccessTime*/
			RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
	}
}

# 特别说明

  • 当springSession与springSecurity结合时,同一个会话的任何的网页(存在sessionId)都会刷新session更新时长
  • 这将导致,任意一个轮询接口,都将导致session长期生效