一、SessionRepositoryRequestWrapper

重点看一下重写的getSession方法,代码如下: 

@Override
public HttpSessionWrapper getSession(boolean create) {
    //1、从本地缓存中获取
    HttpSessionWrapper currentSession = getCurrentSession();
    if (currentSession != null) {
        return currentSession;
    }
    
    //2、从数据库或者Redis中获取
    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)"));
    }

    //3、创建一个新的session
    S session = SessionRepositoryFilter.this.sessionRepository.createSession();
    session.setLastAccessedTime(Instant.now());
    currentSession = new HttpSessionWrapper(session, getServletContext());
    setCurrentSession(currentSession);
    return currentSession;
}

大致分为三步:首先去本地内存中获取session,如果获取不到去指定的数据库中获取,这里其实就是去redis里面获取,sessionRepository就是上面定义的RedisOperationsSessionRepository对象;如果redis里面也没有则创建一个新的session;

 

二、RedisOperationsSessionRepository 

 关于session的保存,更新,删除,获取操作都在此类中;

1、保存session

每次在消息处理完之后,会执行finally中的commitSession方法,每个session被保存都会创建三组数据,如下所示:

127.0.0.1:6379[4]> keys *
1) "spring:session:expirations:1653902160000"
2) "spring:session:expirations:1653901980000"
3) "spring:session:sessions:expires:91de5537-3eec-41da-a78b-1d9648793b17"
4) "spring:session:sessions:91de5537-3eec-41da-a78b-1d9648793b17"

hash结构记录

key格式:spring:session:sessions:[sessionId],对应的value保存session的所有数据包括:creationTime,maxInactiveInterval,lastAccessedTime,attribute;

springcloud 查看redis 连接池使用情况 spring cloud session redis_数据库

 

set结构记录

key格式:spring:session:expirations:[过期时间],对应的value为expires:[sessionId]列表,有效期默认是30分钟,即1800秒;

string结构记录

key格式:spring:session:sessions:expires:[sessionId],对应的value为空;该数据的TTL表示sessionId过期的剩余时间;

 相关代码如下:RedisSessionExpirationPolicy

public void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
    String keyToExpire = "expires:" + session.getId();
    long toExpire = roundUpToNextMinute(expiresInMillis(session));

    if (originalExpirationTimeInMilli != null) {
        long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
        if (toExpire != originalRoundedUp) {
            String expireKey = getExpirationKey(originalRoundedUp);
            this.redis.boundSetOps(expireKey).remove(keyToExpire);
        }
    }

    long sessionExpireInSeconds = session.getMaxInactiveInterval().getSeconds();
    String sessionKey = getSessionKey(keyToExpire);

    if (sessionExpireInSeconds < 0) {
        this.redis.boundValueOps(sessionKey).append("");
        this.redis.boundValueOps(sessionKey).persist();
        this.redis.boundHashOps(getSessionKey(session.getId())).persist();
        return;
    }

    String expireKey = getExpirationKey(toExpire);
    BoundSetOperations<Object, Object> expireOperations = this.redis
            .boundSetOps(expireKey);
    expireOperations.add(keyToExpire);

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

    expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
    if (sessionExpireInSeconds == 0) {
        this.redis.delete(sessionKey);
    }
    else {
        this.redis.boundValueOps(sessionKey).append("");
        this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds,
                TimeUnit.SECONDS);
    }
    this.redis.boundHashOps(getSessionKey(session.getId()))
            .expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
}

static long expiresInMillis(Session session) {
    int maxInactiveInSeconds = (int) session.getMaxInactiveInterval().getSeconds();
    long lastAccessedTimeInMillis = session.getLastAccessedTime().toEpochMilli();
    return lastAccessedTimeInMillis + TimeUnit.SECONDS.toMillis(maxInactiveInSeconds);
}

static long roundUpToNextMinute(long timeInMs) {

    Calendar date = Calendar.getInstance();
    date.setTimeInMillis(timeInMs);
    date.add(Calendar.MINUTE, 1);
    date.clear(Calendar.SECOND);
    date.clear(Calendar.MILLISECOND);
    return date.getTimeInMillis();
}

static long roundDownMinute(long timeInMs) {
    Calendar date = Calendar.getInstance();
    date.setTimeInMillis(timeInMs);
    date.clear(Calendar.SECOND);
    date.clear(Calendar.MILLISECOND);
    return date.getTimeInMillis();
}
  1. session.getMaxInactiveInterval().getSeconds()默认是1800秒;expiresInMillis返回了一个到期的时间戳;
  2. roundUpToNextMinute方法在此基础上添加了1分钟,并且清除了秒和毫秒,返回的long值被用来当做key,用来记录一分钟内应当过期的key列表,也就是上面的set结构记录;
  3. 后面的代码分别为以上三个key值指定了有效期,spring:session:sessions:expires是30分钟,而另外2个都是35分钟;
  4. 理论上只需要为spring:session:sessions:[sessionId]指定有效期就行了,为什么还要再保存两个key,官方的说法是依赖redis自身提供的有效期并不能保证及时删除;

 

2、定期删除

除了依赖redis本身的有效期机制,spring-session提供了一个定时器,用来定期检查需要被清理的session;

public void cleanupExpiredSessions() {
    this.expirationPolicy.cleanExpiredSessions();
}

public void cleanExpiredSessions() {
    long now = System.currentTimeMillis();
    long prevMin = roundDownMinute(now);

    if (logger.isDebugEnabled()) {
        logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
    }

    String expirationKey = getExpirationKey(prevMin);
    Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
    this.redis.delete(expirationKey);
    for (Object session : sessionsToExpire) {
        String sessionKey = getSessionKey((String) session);
        touch(sessionKey);
    }
}

/**
* By trying to access the session we only trigger a deletion if it the TTL is
* expired. This is done to handle
* https://github.com/spring-projects/spring-session/issues/93
*
* @param key the key
*/
private void touch(String key) {
    this.redis.hasKey(key);
}

static long expiresInMillis(Session session) {
    int maxInactiveInSeconds = (int) session.getMaxInactiveInterval().getSeconds();
    long lastAccessedTimeInMillis = session.getLastAccessedTime().toEpochMilli();
    return lastAccessedTimeInMillis + TimeUnit.SECONDS.toMillis(maxInactiveInSeconds);
}

同样是通过roundDownMinute方法来获取key,获取这一分钟内要被删除的session此value是set数据结构,里面存放这需要被删除的sessionId;

(注:这里面存放的的是spring:session:sessions:expires:[sessionId],并不是实际存储session数据的spring:session:sessions:[sessionId])

首先删除了spring:session:expirations:[过期时间],然后遍历set执行touch方法并没有直接执行删除操作,看touch方法的注释大致意义就是尝试访问一下key,如果key已经过去则触发删除操作,利用了redis本身的特性;

3、键空间通知(keyspace notification)

定期删除机制并没有删除实际存储session数据的spring:session:sessions:[sessionId],这里利用了redis的keyspace notification功能,大致就是通过命令产生一个通知。

spring-session的keyspace notification配置在ConfigureNotifyKeyspaceEventsAction类中,RedisOperationsSessionRepository负责接收消息通知,具体代码如下: 

@Override
@SuppressWarnings("unchecked")
public void onMessage(Message message, byte[] pattern) {
    byte[] messageChannel = message.getChannel();
    byte[] messageBody = message.getBody();

    String channel = new String(messageChannel);

    if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
        // TODO: is this thread safe?
        Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer
                .deserialize(message.getBody());
        handleCreated(loaded, channel);
        return;
    }

    String body = new String(messageBody);
    if (!body.startsWith(getExpiredKeyPrefix())) {
        return;
    }

    boolean isDeleted = channel.equals(this.sessionDeletedChannel);
    if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
        int beginIndex = body.lastIndexOf(":") + 1;
        int endIndex = body.length();
        String sessionId = body.substring(beginIndex, endIndex);

        RedisSession session = getSession(sessionId, true);

        if (session == null) {
            logger.warn("Unable to publish SessionDestroyedEvent for session "
                    + sessionId);
            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
        }

        cleanupPrincipalIndex(session);

        if (isDeleted) {
            handleDeleted(session);
        }
        else {
            handleExpired(session);
        }
    }
}

接收已spring:session:sessions:expires开头的通知,然后截取出sessionId,然后通过sessionId删除实际存储session的数据;

此处有个疑问就是:

  1. 为什么要引入spring:session:sessions:expires:[sessionId]类型key?
  2. spring:session:expirations的value直接保存spring:session:sessions:[sessionId]不就可以了吗?

这里使用此key的目的可能是让有效期和实际的数据分开如果不这样有地方监听到session过期,而此时session已经被移除,导致获取不到session的内容;并且在上面设置有效期的时候,spring:session:sessions:[sessionId]的有效期多了5分钟,应该也是为了这个考虑的;