zoukankan      html  css  js  c++  java
  • 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);
                }
            }
        }
  • 相关阅读:
    最近要看的项目
    Lavarel Route::resource
    架构,性能
    Unity ToLua & LuaFramework_UGUI学习笔记(zz)
    Unity UI 布局
    Introduction to Unity UI
    Unity more efficient find
    unity UI如何开启(显示)或者关闭(隐藏)Panel界面最好?
    Unity Canvas vs Panel
    Unity实现新手引导圆形遮罩
  • 原文地址:https://www.cnblogs.com/yuyutianxia/p/10318426.html
Copyright © 2011-2022 走看看