spring-session 2.0 實現細節


一、 前置知識

1. redis 在鍵實際過期之后不一定會被刪除,可能會繼續存留

2. 具有過期時間的 key 有兩種方式來保證過期

一是這個鍵在過期的時候被訪問了

二是后台運行一個定時任務自己刪除過期的 key

划重點:這啟發我們在 key 到期后只需要訪問一下 key 就可以確保 redis 刪除該過期鍵

 

二、三種類型的鍵

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-a338deb0691f

A/B類型的鍵ttl比C的長5分鍾

 

三、運行機制

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM