【详解】Tomcat是如何监控并删除超时Session的?

前言

偶然发现Tomcat会话时间的半小时,并不是说“会话创建后只有半小时的有效使用时间”,而是说“会话空闲半小时后会被删除”。索性就翻了一下源码。做了一番整理。

注:空闲时间,指的是同一个会话两次请求之间的间隔时间

Session相关类图

【详解】Tomcat是如何监控并删除超时Session的?

  • HttpSession就是大家Servlet层可以直接使用的Session.
  • Session是Tomcat内部使用的接口,可以做一些内部调用
  • StandardSession是标准的HttpSession实现,同时它也实现了Session接口,用于Tomcat内部管理
  • StandardSessionFacade,类名已经指明它就是一个“门面类”,它内部会引用一个StandardSession的对象,但对外只提供HttpSession规定的方法。

Manager相关类图

【详解】Tomcat是如何监控并删除超时Session的?

StandardManager与PersitentManager都是Manager的实现,但是它们在存储Session对象的方式上有所不同。

StandarManager

1.Tomcat运行时,把Session存储在内存中

2.Tomcat关闭时(注意是正常的关闭操作,如执行stop.sh,而非突然崩溃),会把Session写入到磁盘中,等到Tomcat重启后再把Session加载进来

PersistentManager

1.总是把Session存储在磁盘中。

Manager与Context的关系

在Tomcat中,一个Context就是部署到Tomcat中的一个应用(Webapp)。每一个Context都有一个单独的Manager对象来管理这个应用的会话信息。

【详解】Tomcat是如何监控并删除超时Session的?

Manager如何存储Session

Manager对象会使用一个Map来存储Session对象

  • Key  => SessionId
  • Value  => Session Object
    /**
     * The set of currently active Sessions for this Manager, keyed by
     * session identifier.
     */
    protected Map<String, Session> sessions = new ConcurrentHashMap<>();

当一个请求到达Context的时候,如果它带有JSESSIONID的Cookie,Manager就能依此找到关联的Session对象,放入到Request对象中。

Manager的定期检查

Manager接口有一个backgroundProcess()方法,顾名思义就是后台处理。

    /**
     * This method will be invoked by the context/container on a periodic
     * basis and allows the manager to implement
     * a method that executes periodic tasks, such as expiring sessions etc.
     */
    public void backgroundProcess();

注:Container接口也有这个方法,这个方法一般在容器启动(start)的时候,开启一个额外的线程来循环执行这个backgroundProcess方法。Context的这个方法启动后,会执行Loader和Manager的backgroundProcess方法。

我们来看看这个方法都做了些什么?

/**
 * {@inheritDoc}
 * <p>
 * Direct call to {@link #processExpires()}
 */
@Override
public void backgroundProcess() {
    count = (count + 1) % processExpiresFrequency;
    if (count == 0)  //如果达到检查频率则开始检查
        processExpires();
}

/**
 * Invalidate all sessions that have expired.
 */
public void processExpires() {

    long timeNow = System.currentTimeMillis();
    Session sessions[] = findSessions(); //获取所有session对象
    int expireHere = 0 ; //过期session的数量,不要被这个变量名骗了

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

}

很多人看到这里,可能会有跟我一样的疑惑,即这里面根本就没有使Session过期失效的操作,好像只做了状态检查。不过后来看到了Session的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;
    }

    //关键所在
    //如果有设置最大空闲时间
    //就获取此Session的空闲时间进行判断
    //如果已超时,则执行expire操作
    if (maxInactiveInterval > 0) { 
        int timeIdle = (int) (getIdleTimeInternal() / 1000L);
        if (timeIdle >= maxInactiveInterval) {
            expire(true);
        }
    }

    return this.isValid;
}

 那这个空闲时间怎么得到?

 其实就是记录上一次请求的时间,然后与本次请求的时间进行比对,得到时间差。

总结

总的来说,就是用一个线程来轮询会话状态,如果某个会话的空闲时间超过设定的最大值,则将该会话销毁。