tomcat的Session周期和常用的缓存失效机制
- 前言
- 1. tomcat的启动方法
- 2. ManagerBase类
- 3. ContainerBackgroundProcessorMonitor和ContainerBackgroundProcessor
- 4. LinkedHashMap过期机制
- 5. Guava中的LocalCache的过期机制
- 最后总结
- 参考资料
前言
session的失效机制实际上是一种LRU过期淘汰机制,在缓存系统设计中有很广泛的应用,比如LinkedHashMap的失效机制,redis的值失效机制以及guava的cache失效机制。本文希望做一个简单的对比,为在以后的类似程序设计中有更好的参考。
1. tomcat的启动方法
本文使用的tomcat代码是springboot-2.2.6.RELEASE版本内嵌的tomcat-embed-core-9.0.33版本。
如果熟悉tomcat的同学应该知道,tomcat的启动/关闭的类是Catalina
,外置的tomcat的启动类Bootstrap
实际是对Catalina
做的一些参数化的组合。
//main方法供startup脚本调用
public static void main(String args[]) {
synchronized (daemonLock) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();//这里是初始化Catalina类
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();//该方法里调用Catalina的start方法
} else if (command.equals("stopd")) {
...省略
}
init方法
public void init() throws Exception {
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");//反射获取Catalina类
Object startupInstance = startupClass.getConstructor().newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);//设置classload的上下文
catalinaDaemon = startupInstance;
}
start() 方法
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);//反射调用catalina的start方法初始化server实例
method.invoke(catalinaDaemon, (Object [])null);
}
内嵌的tomcat则提供了Tomcat
类,有init()方法和start()方法,SpringBoot在启动时,会构造ServletWebServerFactory
,TomcatServletWebServerFactory
是其实现,用来配置相关信息,生产TomcatWebServer
(WebServer 的实现),
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);//默认是rg.apache.coyote.http11.Http11NioProtocol协议,用的是NIO
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
getTomcatWebServer
方法,
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0);
}
TomcatWebServer
的构造器里的initialize()方法去初始化,
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
// Start the server to trigger initialization listeners
this.tomcat.start();//启动内嵌的tomcat
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startDaemonAwaitThread();
}
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
如果查看tomcat
类的的start()方法和Catalina
类的start()方法,本质上一样的,都是启动Server
服务器,这里其实tomcat类也可以直接去调到Catalina类方法,但是内嵌的tomcat类没有这么做,估计是Catalina类会读取配置文件的一系列参数,而这些参数在内嵌的tomcat是没有的,所以内嵌的tomcat类直接初始化Server服务器了。
2. ManagerBase类
ManagerBase类是Manager的实现,是管理session的具体实现,管理session的生命周期,比如createSession,remove,expireSession等方法。
sessions是用ConcurrentHashMap的数据结构存储的,支持并发处理。
/**
* The set of currently active Sessions for this Manager, keyed by
* session identifier.
*/
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
backgroundProcess方法用来处理过期的session,调用的是rocessExpires() 方法。
@Override
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
processExpiresFrequency变量用来控制检查过期session的频率,默认processExpiresFrequency=6,该值可以设置。
public void processExpires() {
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions();//这里将sessions复制成数组,而不是直接操作ConcurrentHashMap,应该是减少对ConcurrentHashMap并发影响。
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 );
}
可以看到处理过期事件是交由seesion.isVaild()去处理的,具体实现在StandardSession
@Override
public boolean isValid() {
if (!this.isValid) {//检查是否已经失效了,volatile类型
return false;
}
if (this.expiring) {//检查是否正在处理过期中,volatile类型
return true;
}
if (ACTIVITY_CHECK && accessCount.get() > 0) {//ACTIVITY_CHECK是可以设置的值,默认是flase,如果是true的话,需要检查accessCount的次数,该值用来计数该session是否还在被访问,一般结束访问时调用endAccess()方法自减。
return true;
}
if (maxInactiveInterval > 0) {
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
if (timeIdle >= maxInactiveInterval) {//检查最后的访问时间是否大于闲时可存活时间,执行过期操作
expire(true);
}
}
return this.isValid;
}
实际上就是检查一些状态是否可以执行过期操作。
public void expire(boolean notify) {
// Check to see if session has already been invalidated.
// Do not check expiring at this point as expire should not return until
// isValid is false
if (!isValid)//再次检查是否有效的
return;
synchronized (this) {//加锁,防止并发,因为seesion的expire()方法是可以主动被调用的。
// Check again, now we are inside the sync so this code only runs once
// Double check locking - isValid needs to be volatile
// The check of expiring is to ensure that an infinite loop is not
// entered as per bug 56339
if (expiring || !isValid)//检查是否正在执行过期动作和检查是否有效
return;
if (manager == null)
return;
// Mark this session as "being expired"
expiring = true;//设置正在处理中
// Notify interested application event listeners
// FIXME - Assumes we call listeners in reverse order
Context context = manager.getContext();
// The call to expire() may not have been triggered by the webapp.
// Make sure the webapp's class loader is set when calling the
// listeners
if (notify) {//下面是一系列的事件通知
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
int j = (listeners.length - 1) - i;
if (!(listeners[j] instanceof HttpSessionListener))
continue;
HttpSessionListener listener =
(HttpSessionListener) listeners[j];
try {
context.fireContainerEvent("beforeSessionDestroyed",
listener);
listener.sessionDestroyed(event);
context.fireContainerEvent("afterSessionDestroyed",
listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent(
"afterSessionDestroyed", listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString("standardSession.sessionEvent"), t);
}
}
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}
if (ACTIVITY_CHECK) {
accessCount.set(0);//设置为0,当前访问已结束
}
// Remove this session from our manager's active sessions
manager.remove(this, true);//这里是调用manger的remove方法,因为manger里持有了sessions的map对象。
// Notify interested session event listeners
if (notify) {
fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
}
// Call the logout method
if (principal instanceof TomcatPrincipal) {
TomcatPrincipal gp = (TomcatPrincipal) principal;
try {
gp.logout();
} catch (Exception e) {
manager.getContext().getLogger().error(
sm.getString("standardSession.logoutfail"),
e);
}
}
// We have completed expire of this session
setValid(false);//设置无效
expiring = false;//处理完毕
// Unbind any objects associated with this session
String keys[] = keys();
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
for (int i = 0; i < keys.length; i++) {
removeAttributeInternal(keys[i], notify);//移除该session里的所有属性
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);//解绑该session的一些关联
}
}
}
从上面的方法里可以看出,session的有效性检查里包含了一系列的操作,包括一些状态的检查,并发性控制,属性的移除,事件的通知,但是session的生命周期理论上是由Manager管理的,所以还需要去回调manager.remove方法,要实现这种,就需要session和manager相互持有对方的引用,不且说这样的方式实现是否优雅,需要注意JVM的垃圾回收机制是否能够有效的回收掉。
回到ManagerBase类中,
public void remove(Session session, boolean update) {
// If the session has expired - as opposed to just being removed from
// the manager because it is being persisted - update the expired stats
if (update) {
long timeNow = System.currentTimeMillis();
int timeAlive =
(int) (timeNow - session.getCreationTimeInternal())/1000;
updateSessionMaxAliveTime(timeAlive);
expiredSessions.incrementAndGet();
SessionTiming timing = new SessionTiming(timeNow, timeAlive);
synchronized (sessionExpirationTiming) {//sessionExpirationTiming是用来统计过期的比例的,填充了100个空值,这个设计也是有特色,可以用并发的队列来实现,不用加synchronized
sessionExpirationTiming.add(timing);
sessionExpirationTiming.poll();
}
}
if (session.getIdInternal() != null) {//从map里remove
sessions.remove(session.getIdInternal());
}
}
3. ContainerBackgroundProcessorMonitor和ContainerBackgroundProcessor
ManagerBase类提供了backgroundProcess()方法用来处理过期的session,ManagerBase本身并没有使用定时调度或者在插入或者删除session操作的时候去调用backgroundProcess()方法,实际的调用者是ContainerBackgroundProcessor类。
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
processChildren(ContainerBase.this);
}
protected void processChildren(Container container) {
ClassLoader originalClassLoader = null;
try {
if (container instanceof Context) {
Loader loader = ((Context) container).getLoader();
// Loader will be null for FailedContext instances
if (loader == null) {
return;
}
// Ensure background processing for Contexts and Wrappers
// is performed under the web app's class loader
originalClassLoader = ((Context) container).bind(false, null);
}
container.backgroundProcess();//处理方法
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i]);//递归调用子类
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("containerBase.backgroundProcess.error"), t);
} finally {
if (container instanceof Context) {
((Context) container).unbind(false, originalClassLoader);
}
}
}
}
这里要理解tomcat里的Container的父子级关系。
- Engine,包含Host和Context,接到请求后仍给相应的Host在相应的Context里处理
- Host,虚拟主机
- Context,具体Web应用的上下文,每个请求都在是相应的上下文里处理
- Wrapper,管理servlet生命周期
真正有效的container.backgroundProcess()的方法是
@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();//调用ManagerBase的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 != null) {
try {
instanceManager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString(
"standardContext.backgroundProcess.instanceManager",
resources), e);
}
}
super.backgroundProcess();
}
ContainerBackgroundProcessor实现了Runnable方法,被线程池定期执行,定期检查session的超时。
/**
* Start the background thread that will periodically check for
* session timeouts.
*/
protected void threadStart() {
//注意这里的条件,首先状态是STARTING_PREP,就是预开始,而且backgroundProcessorFuture==null,也就是说只有在第一次预启动的时候才会初始化定时的调度器,理解这里的意思会对上一步的调用者很有帮助
if (backgroundProcessorDelay > 0
&& (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
&& (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
// There was an error executing the scheduled task, get it and log it
try {
backgroundProcessorFuture.get();
} catch (InterruptedException | ExecutionException e) {
log.error(sm.getString("containerBase.backgroundProcess.error"), e);
}
}
//这里构建了一个延迟10S每10S执行的调度器
backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
.scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
backgroundProcessorDelay, backgroundProcessorDelay,
TimeUnit.SECONDS);
}
}
这里比较吊诡的是threadStart()方法又是由ContainerBackgroundProcessorMonitor类里的run方法调用的,
ContainerBackgroundProcessorMonitor的调用者,ContainerBase的startInternal()方法,
protected synchronized void startInternal() throws LifecycleException {
// Start our subordinate components, if any
logger = null;
getLogger();
Cluster cluster = getClusterInternal();
if (cluster instanceof Lifecycle) {
((Lifecycle) cluster).start();
}
Realm realm = getRealmInternal();
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}
// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
MultiThrowable multiThrowable = null;
for (Future<Void> result : results) {
try {
result.get();
} catch (Throwable e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
if (multiThrowable == null) {
multiThrowable = new MultiThrowable();
}
multiThrowable.add(e);
}
}
if (multiThrowable != null) {
throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
multiThrowable.getThrowable());
}
// Start the Valves in our pipeline (including the basic), if any
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
setState(LifecycleState.STARTING);
// Start our thread
//这里又启动了一个定时的调度器
if (backgroundProcessorDelay > 0) {
monitorFuture = Container.getService(ContainerBase.this).getServer()
.getUtilityExecutor().scheduleWithFixedDelay(
new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS);
}
}
可以看出,如果threadStart() 方法里不做限制,就会出现一个定时的调度器里的任务又启动了一个定时调度器,这样几次下来之后,会出现任务无穷多直到把JVM内存耗光。
这里的tomcat的设计有点不太懂,ContainerBackgroundProcessorMonitor的调度器只有第一次的调用会有效,其他的大部分的调度都是浪费的,可能的理由是这里是防止后面的ContainerBackgroundProcessor启动失败,做的重试机制,但是启动成功了就可以结束最外层的调度的,有点资源浪费。还好,Server::getUtilityExecutor都是一个实现,而且里面只有一个work线程。
总结下来,ManagerBase提供了session的失效机制方法供给容器的定时调度器去检查。
4. LinkedHashMap过期机制
LinkedHashMap提供了removeEldestEntry(Map.Entry<K,V> eldest)方法,该方法的返回值默认为false。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
该方法为protected方法,需要子类去继承和改写。
调用方法为:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
从方法名可以看出是在插入的时候,检查过期策略来删除队列里顶端元素,LinkHashMap内部实现了一个队列且是有序的。
下面的实例构造一个了简单的自定义的CustomLinkedHashMap,继承和重写了removeEldestEntry方法,当size大于3的时候,删除队顶元素。
public static void main(String[] args) {
LinkedHashMap<String,String> map = new CustomLinkedHashMap<>();
map.put("111", "aaa");
map.put("222", "bbb");
map.put("333", "ccc");
map.put("444", "ddd");
map.put("555", "eee");
map.forEach((k,v) -> {System.out.println(k + " : " + v);} );
}
static class CustomLinkedHashMap<K,V> extends LinkedHashMap<K, V>{
private static final long serialVersionUID = -1512329794026226223L;
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
if(super.size() > 3) {
return true;
}
return false;
}
}
执行后的结果为:
333 : ccc
444 : ddd
555 : eee
总结:LinkHashMap的过期机制本质上HashMap预留的模板方法,需要子类去实现,而且只能删除队顶元素,功能较弱,当然也可以在
removeEldestEntry方法里遍历整个map里的元素,判断过期机制来删除其他元素,但是这样的实现不符合父类留给的子类的模板方法,可能会有意想不到的BUG。
5. Guava中的LocalCache的过期机制
在构建Cache对象时,可以通过CacheBuilder类的expireAfterAccess和expireAfterWrite两个方法为缓存中的对象指定过期时间,使用CacheBuilder
构建的缓存不会“自动”执行清理和逐出值,也不会在值到期后立即执行或逐出任何类型。相反,它在写入操作期间执行少量维护,或者在写入很少的情况下偶尔执行读取操作。其中,expireAfterWrite方法指定对象被写入到缓存后多久过期,expireAfterAccess指定对象多久没有被访问后过期。主要在get和put的时候检查是否过期,内部分别用两个队列来存储元素AccessQueue和WriteQueue队列,get的时候会向accessQueue中记录,put的时候会同时向accessQueue和WriteQueue队列中记录。
Guava Cache与java1.7的ConcurrentMap的设计类似,采用了segement的分组桶设计,segement继承了ReentrantLock,用锁机制不会有并发冲突,accessQueue和WriteQueue队列都是属于segement当中的全局变量。
执行过期清理的方法
void cleanUp() {
long now = map.ticker.read();
runLockedCleanup(now);//检查过期失效机制并删除元素
runUnlockedCleanup();//通知事件
}
void expireEntries(long now) {
drainRecencyQueue();//收集弱引用被GC的元素
ReferenceEntry<K, V> e;
while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {//检查WriteQueue队列
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {//检查AccessQueue队列
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
}
Guava Cache也提供了方法供外部调用清除过期数据,cleanUp() 和clear()方法,还提供了RemovalListener监听接口用来监听事件。
总结:Guava Cache的过期机制较为全面,既有在插入和查找的时候执行LRU策略,也提供了外部的“自动清理”的接口。
最后总结
从Tomcat的session过期机制分析了常用的缓存过期机制,比如LinkedHashMap过期机制和Guava Cache,一般常用的过期机制无外乎在增删改查的时候检查过期,还有提供外部的“自动清理”机制,后者更需要关注线程安全问题。