Spring Boot内嵌Tomcat session超时问题

时间:2022-06-03 14:32:07

最近让Spring Boot内嵌Tomcat的session超时问题给坑了一把。

在应用中需要设置session超时时间,然后就习惯的在application.properties配置文件中设置如下,

server.session.timeout=90

这里把超时时间设置的短些,主要想看看到底有没有起作用(不能设值30min然后再看吧,那样太不人道了)。结果没起作用,百度下发现Spring Boot 2后,配置变成如下,

server.servlet.session.timeout=90

但结果依然不起作用,后来就断断续续的懵了逼的找问题原因,各种百度,google,最后觉得还是看源代码吧,顺便也学习下。

1. 既然是Session超时时间问题,那就看看对Session的实现 - StandardSession

其中有isValid()方法

    /**
* Return the <code>isValid</code> flag for this session.
*/
@Override
public boolean isValid() { if (!this.isValid) {
return false;
} if (this.expiring) {
return true;
} if (ACTIVITY_CHECK && accessCount.get() > 0) {
return true;
} if (maxInactiveInterval > 0) {
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
} return this.isValid;
}

看了下,这里的 timeIdle >= maxInactiveInterval就是触发session超时的判断,满足则调用 expire(true)。那么问题就来了,什么时候调用isValid()?

2. 后台肯定有定时调用isValid()的线程

查看调用isValid()的相关类如下,StandardManager和ManagerBase入了法眼了。

Spring Boot内嵌Tomcat session超时问题

StandardManager中的注解表明是用来让所有存活的session过期的,应该是在web容器销毁时调用的,所以就只看 ManagerBase

        // Expire all active sessions
Session sessions[] = findSessions();
for (int i = 0; i < sessions.length; i++) {
Session session = sessions[i];
try {
if (session.isValid()) {
session.expire();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
// Measure against memory leaking if references to the session
// object are kept in a shared field somewhere
session.recycle();
}
}

ManagerBase,注解表明是我们想要的,接下来看调用processExpires()的类。还是ManagerBase。

    /**
* Invalidate all sessions that have expired.
*/
public void processExpires() { long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions();
int expireHere = 0 ; if(log.isDebugEnabled())
log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
for (int i = 0; i < sessions.length; i++) {
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
if(log.isDebugEnabled())
log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
processingTime += ( timeEnd - timeNow ); }

调用processExpires()

    /**
* Frequency of the session expiration, and related manager operations.
* Manager operations will be done once for the specified amount of
* backgroundProcess calls (ie, the lower the amount, the most often the
* checks will occur).
*/
protected int processExpiresFrequency = 6;
    /**
* {@inheritDoc}
* <p>
* Direct call to {@link #processExpires()}
*/
@Override
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}

看到backgroundProcess()方法名就知道离真理不远了。其调用如下,在StandardContext类中,

    @Override
public void backgroundProcess() { if (!getState().isAvailable())
return; Loader loader = getLoader();
if (loader != null) {
try {
loader.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.loader", loader), e);
}
}
Manager manager = getManager();
if (manager != null) {
try {
manager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.manager", manager),
e);
}
}
WebResourceRoot resources = getResources();
if (resources != null) {
try {
resources.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.resources",
resources), e);
}
}
InstanceManager instanceManager = getInstanceManager();
if (instanceManager instanceof DefaultInstanceManager) {
try {
((DefaultInstanceManager)instanceManager).backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.instanceManager",
resources), e);
}
}
super.backgroundProcess();
}

但是还没有看到线程的创建,继续查看调用,ContainerBase.ContainerBackgroundProcessor

    /**
* Private thread class to invoke the backgroundProcess method
* of this container and its children after a fixed delay.
*/
protected class ContainerBackgroundProcessor implements Runnable
                while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
// Ignore
}
if (!threadDone) {
processChildren(ContainerBase.this);
}
}

看到曙光了!看来后台线程每隔 backgroundProcessorDelay * processExpiresFrequency (s)来判断session是否过期。

默认值:

backgroundProcessorDelay  = 30s

ServerProperties.class
     /**
* Delay between the invocation of backgroundProcess methods. If a duration suffix
* is not specified, seconds will be used.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration backgroundProcessorDelay = Duration.ofSeconds(30);

processExpiresFrequency = 6

所以默认情况下后台线程每隔3min去判断session是否超时。这样我之前设置server.servlet.session.timeout=90s,没办法看到效果的。

另外还要注意后台对timeout的处理以min为单位,即90s在后台会认为是1min的。

TomcatServletWebServerFactory.class

    private long getSessionTimeoutInMinutes() {
Duration sessionTimeout = getSession().getTimeout();
if (isZeroOrLess(sessionTimeout)) {
return 0;
}
return Math.max(sessionTimeout.toMinutes(), 1);
}