前面讲到,注解@EnableRedisHttpSession支持配置maxInactiveIntervalInSeconds属性,默认是1800s.
考虑到这个session的过期时间的设置我们系统可能会有要求,之前做MTN项目的时候就有要求用户30分钟不操作就要将用户踢下线,所以这里设置了60s来验证下这个功能:
@EnableRedisHttpSession(redisNamespace = "iotcmp-cc", maxInactiveIntervalInSeconds = 60)
登录系统,停留一分钟之后再操作,发现被我的拦截器拦截到提示用户未登录或Session超时。说明这个session超时功能是OK的。
下面详细看看这个session超时功能:
我们登录系统创建session之后,查看redis中缓存的key:
"spring:session:iotcmp-cc:expirations:1523286240000"
"spring:session:iotcmp-cc:sessions:expires:add13af0-361d-4ea1-9667-04ee475281c0"
"spring:session:iotcmp-cc:sessions:add13af0-361d-4ea1-9667-04ee475281c0"
检查这三个缓存的key的过期时间:
172.16.22.2:7691> ttl spring:session:iotcmp-cc:expirations:1523286240000
(integer) 299
172.16.22.2:7691> ttl spring:session:iotcmp-cc:sessions:add13af0-361d-4ea1-9667-04ee475281c0
(integer) 276
172.16.22.2:7691> ttl spring:session:iotcmp-cc:sessions:expires:add13af0-361d-4ea1-9667-04ee475281c0
(integer) 35
发现这三个key设置的失效时间并非一致。前两个其实设置的失效时间为360s,第三个设置的失效时间为60s.
这里需要思考下,为什么前两个session key的ttl需要额外加上5min?
查看下源码,根据注解@EnableRedisHttpSession找到处理的类RedisHttpSessionConfiguration:
org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration.setImportMetadata(AnnotationMetadata)
启动的时候在这个方法里读取注解上配置的属性maxInactiveIntervalInSeconds的值
使用redis存储session的实现类:RedisOperationsSessionRepository
org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration.sessionRepository(RedisOperations<Object, Object>, ApplicationEventPublisher)
在这个方法中将maxInactiveIntervalInSeconds的值设置到RedisOperationsSessionRepository的defaultMaxInactiveInterval属性上,描述如下:
org.springframework.session.data.redis.RedisOperationsSessionRepository.setDefaultMaxInactiveInterval(int)
/**
* Sets the maximum inactive interval in seconds between requests before newly created
* sessions will be invalidated. A negative time indicates that the session will never
* timeout. The default is 1800 (30 minutes).
*
* @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
* should be kept alive between client requests.
*/
public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
}
再看RedisOperationsSessionRepository这个类,对session过期的策略:
提供类RedisSessionExpirationPolicy来处理session过期策略:
public RedisOperationsSessionRepository(
RedisOperations<Object, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations,
this);
}
提供了一个定时器定时清理过期的session,这里配置的每隔1分钟执行一次
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
更新redis中的session
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
XXXXXXXXXXXXXXXXX
Long originalExpiration = this.originalLastAccessTime == null ? null
: this.originalLastAccessTime + TimeUnit.SECONDS
.toMillis(getMaxInactiveIntervalInSeconds());
RedisOperationsSessionRepository.this.expirationPolicy
.onExpirationUpdated(originalExpiration, this);
}
主要看org.springframework.session.data.redis.RedisSessionExpirationPolicy.onExpirationUpdated(Long, ExpiringSession):
public void onExpirationUpdated(Long originalExpirationTimeInMilli,
ExpiringSession session) {
//expires:eebd8f7a-7c4a-4f1a-bbf2-cb84795aa739
String keyToExpire = "expires:" + session.getId();
long toExpire = roundUpToNextMinute(expiresInMillis(session));
if (originalExpirationTimeInMilli != null) {
long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
if (toExpire != originalRoundedUp) {
String expireKey = getExpirationKey(originalRoundedUp);
this.redis.boundSetOps(expireKey).remove(keyToExpire);
}
}
long sessionExpireInSeconds = session.getMaxInactiveIntervalInSeconds();
//spring:session:iotcmp-cc:sessions:expires:eebd8f7a-7c4a-4f1a-bbf2-cb84795aa739
//maxInactiveIntervalInSeconds
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;
}
//spring:session:iotcmp-cc:expirations:1523290380000
//300+maxInactiveIntervalInSeconds
String expireKey = getExpirationKey(toExpire);
BoundSetOperations<Object, Object> expireOperations = this.redis
.boundSetOps(expireKey);
expireOperations.add(keyToExpire);
long fiveMinutesAfterExpires = sessionExpireInSeconds
+ TimeUnit.MINUTES.toSeconds(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);
}
//spring:session:iotcmp-cc:sessions:eebd8f7a-7c4a-4f1a-bbf2-cb84795aa739
//300+maxInactiveIntervalInSeconds
this.redis.boundHashOps(getSessionKey(session.getId()))
.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
}
上述方法可以知道,session在redis中保存的这三个key:
"spring:session:iotcmp-cc:sessions:expires:53cb02aa-9f5f-4cf9-812b-c2db72ed4fb1"
"spring:session:iotcmp-cc:expirations:1523257920000"
"spring:session:iotcmp-cc:sessions:53cb02aa-9f5f-4cf9-812b-c2db72ed4fb1"
第一个key 存的值是value类型,key格式:spring:session:redisNamespace:sessions:expires:sessionId;该数据的 ttl表示 sessionId 过期的剩余时间,即 maxInactiveInterval
第二个key 存的值是set类型,key格式:spring:session:redisNamespace:expirations:longtime;longtime表示从 1970 年 1 月 1 日 0 点 0 分到现在经过的分钟数
第三个key 存的值是hash类型,key格式:spring:session:redisNamespace:sessions:sessionId;session保存的数据,记录了 creationTime,maxInactiveInterval,lastAccessedTime,attribute。前两个数据是用于 session 过期管理的辅助数据结构。
我们再查看下每个key分别存放了什么信息:
172.16.22.2:7691> get spring:session:iotcmp-cc:sessions:expires:53cb02aa-9f5f-4cf9-812b-c2db72ed4fb1
""
172.16.22.2:7691> smembers spring:session:iotcmp-cc:expirations:1523257920000
1) "\xac\xed\x00\x05t\x00,expires:53cb02aa-9f5f-4cf9-812b-c2db72ed4fb1"
172.16.22.2:7691> hkeys spring:session:iotcmp-cc:sessions:53cb02aa-9f5f-4cf9-812b-c2db72ed4fb1
1) "sessionAttr:randCheckCode"
2) "maxInactiveInterval"
3) "creationTime"
4) "lastAccessedTime"
5) "sessionAttr:USER_LOGIN_INFO"
定期清理Job调用的方法:org.springframework.session.data.redis.RedisSessionExpirationPolicy.cleanExpiredSessions()
public void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);
if (logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
}
String expirationKey = getExpirationKey(prevMin);
Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
this.redis.delete(expirationKey);
for (Object session : sessionsToExpire) {
String sessionKey = getSessionKey((String) session);
touch(sessionKey);
}
}