Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1106421
  • 博文数量: 165
  • 博客积分: 3900
  • 博客等级: 中校
  • 技术积分: 1887
  • 用 户 组: 普通用户
  • 注册时间: 2007-04-06 15:15
文章分类

全部博文(165)

文章存档

2020年(3)

2019年(8)

2017年(2)

2016年(8)

2015年(14)

2013年(15)

2012年(32)

2011年(11)

2010年(14)

2009年(7)

2008年(20)

2007年(31)

分类: 服务器与存储

2016-08-23 22:35:40

1缓存机制

Tomcat默认将Session保存到内存中。但同时,Tomcat也提供了PersistentManager配合不同的Store实现的方式,使Session可以被保存到不同地方(Database,Redis,Memcached等)。 

例如下面的配置:


[html] view plain copy
  1. <ManagerclassNameManagerclassName="org.apache.catalina.session.PersistentManager"  
  2.           debug="0"  
  3.           saveOnRestart="true"  
  4.           maxActiveSession="-1"  
  5.           minIdleSwap="0"  
  6.           maxIdleSwap="0"  
  7.           maxIdleBackup="-1">  
  8.           <StoreclassNameStoreclassName="com.cdai.test.RedisStore" host="192.168.1.1"port="6379"/>  
  9. 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源码。


[java] view plain copy
  1. public class LoginServletextends HttpServlet {  
  2.    
  3.     private static int i = 0;  
  4.    
  5.     @Override  
  6.     protected void service(HttpServletRequestreq, HttpServletResponse resp) throws ServletException, IOException {  
  7.         HttpSession session =req.getSession(true);  
  8.         System.out.println(session.getAttribute("cdai"));  
  9.         session.setAttribute("cdai","helloworld" + i++);  
  10.     }  
  11.    
  12. }  


默认配置下,我们通过HttpServletRequest得到的HttpSession实现是StandardSession。设置属性时,只是把属性值保存到StandardSession的一个Map中。

 

2.2后台线程

在Tomcat 6中所有后台线程的调度都是在ContainerBase.backgroundProcess()中统一处理的。


[java] view plain copy
  1. public void backgroundProcess() {  
  2.      
  3.     if (!started)  
  4.         return;  
  5.   
  6.     if (cluster != null) {  
  7.         try {  
  8.             cluster.backgroundProcess();  
  9.         } catch (Exception e) {  
  10.            log.warn(sm.getString("containerBase.backgroundProcess.cluster",cluster), e);                 
  11.         }  
  12.     }  
  13.     if (loader != null) {  
  14.         try {  
  15.             loader.backgroundProcess();  
  16.         } catch (Exception e) {  
  17.            log.warn(sm.getString("containerBase.backgroundProcess.loader",loader), e);                 
  18.         }  
  19.     }  
  20.     if (manager != null) {  
  21.         try {  
  22.             manager.backgroundProcess();  
  23.         } catch (Exception e) {  
  24.            log.warn(sm.getString("containerBase.backgroundProcess.manager",manager), e);                 
  25.         }  
  26.     }  
  27.     if (realm != null) {  
  28.         try {  
  29.             realm.backgroundProcess();  
  30.         } catch (Exception e) {  
  31.            log.warn(sm.getString("containerBase.backgroundProcess.realm",realm), e);                 
  32.         }  
  33.     }  
  34.     Valve current = pipeline.getFirst();  
  35.     while (current != null) {  
  36.         try {  
  37.             current.backgroundProcess();  
  38.         } catch (Exception e) {  
  39.            log.warn(sm.getString("containerBase.backgroundProcess.valve",current), e);                 
  40.         }  
  41.         current = current.getNext();  
  42.     }  
  43.    lifecycle.fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);  
  44. }  


上面源码中我们关心的是manager.backgroundProcess()一行,这是Manager后台处理的入口。

先来看下Manager的类关系:

ManagerBase 实现了Manager接口的backgroundProcess()方法。在ManagerBase.backgroundProcess()中调用子类PersistentManagerBase.processExpire()。在processExpire()中会对前面提到的Session被持久化的三种情况进行处理。下面就来看下这三种情况的源码。

 

2.3换出和备份

先以情况1换出空闲时间过长的Session的源码为例。


[java] view plain copy
  1. protected void processMaxIdleSwaps() {  
  2.     if (!isStarted() || maxIdleSwap < 0)  
  3.         return;  
  4.   
  5.     Session sessions[] = findSessions();  
  6.     long timeNow =System.currentTimeMillis();  
  7.   
  8.     // Swap out all sessions idle longerthan maxIdleSwap  
  9.     if (maxIdleSwap >= 0) {  
  10.         for (int i = 0; i 
  11.             StandardSession session =(StandardSession) sessions[i];  
  12.             synchronized (session) {  
  13.                 if (!session.isValid())  
  14.                     continue;  
  15.                 int timeIdle = // Truncate,do not round up  
  16.                     (int) ((timeNow -session.getLastAccessedTime()) / 1000L);  
  17.                 if (timeIdle > maxIdleSwap && timeIdle> minIdleSwap) {  
  18.                     if (session.accessCount!= null &&  
  19.                            session.accessCount.get() > 0) {  
  20.                             // Session iscurrently being accessed - skip it  
  21.                             continue;  
  22.                         }  
  23.                     if(log.isDebugEnabled())  
  24.                        log.debug(sm.getString  
  25.                            ("persistentManager.swapMaxIdle",  
  26.                              session.getIdInternal(), newInteger(timeIdle)));  
  27.                     try {  
  28.                         swapOut(session);  
  29.                     } catch (IOException e){  
  30.                         ;   // This is logged in writeSession()  
  31.                     }  
  32.                 }  
  33.             }  
  34.         }  
  35.     }  
  36.   
  37.   
  38.   
  39. protected void swapOut(Session session)throws IOException {  
  40.     if (store == null ||!session.isValid()) {  
  41.         return;  
  42.     }  
  43.   
  44.     ((StandardSession)session).passivate();  
  45.     writeSession(session);  
  46.     super.remove(session);  
  47.     session.recycle();  
  48. }  
  49.   
  50. protected void writeSession(Sessionsession) throws IOException {  
  51.     if (store == null || !session.isValid()){  
  52.         return;  
  53.     }  
  54.   
  55.     try {  
  56.         if(SecurityUtil.isPackageProtectionEnabled()){  
  57.             try{  
  58.                 AccessController.doPrivileged(newPrivilegedStoreSave(session));  
  59.             }catch(PrivilegedActionExceptionex){  
  60.                 Exception exception =ex.getException();  
  61.                 log.error("Exceptionin the Store during writeSession: "  
  62.                           + exception);  
  63.                exception.printStackTrace();                         
  64.             }  
  65.         } else {  
  66.              store.save(session);  
  67.         }    
  68.     } catch (IOException e) {  
  69.         log.error(sm.getString  
  70.             ("persistentManager.serializeError",session.getIdInternal(), e));  
  71.         throw e;  
  72.     }  
  73. }  
  74.   
  75. private class PrivilegedStoreSave implementsPrivilegedExceptionAction {  
  76.     private Session session;     
  77.   
  78.     PrivilegedStoreSave(Session session){      
  79.         this.session = session;  
  80.     }  
  81.   
  82.     public Object run() throws Exception{  
  83.        store.save(session);  
  84.        return null;  
  85.     }  
  86. }  

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官方提供的配置:

        host="localhost"

        port="6379"

        database="0"

        maxInactiveInterval="60" />

 

可以看到除了自定义了Manager,它还提供了一个Valve实现。在Tomcat中,请求被Servlet处理前是要经过管道中的许多Valve对象处理的,类似于Struts2中的Interceptor。


[java] view plain copy
  1. public classRedisSessionHandlerValve extends ValveBase {  
  2.   private final Log log =LogFactory.getLog(RedisSessionManager.class);  
  3.   private RedisSessionManager manager;  
  4.    
  5.   public voidsetRedisSessionManager(RedisSessionManager manager) {  
  6.     this.manager = manager;  
  7.   }  
  8.    
  9.   @Override  
  10.   public void invoke(Request request, Responseresponse) throws IOException, ServletException {  
  11.     try {  
  12.       getNext().invoke(request, response);  
  13.     } finally {  
  14.       final Session session =request.getSessionInternal(false);  
  15.       storeOrRemoveSession(session);  
  16.       manager.afterRequest();  
  17.     }  
  18.   }  
  19.    
  20.   private void storeOrRemoveSession(Sessionsession) {  
  21.     try {  
  22.       if (session != null) {  
  23.         if (session.isValid()) {  
  24.           log.trace("Request with sessioncompleted, saving session " + session.getId());  
  25.           if (session.getSession() != null) {  
  26.             log.trace("HTTP Sessionpresent, saving " + session.getId());  
  27.             manager.save(session);  
  28.           } else {  
  29.             log.trace("No HTTP Sessionpresent, Not saving " + session.getId());  
  30.           }  
  31.         } else {  
  32.           log.trace("HTTP Session has beeninvalidated, removing :" + session.getId());  
  33.           manager.remove(session);  
  34.         }  
  35.       }  
  36.     } catch (Exception e) {  
  37.       // Do nothing.  
  38.     }  
  39.   }  
  40. }  


RedisSessionHandlerValve的处理逻辑很简单:调用getNext().invoke(request,response)使后续Valve继续处理请求。在storeOrRemoveSession()中直接调用manager.save(),而不是像之前等待Tomcat调度。

 

RedisSessionManager.save()实现也很简单,将Session序列化后使用Jedis保存到Redis中。


[java] view plain copy
  1. public void save(Session session) throwsIOException {  
  2.   Jedis jedis = null;  
  3.   Boolean error = true;  
  4.   
  5.   try {  
  6.     log.trace("Saving session " +session + " into Redis");  
  7.   
  8.     RedisSession redisSession =(RedisSession) session;  
  9.   
  10.     if (log.isTraceEnabled()) {  
  11.       log.trace("Session Contents[" + redisSession.getId() + "]:");  
  12.       Enumeration en =redisSession.getAttributeNames();  
  13.       while(en.hasMoreElements()) {  
  14.         log.trace("  " + en.nextElement());  
  15.       }  
  16.     }  
  17.   
  18.     Boolean sessionIsDirty =redisSession.isDirty();  
  19.   
  20.     redisSession.resetDirtyTracking();  
  21.     byte[] binaryId =redisSession.getId().getBytes();  
  22.   
  23.     jedis = acquireConnection();  
  24.   
  25.     Boolean isCurrentSessionPersisted =this.currentSessionIsPersisted.get();  
  26.     if (sessionIsDirty ||(isCurrentSessionPersisted == null || !isCurrentSessionPersisted)) {  
  27.       jedis.set(binaryId, serializer.serializeFrom(redisSession));  
  28.     }  
  29.   
  30.     currentSessionIsPersisted.set(true);  
  31.   
  32.     log.trace("Setting expire timeout onsession [" + redisSession.getId() + "] to " +getMaxInactiveInterval());  
  33.     jedis.expire(binaryId,getMaxInactiveInterval());  
  34.   
  35.     error = false;  
  36.   } catch (IOException e) {  
  37.     log.error(e.getMessage());  
  38.   
  39.     throw e;  
  40.   } finally {  
  41.     if (jedis != null) {  
  42.       returnConnection(jedis, error);  
  43.     }  
  44.   }  
  45. }  
阅读(994) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~