• 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);
                }
            }
        }
  • 相关阅读:
    windows利用net use删除smb连接
    用jquery的ajax功能获取网站alexa的方法
    11对于Web开发人员和设计师非常有用的在线工具
    Php获取Alexa排行统计
    php获取alexa世界排名值的函数
    26个免费矢量图片免费下载
    对makefile中双冒号规则的学习
    对makefile中 $*的理解
    GNU make manual 翻译(七十三)
    对makefile 中的 静态模式规则的理解
  • 原文地址:https://www.cnblogs.com/yuyutianxia/p/10318426.html
Copyright © 2020-2023  润新知