Tomcat Session管理分析

前言

在上文Nginx+Tomcat關於Session的管理中簡單介紹瞭如何使用redis來集中管理session,本文首先將介紹默認的管理器是如何管理Session的生命週期的,而後在此基礎上對Redis集中式管理Session進行分析。java

Tomcat Manager介紹

上文中在Tomcat的context.xml中配置了Session管理器RedisSessionManager,實現了經過redis來存儲session的功能;Tomcat自己提供了多種Session管理器,以下類圖:
圖片描述git

1.Manager接口類
定義了用來管理session的基本接口,包括:createSession,findSession,add,remove等對session操做的方法;還有getMaxActive,setMaxActive,getActiveSessions活躍會話的管理;還有Session有效期的接口;以及與Container相關聯的接口;github

2.ManagerBase抽象類
實現了Manager接口,提供了基本的功能,使用ConcurrentHashMap存放session,提供了對session的create,find,add,remove功能,而且在createSession中了使用類SessionIdGenerator來生成會話id,做爲session的惟一標識;web

3.ClusterManager接口類
實現了Manager接口,集羣session的管理器,Tomcat內置的集羣服務器之間的session複製功能;redis

4.ClusterManagerBase抽象類
繼承了ManagerBase抽象類,實現ClusterManager接口類,實現session複製基本功能;spring

5.PersistentManagerBase抽象類
繼承了ManagerBase抽象類,實現了session管理器持久化的基本功能;內部有一個Store存儲類,具體實現有:FileStore和JDBCStore;segmentfault

6.StandardManager類
繼承ManagerBase抽象類,Tomcat默認的Session管理器(單機版);對session提供了持久化功能,tomcat關閉的時候會將session保存到javax.servlet.context.tempdir路徑下的SESSIONS.ser文件中,啓動的時候會今後文件中加載session;瀏覽器

7.PersistentManager類
繼承PersistentManagerBase抽象類,若是session空閒時間過長,將空閒session轉換爲存儲,因此在findsession時會首先從內存中獲取session,獲取不到會多一步到store中獲取,這也是PersistentManager類和StandardManager類的區別;tomcat

8.DeltaManager類
繼承ClusterManagerBase,每個節點session發生變動(增刪改),都會通知其餘全部節點,其餘全部節點進行更新操做,任何一個session在每一個節點都有備份;安全

9.BackupManager類
繼承ClusterManagerBase,會話數據只有一個備份節點,這個備份節點的位置集羣中全部節點均可見;相比較DeltaManager數據傳輸量較小,當集羣規模比較大時DeltaManager的數據傳輸量會很是大;

10.RedisSessionManager類
繼承ManagerBase抽象類,非Tomcat內置的管理器,使用redis集中存儲session,省去了節點之間的session複製,依賴redis的可靠性,比起sessin複製擴展性更好;

Session的生命週期

1.解析獲取requestedSessionId

當咱們在類中經過request.getSession()時,tomcat是如何處理的,能夠查看Request中的doGetSession方法:

protected Session doGetSession(boolean create) {
 
    // There cannot be a session if no context has been assigned yet
    Context context = getContext();
    if (context == null) {
        return (null);
    }
 
    // Return the current session if it exists and is valid
    if ((session != null) && !session.isValid()) {
        session = null;
    }
    if (session != null) {
        return (session);
    }
 
    // Return the requested session if it exists and is valid
    Manager manager = context.getManager();
    if (manager == null) {
        return null;        // Sessions are not supported
    }
    if (requestedSessionId != null) {
        try {
            session = manager.findSession(requestedSessionId);
        } catch (IOException e) {
            session = null;
        }
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        if (session != null) {
            session.access();
            return (session);
        }
    }
 
    // Create a new session if requested and the response is not committed
    if (!create) {
        return (null);
    }
    if ((response != null) &&
            context.getServletContext().getEffectiveSessionTrackingModes().
            contains(SessionTrackingMode.COOKIE) &&
            response.getResponse().isCommitted()) {
        throw new IllegalStateException
        (sm.getString("coyoteRequest.sessionCreateCommitted"));
    }
 
    // Re-use session IDs provided by the client in very limited
    // circumstances.
    String sessionId = getRequestedSessionId();
    if (requestedSessionSSL) {
        // If the session ID has been obtained from the SSL handshake then
        // use it.
    } else if (("/".equals(context.getSessionCookiePath())
            && isRequestedSessionIdFromCookie())) {
        /* This is the common(ish) use case: using the same session ID with
         * multiple web applications on the same host. Typically this is
         * used by Portlet implementations. It only works if sessions are
         * tracked via cookies. The cookie must have a path of "/" else it
         * won't be provided for requests to all web applications.
         *
         * Any session ID provided by the client should be for a session
         * that already exists somewhere on the host. Check if the context
         * is configured for this to be confirmed.
         */
        if (context.getValidateClientProvidedNewSessionId()) {
            boolean found = false;
            for (Container container : getHost().findChildren()) {
                Manager m = ((Context) container).getManager();
                if (m != null) {
                    try {
                        if (m.findSession(sessionId) != null) {
                            found = true;
                            break;
                        }
                    } catch (IOException e) {
                        // Ignore. Problems with this manager will be
                        // handled elsewhere.
                    }
                }
            }
            if (!found) {
                sessionId = null;
            }
        }
    } else {
        sessionId = null;
    }
    session = manager.createSession(sessionId);
 
    // Creating a new session cookie based on that session
    if ((session != null) && (getContext() != null)
            && getContext().getServletContext().
            getEffectiveSessionTrackingModes().contains(
                    SessionTrackingMode.COOKIE)) {
        Cookie cookie =
                ApplicationSessionCookieConfig.createSessionCookie(
                        context, session.getIdInternal(), isSecure());
 
        response.addSessionCookieInternal(cookie);
    }
 
    if (session == null) {
        return null;
    }
 
    session.access();
    return session;
}

若是session已經存在,則直接返回;若是不存在則斷定requestedSessionId是否爲空,若是不爲空則經過requestedSessionId到Session manager中獲取session,若是爲空,而且不是建立session操做,直接返回null;不然會調用Session manager建立一個新的session;
關於requestedSessionId是如何獲取的,Tomcat內部能夠支持從cookie和url中獲取,具體能夠查看CoyoteAdapter類的postParseRequest方法部分代碼:

String sessionID;
if (request.getServletContext().getEffectiveSessionTrackingModes()
        .contains(SessionTrackingMode.URL)) {
 
    // Get the session ID if there was one
    sessionID = request.getPathParameter(
            SessionConfig.getSessionUriParamName(
                    request.getContext()));
    if (sessionID != null) {
        request.setRequestedSessionId(sessionID);
        request.setRequestedSessionURL(true);
    }
}
 
// Look for session ID in cookies and SSL session
parseSessionCookiesId(req, request);

能夠發現首先去url解析sessionId,若是獲取不到則去cookie中獲取,此處的SessionUriParamName=jsessionid;在cookie被瀏覽器禁用的狀況下,咱們能夠看到url後面跟着參數jsessionid=xxxxxx;下面看一下parseSessionCookiesId方法:

String sessionCookieName = SessionConfig.getSessionCookieName(context);
 
for (int i = 0; i < count; i++) {
    ServerCookie scookie = serverCookies.getCookie(i);
    if (scookie.getName().equals(sessionCookieName)) {
        // Override anything requested in the URL
        if (!request.isRequestedSessionIdFromCookie()) {
            // Accept only the first session id cookie
            convertMB(scookie.getValue());
            request.setRequestedSessionId
                (scookie.getValue().toString());
            request.setRequestedSessionCookie(true);
            request.setRequestedSessionURL(false);
            if (log.isDebugEnabled()) {
                log.debug(" Requested cookie session id is " +
                    request.getRequestedSessionId());
            }
        } else {
            if (!request.isRequestedSessionIdValid()) {
                // Replace the session id until one is valid
                convertMB(scookie.getValue());
                request.setRequestedSessionId
                    (scookie.getValue().toString());
            }
        }
    }
}

sessionCookieName也是jsessionid,而後遍歷cookie,從裏面找出name=jsessionid的值賦值給request的requestedSessionId屬性;

2.findSession查詢session

獲取到requestedSessionId以後,會經過此id去session Manager中獲取session,不一樣的管理器獲取的方式不同,已默認的StandardManager爲例:

protected Map<String, Session> sessions = new ConcurrentHashMap<String, Session>();
 
public Session findSession(String id) throws IOException {
    if (id == null) {
        return null;
    }
    return sessions.get(id);
}

3.createSession建立session

沒有獲取到session,指定了create=true,則建立session,已默認的StandardManager爲例:

public Session createSession(String sessionId) {
     
    if ((maxActiveSessions >= 0) &&
            (getActiveSessions() >= maxActiveSessions)) {
        rejectedSessions++;
        throw new TooManyActiveSessionsException(
                sm.getString("managerBase.createSession.ise"),
                maxActiveSessions);
    }
     
    // Recycle or create a Session instance
    Session session = createEmptySession();
 
    // Initialize the properties of the new session and return it
    session.setNew(true);
    session.setValid(true);
    session.setCreationTime(System.currentTimeMillis());
    session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
    String id = sessionId;
    if (id == null) {
        id = generateSessionId();
    }
    session.setId(id);
    sessionCounter++;
 
    SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
    synchronized (sessionCreationTiming) {
        sessionCreationTiming.add(timing);
        sessionCreationTiming.poll();
    }
    return (session);
 
}

若是傳的sessionId爲空,tomcat會生成一個惟一的sessionId,具體能夠參考類StandardSessionIdGenerator的generateSessionId方法;這裏發現建立完session以後並無把session放入ConcurrentHashMap中,其實在session.setId(id)中處理了,具體代碼以下:

public void setId(String id, boolean notify) {
 
    if ((this.id != null) && (manager != null))
        manager.remove(this);
 
    this.id = id;
 
    if (manager != null)
        manager.add(this);
 
    if (notify) {
        tellNew();
    }
}

4.銷燬Session

Tomcat會按期檢測出不活躍的session,而後將其刪除,一方面session佔用內存,另外一方面是安全性的考慮;啓動tomcat的同時會啓動一個後臺線程用來檢測過時的session,具體能夠查看ContainerBase的內部類ContainerBackgroundProcessor:

protected class ContainerBackgroundProcessor implements Runnable {
 
     @Override
     public void run() {
         Throwable t = null;
         String unexpectedDeathMessage = sm.getString(
                 "containerBase.backgroundProcess.unexpectedThreadDeath",
                 Thread.currentThread().getName());
         try {
             while (!threadDone) {
                 try {
                     Thread.sleep(backgroundProcessorDelay * 1000L);
                 } catch (InterruptedException e) {
                     // Ignore
                 }
                 if (!threadDone) {
                     Container parent = (Container) getMappingObject();
                     ClassLoader cl =
                         Thread.currentThread().getContextClassLoader();
                     if (parent.getLoader() != null) {
                         cl = parent.getLoader().getClassLoader();
                     }
                     processChildren(parent, cl);
                 }
             }
         } catch (RuntimeException e) {
             t = e;
             throw e;
         } catch (Error e) {
             t = e;
             throw e;
         } finally {
             if (!threadDone) {
                 log.error(unexpectedDeathMessage, t);
             }
         }
     }
 
     protected void processChildren(Container container, ClassLoader cl) {
         try {
             if (container.getLoader() != null) {
                 Thread.currentThread().setContextClassLoader
                     (container.getLoader().getClassLoader());
             }
             container.backgroundProcess();
         } catch (Throwable t) {
             ExceptionUtils.handleThrowable(t);
             log.error("Exception invoking periodic operation: ", t);
         } finally {
             Thread.currentThread().setContextClassLoader(cl);
         }
         Container[] children = container.findChildren();
         for (int i = 0; i < children.length; i++) {
             if (children[i].getBackgroundProcessorDelay() <= 0) {
                 processChildren(children[i], cl);
             }
         }
     }
 }

backgroundProcessorDelay默認值是10,也就是每10秒檢測一次,而後調用Container的backgroundProcess方法,此方法又調用Manager裏面的backgroundProcess:

public void backgroundProcess() {
    count = (count + 1) % processExpiresFrequency;
    if (count == 0)
        processExpires();
}
 
/**
 * Invalidate all sessions that have expired.
 */
public void processExpires() {
 
    long timeNow = System.currentTimeMillis();
    Session sessions[] = findSessions();
    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 );
 
}

processExpiresFrequency默認值是6,那其實最後就是6*10=60秒執行一次processExpires,具體如何檢測過時在session的isValid方法中:

public boolean isValid() {
 
    if (!this.isValid) {
        return false;
    }
 
    if (this.expiring) {
        return true;
    }
 
    if (ACTIVITY_CHECK && accessCount.get() > 0) {
        return true;
    }
 
    if (maxInactiveInterval > 0) {
        long timeNow = System.currentTimeMillis();
        int timeIdle;
        if (LAST_ACCESS_AT_START) {
            timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L);
        } else {
            timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L);
        }
        if (timeIdle >= maxInactiveInterval) {
            expire(true);
        }
    }
 
    return this.isValid;
}

主要是經過對比當前時間到上次活躍的時間是否超過了maxInactiveInterval,若是超過了就作expire處理;

Redis集中式管理Session分析

在上文中使用tomcat-redis-session-manager來管理session,下面來分析一下是若是經過redis來集中式管理Session的;圍繞session如何獲取,如何建立,什麼時候更新到redis,以及什麼時候被移除;

1.如何獲取

RedisSessionManager重寫了findSession方法

public Session findSession(String id) throws IOException {
    RedisSession session = null;
 
    if (null == id) {
      currentSessionIsPersisted.set(false);
      currentSession.set(null);
      currentSessionSerializationMetadata.set(null);
      currentSessionId.set(null);
    } else if (id.equals(currentSessionId.get())) {
      session = currentSession.get();
    } else {
      byte[] data = loadSessionDataFromRedis(id);
      if (data != null) {
        DeserializedSessionContainer container = sessionFromSerializedData(id, data);
        session = container.session;
        currentSession.set(session);
        currentSessionSerializationMetadata.set(container.metadata);
        currentSessionIsPersisted.set(true);
        currentSessionId.set(id);
      } else {
        currentSessionIsPersisted.set(false);
        currentSession.set(null);
        currentSessionSerializationMetadata.set(null);
        currentSessionId.set(null);
      }
    }

sessionId不爲空的狀況下,會先比較sessionId是否等於currentSessionId中的sessionId,若是等於則從currentSession中取出session,currentSessionId和currentSession都是ThreadLocal變量,這裏並無直接從redis裏面取數據,若是同一線程沒有去處理其餘用戶信息,是能夠直接從內存中取出的,提升了性能;最後才從redis裏面獲取數據,從redis裏面獲取的是一段二進制數據,須要進行反序列化操做,相關序列化和反序列化都在JavaSerializer類中:

public void deserializeInto(byte[] data, RedisSession session, SessionSerializationMetadata metadata)
        throws IOException, ClassNotFoundException {
    BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data));
    Throwable arg4 = null;
 
    try {
        CustomObjectInputStream x2 = new CustomObjectInputStream(bis, this.loader);
        Throwable arg6 = null;
 
        try {
            SessionSerializationMetadata x21 = (SessionSerializationMetadata) x2.readObject();
            metadata.copyFieldsFrom(x21);
            session.readObjectData(x2);
        } catch (Throwable arg29) {
    ......
}

二進制數據中保存了2個對象,分別是SessionSerializationMetadata和RedisSession,SessionSerializationMetadata裏面保存的是Session中的attributes信息,RedisSession其實也有attributes數據,至關於這份數據保存了2份;

2.如何建立

一樣RedisSessionManager重寫了createSession方法,2個重要的點分別:sessionId的惟一性問題和session保存到redis中;

// Ensure generation of a unique session identifier.
if (null != requestedSessionId) {
  sessionId = sessionIdWithJvmRoute(requestedSessionId, jvmRoute);
  if (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L) {
    sessionId = null;
  }
} else {
  do {
    sessionId = sessionIdWithJvmRoute(generateSessionId(), jvmRoute);
  } while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already existed
}

分佈式環境下有可能出現生成的sessionId相同的狀況,因此須要確保惟一性;保存session到redis中是最核心的一個方法,什麼時候更新,什麼時候過時都在此方法中處理;

3.什麼時候更新到redis

具體看saveInternal方法

protected boolean saveInternal(Jedis jedis, Session session, boolean forceSave) throws IOException {
    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());
        }
      }
 
      byte[] binaryId = redisSession.getId().getBytes();
 
      Boolean isCurrentSessionPersisted;
      SessionSerializationMetadata sessionSerializationMetadata = currentSessionSerializationMetadata.get();
      byte[] originalSessionAttributesHash = sessionSerializationMetadata.getSessionAttributesHash();
      byte[] sessionAttributesHash = null;
      if (
           forceSave
           || redisSession.isDirty()
           || null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get())
            || !isCurrentSessionPersisted
           || !Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.attributesHashFrom(redisSession)))
         ) {
 
        log.trace("Save was determined to be necessary");
 
        if (null == sessionAttributesHash) {
          sessionAttributesHash = serializer.attributesHashFrom(redisSession);
        }
 
        SessionSerializationMetadata updatedSerializationMetadata = new SessionSerializationMetadata();
        updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash);
 
        jedis.set(binaryId, serializer.serializeFrom(redisSession, updatedSerializationMetadata));
 
        redisSession.resetDirtyTracking();
        currentSessionSerializationMetadata.set(updatedSerializationMetadata);
        currentSessionIsPersisted.set(true);
      } else {
        log.trace("Save was determined to be unnecessary");
      }
 
      log.trace("Setting expire timeout on session [" + redisSession.getId() + "] to " + getMaxInactiveInterval());
      jedis.expire(binaryId, getMaxInactiveInterval());
 
      error = false;
 
      return error;
    } catch (IOException e) {
      log.error(e.getMessage());
 
      throw e;
    } finally {
      return error;
    }
  }

以上方法中大體有5中狀況下須要保存數據到redis中,分別是:forceSave,redisSession.isDirty(),null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get()),!isCurrentSessionPersisted以及!Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.attributesHashFrom(redisSession)))其中一個爲true的狀況下保存數據到reids中;

3.1重點看一下forceSave,能夠理解forceSave就是內置保存策略的一個標識,提供了三種內置保存策略:DEFAULT,SAVE_ON_CHANGE,ALWAYS_SAVE_AFTER_REQUEST
DEFAULT:默認保存策略,依賴其餘四種狀況保存session,
SAVE_ON_CHANGE:每次session.setAttribute()、session.removeAttribute()觸發都會保存,
ALWAYS_SAVE_AFTER_REQUEST:每個request請求後都強制保存,不管是否檢測到變化;

3.2redisSession.isDirty()檢測session內部是否有髒數據

public Boolean isDirty() {
    return Boolean.valueOf(this.dirty.booleanValue() || !this.changedAttributes.isEmpty());
}

每個request請求後檢測是否有髒數據,有髒數據才保存,實時性沒有SAVE_ON_CHANGE高,可是也沒有ALWAYS_SAVE_AFTER_REQUEST來的粗暴;

3.3後面三種狀況都是用來檢測三個ThreadLocal變量;

4.什麼時候被移除

上一節中介紹了Tomcat內置看按期檢測session是否過時,ManagerBase中提供了processExpires方法來處理session過去的問題,可是在RedisSessionManager重寫了此方法

public void processExpires() {
}

直接不作處理了,具體是利用了redis的設置生存時間功能,具體在saveInternal方法中:

jedis.expire(binaryId, getMaxInactiveInterval());

總結

本文大體分析了Tomcat Session管理器,以及tomcat-redis-session-manager是如何進行session集中式管理的,可是此工具徹底依賴tomcat容器,若是想徹底獨立於應用服務器的方案,
Spring session是一個不錯的選擇。

相關文章
相關標籤/搜索