tomcat session 设计分析
tomcat session 组件图如下所示,其中 Context
对应一个 webapp 应用,每个 webapp 有多个 HttpSessionListener
, 并且每个应用的 session 是独立管理的,而 session 的创建、销毁由 Manager
组件完成,它内部维护了 N 个 Session
实例对象。在前面的文章中,我们分析了 Context
组件,它的默认实现是 StandardContext
,它与 Manager
是一对一的关系,Manager
创建、销毁会话时,需要借助 StandardContext
获取 HttpSessionListener
列表并进行事件通知,而 StandardContext
的后台线程会对 Manager
进行过期 Session 的清理工作
org.apache.catalina.Manager
接口的主要方法如下所示,它提供了 Context
、org.apache.catalina.SessionIdGenerator
的 getter/setter 接口,以及创建、添加、移除、查找、遍历 Session
的 API 接口,此外还提供了 Session
持久化的接口(load/unload) 用于加载/卸载会话信息,当然持久化要看不同的实现类
public interface Manager {
public Context getContext();
public void setContext(Context context);
public SessionIdGenerator getSessionIdGenerator();
public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
public void add(Session session);
public void addPropertyChangeListener(PropertyChangeListener listener);
public void changeSessionId(Session session);
public void changeSessionId(Session session, String newId);
public Session createEmptySession();
public Session createSession(String sessionId);
public Session findSession(String id) throws IOException;
public Session[] findSessions();
public void remove(Session session);
public void remove(Session session, boolean update);
public void removePropertyChangeListener(PropertyChangeListener listener);
public void unload() throws IOException;
public void backgroundProcess();
public boolean willAttributeDistribute(String name, Object value);
tomcat8.5 提供了 4 种实现,默认使用 StandardManager
,tomcat 还提供了集群会话的解决方案,但是在实际项目中很少运用,关于 Manager
的详细配置信息请参考 tomcat 官方文档
- StandardManager:Manager 默认实现,在内存中管理 session,宕机将导致 session 丢失;但是当调用 Lifecycle 的 start/stop 接口时,将采用 jdk 序列化保存 Session 信息,因此当 tomcat 发现某个应用的文件有变更进行 reload 操作时,这种情况下不会丢失 Session 信息
- DeltaManager:增量 Session 管理器,用于Tomcat集群的会话管理器,某个节点变更 Session 信息都会同步到集群中的所有节点,这样可以保证 Session 信息的实时性,但是这样会带来较大的网络开销
- BackupManager:用于 Tomcat 集群的会话管理器,与DeltaManager不同的是,某个节点变更 Session 信息的改变只会同步给集群中的另一个 backup 节点
- PersistentManager:当会话长时间空闲时,将会把 Session 信息写入磁盘,从而限制内存中的活动会话数量;此外,它还支持容错,会定期将内存中的 Session 信息备份到磁盘
Session 相关的类图如下所示,StandardSession
同时实现了 javax.servlet.http.HttpSession
、org.apache.catalina.Session
接口,并且对外提供的是 StandardSessionFacade
外观类,保证了 StandardSession
的安全,避免开发人员调用其内部方法进行不当操作。而 org.apache.catalina.connector.Request
实现了 javax.servlet.http.HttpServletRequest
接口,它持有 StandardSession
的引用,对外也是暴露 RequestFacade
外观类。而 StandardManager
内部维护了其创建的 StandardSession
,是一对多的关系,并且持有 StandardContext
的引用,而 StandardContext
内部注册了 webapp 所有的 HttpSessionListener
实例。
创建Session
我们以 HttpServletRequest#getSession()
作为切入点,对 Session 的创建过程进行分析
public class SessionExample extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
HttpSession session = request.getSession();
// other code......
}
整个流程图如下图所示(查看原图):
tomcat 创建 session 的流程如上图所示,我们的应用程序拿到的 HttpServletRequest
是 org.apache.catalina.connector.RequestFacade
(除非某些 Filter 进行了特殊处理),它是 org.apache.catalina.connector.Request
的门面模式。首先,会判断 Request
对象中是否存在 Session
,如果存在并且未失效则直接返回,因为在 tomcat 中 Request
对象是被重复利用的,只会替换部分组件,所以会进行这步判断。此时,如果不存在 Session
,则尝试根据 requestedSessionId
查找 Session
,而该 requestedSessionId
会在 HTTP Connector 中进行赋值(如果存在的话),如果存在 Session
的话则直接返回,如果不存在的话,则创建新的 Session
,并且把 sessionId
添加到 Cookie
中,后续的请求便会携带该 Cookie
,这样便可以根据 Cookie
中的sessionId
找到原来创建的 Session
了
在上面的过程中,Session
的查找、创建都是由 Manager
完成的,下面我们分析下 StandardManager
创建 Session
的具体逻辑。首先,我们来看下 StandardManager
的类图,它也是个 Lifecycle
组件,并且 ManagerBase
实现了主要的逻辑。
整个创建 Session
的过程比较简单,就是实例化 StandardSession
对象并设置其基本属性,以及生成唯一的 sessionId
,其次就是记录创建时间,关键代码如下所示:
public Session createSession(String sessionId) {
// 限制 session 数量,默认不做限制,maxActiveSessions = -1
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"), maxActiveSessions);
}
// 创建 StandardSession 实例,子类可以重写该方法
Session session = createEmptySession();
// 设置属性,包括创建时间,最大失效时间
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
// 设置最大不活跃时间(单位s),如果超过这个时间,仍然没有请求的话该Session将会失效
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);
sessionCounter++; // 这个地方不是线程安全的,可能当时开发人员认为计数器不要求那么准确
// 将创建时间添加到LinkedList中,并且把最先添加的时间移除,主要还是方便清理过期session
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return (session);
在 tomcat 中是可以限制 session 数量的,如果需要限制,请指定 Manager
的 maxActiveSessions
参数,默认不做限制,不建议进行设置,但是如果存在恶意攻击,每次请求不携带 Cookie
就有可能会频繁创建 Session
,导致 Session
对象爆满最终出现 OOM。另外 sessionId
采用随机算法生成,并且每次生成都会判断当前是否已经存在该 id,从而避免 sessionId
重复。而 StandardManager
是使用 ConcurrentHashMap
存储 session 对象的,sessionId
作为 key,org.apache.catalina.Session
作为 value。此外,值得注意的是 StandardManager
创建的是 tomcat 的 org.apache.catalina.session.StandardSession
,同时他也实现了 servlet 的 HttpSession
,但是为了安全起见,tomcat 并不会把这个 StandardSession
直接交给应用程序,因此需要调用 org.apache.catalina.Session#getSession()
获取 HttpSession
。
我们再来看看 StandardSession
的内部结构
- attributes:使用 ConcurrentHashMap 解决多线程读写的并发问题
- creationTime:Session 的创建时间
- expiring:用于标识 Session 是否过期
- lastAccessedTime:上一次访问的时间,用于计算 Session 的过期时间
- maxInactiveInterval:Session 的最大存活时间,如果超过这个时间没有请求,Session 就会被清理、
- listeners:这是 tomcat 的 SessionListener,并不是 servlet 的 HttpSessionListener
- facade:HttpSession 的外观模式,应用程序拿到的是该对象