1缓存机制
Tomcat默认将Session保存到内存中。但同时,Tomcat也提供了PersistentManager配合不同的Store实现的方式,使Session可以被保存到不同地方(Database,Redis,Memcached等)。
例如下面的配置:
<ManagerclassName="org.apache.catalina.session.PersistentManager" debug="0" saveOnRestart="true" maxActiveSession="-1" minIdleSwap="0" maxIdleSwap="0" maxIdleBackup="-1"> <StoreclassName="com.cdai.test.RedisStore" host="192.168.1.1"port="6379"/> </Manager>
通过PersistentManager配合RedisStore,实现了Session存储到Redis中。但要注意的是:切换Manager和Store实现都不会让Session全部保存到其他位置。
Tomcat只是在下面三种情况会将Session通过Store保存起来:
Ø 当Session的空闲时间超过minIdleSwap和maxIdleSwap时,会将Session换出
Ø 当Session的空闲时间超过maxIdleBackup时,会将Session备份出去
Ø 当Session总数大于maxActiveSession时,会将超出部分的空闲Session换出
所以只是简单地切换Manager和Store的实现,并不能使Tomcat把Session都保存到我们想要的地方。Tomcat还是会把内存当做Session的主存储器,我们配置的Store只是作为辅助存储器。 下面先来深入研究下Tomcat源码来研究下Session管理机制,最后我们再看下如何能达到我们想要的效果。
2深入源码
2.1使用Session
下面以一个使用Session的Servlet为切入点,开始分析Tomcat源码。
public class LoginServletextends HttpServlet { private static int i = 0; @Override protected void service(HttpServletRequestreq, HttpServletResponse resp) throws ServletException, IOException { HttpSession session =req.getSession(true); System.out.println(session.getAttribute("cdai")); session.setAttribute("cdai","helloworld" + i++); } }
默认配置下,我们通过HttpServletRequest得到的HttpSession实现是StandardSession。设置属性时,只是把属性值保存到StandardSession的一个Map中。
2.2后台线程
在Tomcat 6中所有后台线程的调度都是在ContainerBase.backgroundProcess()中统一处理的。
public void backgroundProcess() { if (!started) return; if (cluster != null) { try { cluster.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.cluster",cluster), e); } } if (loader != null) { try { loader.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.loader",loader), e); } } if (manager != null) { try { manager.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.manager",manager), e); } } if (realm != null) { try { realm.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.realm",realm), e); } } Valve current = pipeline.getFirst(); while (current != null) { try { current.backgroundProcess(); } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.valve",current), e); } current = current.getNext(); } lifecycle.fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); }
上面源码中我们关心的是manager.backgroundProcess()一行,这是Manager后台处理的入口。
先来看下Manager的类关系:
ManagerBase 实现了Manager接口的backgroundProcess()方法。在ManagerBase.backgroundProcess()中调用子类PersistentManagerBase.processExpire()。在processExpire()中会对前面提到的Session被持久化的三种情况进行处理。下面就来看下这三种情况的源码。
2.3换出和备份
先以情况1换出空闲时间过长的Session的源码为例。
protected void processMaxIdleSwaps() { if (!isStarted() || maxIdleSwap < 0) return; Session sessions[] = findSessions(); long timeNow =System.currentTimeMillis(); // Swap out all sessions idle longerthan maxIdleSwap if (maxIdleSwap >= 0) { for (int i = 0; i <sessions.length; i++) { StandardSession session =(StandardSession) sessions[i]; synchronized (session) { if (!session.isValid()) continue; int timeIdle = // Truncate,do not round up (int) ((timeNow -session.getLastAccessedTime()) / 1000L); if (timeIdle > maxIdleSwap && timeIdle> minIdleSwap) { if (session.accessCount!= null && session.accessCount.get() > 0) { // Session iscurrently being accessed - skip it continue; } if(log.isDebugEnabled()) log.debug(sm.getString ("persistentManager.swapMaxIdle", session.getIdInternal(), newInteger(timeIdle))); try { swapOut(session); } catch (IOException e){ ; // This is logged in writeSession() } } } } } } protected void swapOut(Session session)throws IOException { if (store == null ||!session.isValid()) { return; } ((StandardSession)session).passivate(); writeSession(session); super.remove(session); session.recycle(); } protected void writeSession(Sessionsession) throws IOException { if (store == null || !session.isValid()){ return; } try { if(SecurityUtil.isPackageProtectionEnabled()){ try{ AccessController.doPrivileged(newPrivilegedStoreSave(session)); }catch(PrivilegedActionExceptionex){ Exception exception =ex.getException(); log.error("Exceptionin the Store during writeSession: " + exception); exception.printStackTrace(); } } else { store.save(session); } } catch (IOException e) { log.error(sm.getString ("persistentManager.serializeError",session.getIdInternal(), e)); throw e; } } private class PrivilegedStoreSave implementsPrivilegedExceptionAction { private Session session; PrivilegedStoreSave(Session session){ this.session = session; } public Object run() throws Exception{ store.save(session); return null; } }
processMaxIdleSwaps()会将每个Session的空闲时间与minIdleSwap和maxIdleSwap比较,然后调用swapOut()将Session换出。在swapOut()中,通过PrivilegedStoreSave类调用store的save()方法将session保存到不同的地方。
另外两种情况的处理与processMaxIdleSwaps()类似。处理方法为:processMaxIdleBackups()和processMaxActiveSwaps()。
3切换方案
3.1简单方案
一种简单方法是依旧使用Tomcat提供的PersistentManager,自己实现Store类。之后通过调整Manager参数让Tomcat尽快把Session换出,例如开篇那段配置中,将min/maxIdleSwap设置为0。这种方法的缺点是Servlet设置Session属性,并且请求结束后,可能很大延迟后Session才会被换出。
3.2最终方案
下面来看下开源tomcat-redis-session-manager实现的源码,分析一下如何能完美切换。
这是tomcat-redis-session-manager官方提供的配置:
<ValveclassName="com.radiadesign.catalina.session.RedisSessionHandlerValve"/>
<ManagerclassName="com.radiadesign.catalina.session.RedisSessionManager"
host="localhost" <!-- optional: defaults to"localhost" -->
port="6379" <!-- optional: defaults to "6379"-->
database="0" <!-- optional: defaults to "0"-->
maxInactiveInterval="60" <!-- optional: defaults to"60" (in seconds) --> />
可以看到除了自定义了Manager,它还提供了一个Valve实现。在Tomcat中,请求被Servlet处理前是要经过管道中的许多Valve对象处理的,类似于Struts2中的Interceptor。
public classRedisSessionHandlerValve extends ValveBase { private final Log log =LogFactory.getLog(RedisSessionManager.class); private RedisSessionManager manager; public voidsetRedisSessionManager(RedisSessionManager manager) { this.manager = manager; } @Override public void invoke(Request request, Responseresponse) throws IOException, ServletException { try { getNext().invoke(request, response); } finally { final Session session =request.getSessionInternal(false); storeOrRemoveSession(session); manager.afterRequest(); } } private void storeOrRemoveSession(Sessionsession) { try { if (session != null) { if (session.isValid()) { log.trace("Request with sessioncompleted, saving session " + session.getId()); if (session.getSession() != null) { log.trace("HTTP Sessionpresent, saving " + session.getId()); manager.save(session); } else { log.trace("No HTTP Sessionpresent, Not saving " + session.getId()); } } else { log.trace("HTTP Session has beeninvalidated, removing :" + session.getId()); manager.remove(session); } } } catch (Exception e) { // Do nothing. } } }
RedisSessionHandlerValve的处理逻辑很简单:调用getNext().invoke(request,response)使后续Valve继续处理请求。在storeOrRemoveSession()中直接调用manager.save(),而不是像之前等待Tomcat调度。
RedisSessionManager.save()实现也很简单,将Session序列化后使用Jedis保存到Redis中。
public void save(Session session) throwsIOException { Jedis jedis = null; Boolean error = true; try { log.trace("Saving session " +session + " into Redis"); RedisSession redisSession =(RedisSession) session; if (log.isTraceEnabled()) { log.trace("Session Contents[" + redisSession.getId() + "]:"); Enumeration en =redisSession.getAttributeNames(); while(en.hasMoreElements()) { log.trace(" " + en.nextElement()); } } Boolean sessionIsDirty =redisSession.isDirty(); redisSession.resetDirtyTracking(); byte[] binaryId =redisSession.getId().getBytes(); jedis = acquireConnection(); Boolean isCurrentSessionPersisted =this.currentSessionIsPersisted.get(); if (sessionIsDirty ||(isCurrentSessionPersisted == null || !isCurrentSessionPersisted)) { jedis.set(binaryId, serializer.serializeFrom(redisSession)); } currentSessionIsPersisted.set(true); log.trace("Setting expire timeout onsession [" + redisSession.getId() + "] to " +getMaxInactiveInterval()); jedis.expire(binaryId,getMaxInactiveInterval()); error = false; } catch (IOException e) { log.error(e.getMessage()); throw e; } finally { if (jedis != null) { returnConnection(jedis, error); } } }
参考资料
1 tomcat-redis-session-manager官网
https://github.com/jcoleman/tomcat-redis-session-manager
2《深入剖析Tomcat》