spring-session 2.0 实现细节

1、 前置知识git

1. redis 在键实际过时以后不必定会被删除,可能会继续存留github

2. 具备过时时间的 key 有两种方式来保证过时redis

一是这个键在过时的时候被访问了spring

二是后台运行一个定时任务本身删除过时的 keysession

划重点:这启发咱们在 key 到期后只须要访问一下 key 就能够确保 redis 删除该过时键并发

 

2、三种类型的键app

192.168.1.251:6379> type spring:session:sessions:804f5333-e5dc-48c8-a3d3-86e832f41045
hash

192.168.1.251:6379> hgetall spring:session:sessions:804f5333-e5dc-48c8-a3d3-86e832f41045
1) "lastAccessedTime"
2) "1546913894340"
3) "sessionAttr:_SESSION_CACHE_PREFIX_"
4) "{\"@class\":\"com.reals.session.SessionInfo\",\"mainBindId\":1,\"bindIds\":null,\"phone\":null,\"loginMode\":null,\"openId\":\"o6kAJ4z4LvyPao\",\"platform\":\"Miniprogram\",\"sid\":\"804f5333-e5dc-48c8-a3d3-86e832f41045\",\"validSeconds\":2678400,\"session_key\":\"bBhW9tWg==\"}"
5) "maxInactiveInterval"
6) "2678400"
7) "creationTime"
8) "1546913846141"


192.168.1.251:6379> type spring:session:expirations:1549592340000
set
192.168.1.251:6379>
192.168.1.251:6379> smembers spring:session:expirations:1549592340000
1) "\"expires:804f5333-e5dc-48c8-a3d3-86e832f41045\""


92.168.1.251:6379> type spring:session:sessions:expires:804f5333-e5dc-48c8-a3d3-86e832f41045
string
192.168.1.251:6379> get spring:session:sessions:expires:804f5333-e5dc-48c8-a3d3-86e832f41045
""

 

 

A型键(Hash):spring:session:sessions:2ce8e358-3c23-4233-af40-a338deb0691f
B型键(Set):spring:session:expirations:1550627520000
C型键(String):spring:session:sessions:expires:2ce8e358-3c23-4233-af40-a338deb0691fthis

A/B类型的键ttl比C的长5分钟spa

 

3、运行机制debug

1. 定时任务每分钟查找spring:session:expirations:{timestamp}的值

RedisSessionExpirationPolicy.cleanExpiredSessions
public void cleanExpiredSessions() {
    long now = System.currentTimeMillis();
    long prevMin = roundDownMinute(now);
  //看到是set操做,是B型键
    String expirationKey = getExpirationKey(prevMin);
    Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
    this.redis.delete(expirationKey);
  //B型键有三种类型的值,以下示例
    for (Object session : sessionsToExpire) {
        String sessionKey = getSessionKey((String) session);
        touch(sessionKey);
    }
}

 


参考github issue并发致使的问题

Cleanup in RedisOperationsSessionRepository can cause session to be deleted incorrectly

    /**
     * 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);
    }

 

 

2. B类型键的值

# 1. 已过时,已被删除的键。
# 2. 已过时,可是还没来得及被 redis 清除的 key。在 key 到期后只须要访问一下 key 就能够确保 redis 删除该过时键
# 3. 并发问题致使的多余数据,实际上并未过时。
192.168.0.200:6379[2]> smembers  spring:session:expirations:1550627520000
1) "\"86719669-9214-4dfa-952d-e4a956a201c2\""
192.168.0.200:6379[2]>
192.168.0.200:6379[2]> smembers spring:session:expirations:1549766100000
# RedisSessionExpirationPolicy.onExpirationUpdated 在这里加了下面这种类型的值
1) "\"expires:00e801a5-30dd-4e12-8398-ac9b9336e3b1\""

 

 

3. RedisSessionExpirationPolicy.onExpirationUpdated

    public void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
        String keyToExpire = "expires:" + session.getId();
        long toExpire = roundUpToNextMinute(expiresInMillis(session));
        //删除B型键的旧值
        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();
        //C型键spring:session:sessions:expires:2ce8e358-3c23-4233-af40-a338deb0691f
        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;
        }
        //B型键spring:session:expirations:1550627520000
        String expireKey = getExpirationKey(toExpire);
        BoundSetOperations<Object, Object> expireOperations = this.redis
                .boundSetOps(expireKey);
        expireOperations.add(keyToExpire);

        long fiveMinutesAfterExpires = sessionExpireInSeconds
                + TimeUnit.MINUTES.toSeconds(5);
        //A、B型键的过时时间加多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);
    }

 

You will note that the expiration that is set is 5 minutes after the session
actually expires. This is necessary so that the value of the session can be
accessed when the session expires. An expiration is set on the session itself
five minutes after it actually expires to ensure it is cleaned up, but only
after we perform any necessary processing.

 

4.删除String类型键spring:session:sessions:expires触发键空间通知

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

        String channel = new String(messageChannel);

        if (channel.startsWith(getSessionCreatedChannelPrefix())) {
            // 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);
        //C型键spring:session:sessions:expires才继续执行
        if (!body.startsWith(getExpiredKeyPrefix())) {
            return;
        }

        boolean isDeleted = channel.endsWith(":del");
        if (isDeleted || channel.endsWith(":expired")) {
            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);
            }
        }
    }
相关文章
相关标签/搜索