• 深入剖析Tomcat会话机制


    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》

  • 相关阅读:
    并查集的来龙去脉
    改变像素
    PHP自学之路-----javascript基础入门
    推荐管理类图书
    ewebeditor编辑器ASP/ASPX/PHP/JSP版本漏洞利用总结及解决方法
    PHP文件包含漏洞剖析
    文件上传漏洞总结
    服务器软件解析漏洞总结
    JAVA环境变量配置
    Python2.x与Python3.x的区别
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157722.html
Copyright © 2020-2023  润新知