前言
在上文Nginx+Tomcat關(guān)于Session的管理中簡(jiǎn)單介紹了如何使用redis來(lái)集中管理session,本文首先將介紹默認(rèn)的管理器是如何管理Session的生命周期的,然后在此基礎(chǔ)上對(duì)Redis集中式管理Session進(jìn)行分析。
Tomcat Manager介紹
上文中在Tomcat的context.xml中配置了Session管理器RedisSessionManager,實(shí)現(xiàn)了通過(guò)redis來(lái)存儲(chǔ)session的功能;Tomcat本身提供了多種Session管理器,如下類圖:
1.Manager接口類
定義了用來(lái)管理session的基本接口,包括:createSession,findSession,add,remove等對(duì)session操作的方法;還有g(shù)etMaxActive,setMaxActive,getActiveSessions活躍會(huì)話的管理;還有Session有效期的接口;以及與Container相關(guān)聯(lián)的接口;
2.ManagerBase抽象類
實(shí)現(xiàn)了Manager接口,提供了基本的功能,使用ConcurrentHashMap存放session,提供了對(duì)session的create,find,add,remove功能,并且在createSession中了使用類SessionIdGenerator來(lái)生成會(huì)話id,作為session的唯一標(biāo)識(shí);
3.ClusterManager接口類
實(shí)現(xiàn)了Manager接口,集群session的管理器,Tomcat內(nèi)置的集群服務(wù)器之間的session復(fù)制功能;
4.ClusterManagerBase抽象類
繼承了ManagerBase抽象類,實(shí)現(xiàn)ClusterManager接口類,實(shí)現(xiàn)session復(fù)制基本功能;
5.PersistentManagerBase抽象類
繼承了ManagerBase抽象類,實(shí)現(xiàn)了session管理器持久化的基本功能;內(nèi)部有一個(gè)Store存儲(chǔ)類,具體實(shí)現(xiàn)有:FileStore和JDBCStore;
6.StandardManager類
繼承ManagerBase抽象類,Tomcat默認(rèn)的Session管理器(單機(jī)版);對(duì)session提供了持久化功能,tomcat關(guān)閉的時(shí)候會(huì)將session保存到j(luò)avax.servlet.context.tempdir路徑下的SESSIONS.ser文件中,啟動(dòng)的時(shí)候會(huì)從此文件中加載session;
7.PersistentManager類
繼承PersistentManagerBase抽象類,如果session空閑時(shí)間過(guò)長(zhǎng),將空閑session轉(zhuǎn)換為存儲(chǔ),所以在findsession時(shí)會(huì)首先從內(nèi)存中獲取session,獲取不到會(huì)多一步到store中獲取,這也是PersistentManager類和StandardManager類的區(qū)別;
8.DeltaManager類
繼承ClusterManagerBase,每一個(gè)節(jié)點(diǎn)session發(fā)生變更(增刪改),都會(huì)通知其他所有節(jié)點(diǎn),其他所有節(jié)點(diǎn)進(jìn)行更新操作,任何一個(gè)session在每個(gè)節(jié)點(diǎn)都有備份;
9.BackupManager類
繼承ClusterManagerBase,會(huì)話數(shù)據(jù)只有一個(gè)備份節(jié)點(diǎn),這個(gè)備份節(jié)點(diǎn)的位置集群中所有節(jié)點(diǎn)都可見;相比較DeltaManager數(shù)據(jù)傳輸量較小,當(dāng)集群規(guī)模比較大時(shí)DeltaManager的數(shù)據(jù)傳輸量會(huì)非常大;
10.RedisSessionManager類
繼承ManagerBase抽象類,非Tomcat內(nèi)置的管理器,使用redis集中存儲(chǔ)session,省去了節(jié)點(diǎn)之間的session復(fù)制,依賴redis的可靠性,比起sessin復(fù)制擴(kuò)展性更好;
Session的生命周期
1.解析獲取requestedSessionId
當(dāng)我們?cè)陬愔型ㄟ^(guò)request.getSession()時(shí),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已經(jīng)存在,則直接返回;如果不存在則判定requestedSessionId是否為空,如果不為空則通過(guò)requestedSessionId到Session manager中獲取session,如果為空,并且不是創(chuàng)建session操作,直接返回null;否則會(huì)調(diào)用Session manager創(chuàng)建一個(gè)新的session;
關(guān)于requestedSessionId是如何獲取的,Tomcat內(nèi)部可以支持從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);
可以發(fā)現(xiàn)首先去url解析sessionId,如果獲取不到則去cookie中獲取,此處的SessionUriParamName=jsessionid;在cookie被瀏覽器禁用的情況下,我們可以看到url后面跟著參數(shù)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之后,會(huì)通過(guò)此id去session Manager中獲取session,不同的管理器獲取的方式不一樣,已默認(rèn)的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創(chuàng)建session
沒有獲取到session,指定了create=true,則創(chuàng)建session,已默認(rèn)的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會(huì)生成一個(gè)唯一的sessionId,具體可以參考類StandardSessionIdGenerator的generateSessionId方法;這里發(fā)現(xiàn)創(chuàng)建完session之后并沒有把session放入ConcurrentHashMap中,其實(shí)在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會(huì)定期檢測(cè)出不活躍的session,然后將其刪除,一方面session占用內(nèi)存,另一方面是安全性的考慮;啟動(dòng)tomcat的同時(shí)會(huì)啟動(dòng)一個(gè)后臺(tái)線程用來(lái)檢測(cè)過(guò)期的session,具體可以查看ContainerBase的內(nèi)部類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默認(rèn)值是10,也就是每10秒檢測(cè)一次,然后調(diào)用Container的backgroundProcess方法,此方法又調(diào)用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默認(rèn)值是6,那其實(shí)最后就是6*10=60秒執(zhí)行一次processExpires,具體如何檢測(cè)過(guò)期在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;
}
主要是通過(guò)對(duì)比當(dāng)前時(shí)間到上次活躍的時(shí)間是否超過(guò)了maxInactiveInterval,如果超過(guò)了就做expire處理;
Redis集中式管理Session分析
在上文中使用tomcat-redis-session-manager來(lái)管理session,下面來(lái)分析一下是如果通過(guò)redis來(lái)集中式管理Session的;圍繞session如何獲取,如何創(chuàng)建,何時(shí)更新到redis,以及何時(shí)被移除;
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不為空的情況下,會(huì)先比較sessionId是否等于currentSessionId中的sessionId,如果等于則從currentSession中取出session,currentSessionId和currentSession都是ThreadLocal變量,這里并沒有直接從redis里面取數(shù)據(jù),如果同一線程沒有去處理其他用戶信息,是可以直接從內(nèi)存中取出的,提高了性能;最后才從redis里面獲取數(shù)據(jù),從redis里面獲取的是一段二進(jìn)制數(shù)據(jù),需要進(jìn)行反序列化操作,相關(guān)序列化和反序列化都在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) {
......
}
二進(jìn)制數(shù)據(jù)中保存了2個(gè)對(duì)象,分別是SessionSerializationMetadata和RedisSession,SessionSerializationMetadata里面保存的是Session中的attributes信息,RedisSession其實(shí)也有attributes數(shù)據(jù),相當(dāng)于這份數(shù)據(jù)保存了2份;
2.如何創(chuàng)建
同樣RedisSessionManager重寫了createSession方法,2個(gè)重要的點(diǎn)分別: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
}
分布式環(huán)境下有可能出現(xiàn)生成的sessionId相同的情況,所以需要確保唯一性;保存session到redis中是最核心的一個(gè)方法,何時(shí)更新,何時(shí)過(guò)期都在此方法中處理;
3.何時(shí)更新到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中情況下需要保存數(shù)據(jù)到redis中,分別是:forceSave,redisSession.isDirty(),null == (isCurrentSessionPersisted = this.currentSessionIsPersisted.get()),!isCurrentSessionPersisted以及!Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.attributesHashFrom(redisSession)))其中一個(gè)為true的情況下保存數(shù)據(jù)到reids中;
3.1重點(diǎn)看一下forceSave,可以理解forceSave就是內(nèi)置保存策略的一個(gè)標(biāo)識(shí),提供了三種內(nèi)置保存策略:DEFAULT,SAVE_ON_CHANGE,ALWAYS_SAVE_AFTER_REQUEST
- DEFAULT:默認(rèn)保存策略,依賴其他四種情況保存session,
- SAVE_ON_CHANGE:每次session.setAttribute()、session.removeAttribute()觸發(fā)都會(huì)保存,
- ALWAYS_SAVE_AFTER_REQUEST:每一個(gè)request請(qǐng)求后都強(qiáng)制保存,無(wú)論是否檢測(cè)到變化;
3.2redisSession.isDirty()檢測(cè)session內(nèi)部是否有臟數(shù)據(jù)
public Boolean isDirty() {
return Boolean.valueOf(this.dirty.booleanValue() || !this.changedAttributes.isEmpty());
}
每一個(gè)request請(qǐng)求后檢測(cè)是否有臟數(shù)據(jù),有臟數(shù)據(jù)才保存,實(shí)時(shí)性沒有SAVE_ON_CHANGE高,但是也沒有ALWAYS_SAVE_AFTER_REQUEST來(lái)的粗暴;
3.3后面三種情況都是用來(lái)檢測(cè)三個(gè)ThreadLocal變量;
4.何時(shí)被移除
上一節(jié)中介紹了Tomcat內(nèi)置看定期檢測(cè)session是否過(guò)期,ManagerBase中提供了processExpires方法來(lái)處理session過(guò)去的問題,但是在RedisSessionManager重寫了此方法
public void processExpires() {
}
直接不做處理了,具體是利用了redis的設(shè)置生存時(shí)間功能,具體在saveInternal方法中:
jedis.expire(binaryId, getMaxInactiveInterval());
總結(jié)
本文大致分析了Tomcat Session管理器,以及tomcat-redis-session-manager是如何進(jìn)行session集中式管理的,但是此工具完全依賴tomcat容器,如果想完全獨(dú)立于應(yīng)用服務(wù)器的方案,
Spring session是一個(gè)不錯(cuò)的選擇。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。