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