一、 前置知識
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); } } }