您当前的位置:网站首页>碎碎语>Tomcat Session管理分析

Tomcat Session管理分析

2022年11月02日 投稿作者:admin 围观人数:604
Tomcat Session管理分析

在上文Nginx+Tomcat关于Session的管理中简单介绍了如何使用redis来集中管理session,本文首先将介绍默认的管理器是如何管理Session的生命周期的,然后在此基础上对Redis集中式管理Session进行分析。

Tomcat Manager介绍

上文中在Tomcat的context.xml中配置了Session管理器RedisSessionManager,实现了通过redis来存储session的功能;Tomcat本身提供了多种Session管理器,如下类图:

Tomcat Session管理分析 [db:标签] 碎碎语  第1张

1.Manager接口类

定义了用来管理session的基本接口,包括:createSession,findSession,add,remove等对session操作的方法;还有getMaxActive,setMaxActive,getActiveSessions活跃会话的管理;还有Session有效期的接口;以及与Container相关联的接口;

2.ManagerBase抽象类

实现了Manager接口,提供了基本的功能,使用ConcurrentHashMap存放session,提供了对session的create,find,add,remove功能,并且在createSession中了使用类SessionIdGenerator来生成会话id,作为session的唯一标识;

3.ClusterManager接口类

实现了Manager接口,集群session的管理器,Tomcat内置的集群服务器之间的session复制功能;

4.ClusterManagerBase抽象类

继承了ManagerBase抽象类,实现ClusterManager接口类,实现session复制基本功能;

5.PersistentManagerBase抽象类

继承了ManagerBase抽象类,实现了session管理器持久化的基本功能;内部有一个Store存储类,具体实现有:FileStore和JDBCStore;

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类的区别;

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方法:

protectedSessiondoGetSession(booleancreate){//TherecannotbeasessionifnocontexthasbeenassignedyetContextcontext=getContext();if(context==null){return(null);}//Returnthecurrentsessionifitexistsandisvalidif((session!=null)&&!session.isValid()){session=null;}if(session!=null){return(session);}//ReturntherequestedsessionifitexistsandisvalidManagermanager=context.getManager();if(manager==null){returnnull;//Sessionsarenotsupported}if(requestedSessionId!=null){try{session=manager.findSession(requestedSessionId);}catch(IOExceptione){session=null;}if((session!=null)&&!session.isValid()){session=null;}if(session!=null){session.access();return(session);}}//Createanewsessionifrequestedandtheresponseisnotcommittedif(!create){return(null);}if((response!=null)&&context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)&&response.getResponse().isCommitted()){thrownewIllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));}//Re-usesessionIDsprovidedbytheclientinverylimited//circumstances.StringsessionId=getRequestedSessionId();if(requestedSessionSSL){//IfthesessionIDhasbeenobtainedfromtheSSLhandshakethen//useit.}elseif(("/".equals(context.getSessionCookiePath())&&isRequestedSessionIdFromCookie())){/*Thisisthecommon(ish)usecase:usingthesamesessionIDwith*multiplewebapplicationsonthesamehost.Typicallythisis*usedbyPortletimplementations.Itonlyworksifsessionsare*trackedviacookies.Thecookiemusthaveapathof"/"elseit*won'tbeprovidedforrequeststoallwebapplications.**AnysessionIDprovidedbytheclientshouldbeforasession*thatalreadyexistssomewhereonthehost.Checkifthecontext*isconfiguredforthistobeconfirmed.*/if(context.getValidateClientProvidedNewSessionId()){booleanfound=false;for(Containercontainer:getHost().findChildren()){Managerm=((Context)container).getManager();if(m!=null){try{if(m.findSession(sessionId)!=null){found=true;break;}}catch(IOExceptione){//Ignore.Problemswiththismanagerwillbe//handledelsewhere.}}}if(!found){sessionId=null;}}}else{sessionId=null;}session=manager.createSession(sessionId);//Creatinganewsessioncookiebasedonthatsessionif((session!=null)&&(getContext()!=null)&&getContext().getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)){Cookiecookie=ApplicationSessionCookieConfig.createSessionCookie(context,session.getIdInternal(),isSecure());response.addSessionCookieInternal(cookie);}if(session==null){returnnull;}session.access();returnsession;}

如果session已经存在,则直接返回;如果不存在则判定requestedSessionId是否为空,如果不为空则通过requestedSessionId到Session manager中获取session,如果为空,并且不是创建session操作,直接返回null;否则会调用Session manager创建一个新的session;

关于requestedSessionId是如何获取的,Tomcat内部可以支持从cookie和url中获取,具体可以查看CoyoteAdapter类的postParseRequest方法部分代码:

StringsessionID;if(request.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.URL)){//GetthesessionIDiftherewasonesessionID=request.getPathParameter(SessionConfig.getSessionUriParamName(request.getContext()));if(sessionID!=null){request.setRequestedSessionId(sessionID);request.setRequestedSessionURL(true);}}//LookforsessionIDincookiesandSSLsessionparseSessionCookiesId(req,request);

可以发现首先去url解析sessionId,如果获取不到则去cookie中获取,此处的SessionUriParamName=jsessionid;在cookie被浏览器禁用的情况下,我们可以看到url后面跟着参数jsessionid=xxxxxx;下面看一下parseSessionCookiesId方法:

StringsessionCookieName=SessionConfig.getSessionCookieName(context);for(inti=0;i=0)&&(getActiveSessions()>=maxActiveSessions)){rejectedSessions++;thrownewTooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"),maxActiveSessions);}//RecycleorcreateaSessioninstanceSessionsession=createEmptySession();//Initializethepropertiesofthenewsessionandreturnitsession.setNew(true);session.setValid(true);session.setCreationTime(System.currentTimeMillis());session.setMaxInactiveInterval(((Context)getContainer()).getSessionTimeout()*60);Stringid=sessionId;if(id==null){id=generateSessionId();}session.setId(id);sessionCounter++;SessionTimingtiming=newSessionTiming(session.getCreationTime(),0);synchronized(sessionCreationTiming){sessionCreationTiming.add(timing);sessionCreationTiming.poll();}return(session);}

如果传的sessionId为空,tomcat会生成一个唯一的sessionId,具体可以参考类StandardSessionIdGenerator的generateSessionId方法;这里发现创建完session之后并没有把session放入ConcurrentHashMap中,其实在session.setId(id)中处理了,具体代码如下:

publicvoidsetId(Stringid,booleannotify){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:

protectedclassContainerBackgroundProcessorimplementsRunnable{@Overridepublicvoidrun(){Throwablet=null;StringunexpectedDeathMessage=sm.getString("containerBase.backgroundProcess.unexpectedThreadDeath",Thread.currentThread().getName());try{while(!threadDone){try{Thread.sleep(backgroundProcessorDelay*1000L);}catch(InterruptedExceptione){//Ignore}if(!threadDone){Containerparent=(Container)getMappingObject();ClassLoadercl=Thread.currentThread().getContextClassLoader();if(parent.getLoader()!=null){cl=parent.getLoader().getClassLoader();}processChildren(parent,cl);}}}catch(RuntimeExceptione){t=e;throwe;}catch(Errore){t=e;throwe;}finally{if(!threadDone){log.error(unexpectedDeathMessage,t);}}}protectedvoidprocessChildren(Containercontainer,ClassLoadercl){try{if(container.getLoader()!=null){Thread.currentThread().setContextClassLoader(container.getLoader().getClassLoader());}container.backgroundProcess();}catch(Throwablet){ExceptionUtils.handleThrowable(t);log.error("Exceptioninvokingperiodicoperation:",t);}finally{Thread.currentThread().setContextClassLoader(cl);}Container[]children=container.findChildren();for(inti=0;i=maxInactiveInterval){expire(true);}}returnthis.isValid;}

主要是通过对比当前时间到上次活跃的时间是否超过了maxInactiveInterval,如果超过了就做expire处理;

Redis集中式管理Session分析

在上文中使用tomcat-redis-session-manager来管理session,下面来分析一下是如果通过redis来集中式管理Session的;围绕session如何获取,如何创建,何时更新到redis,以及何时被移除;

1.如何获取

RedisSessionManager重写了findSession方法

publicSessionfindSession(Stringid)throwsIOException{RedisSessionsession=null;if(null==id){currentSessionIsPersisted.set(false);currentSession.set(null);currentSessionSerializationMetadata.set(null);currentSessionId.set(null);}elseif(id.equals(currentSessionId.get())){session=currentSession.get();}else{byte[]data=loadSessionDataFromRedis(id);if(data!=null){DeserializedSessionContainercontainer=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类中:

publicvoiddeserializeInto(byte[]data,RedisSessionsession,SessionSerializationMetadatametadata)throwsIOException,ClassNotFoundException{BufferedInputStreambis=newBufferedInputStream(newByteArrayInputStream(data));Throwablearg4=null;try{CustomObjectInputStreamx2=newCustomObjectInputStream(bis,this.loader);Throwablearg6=null;try{SessionSerializationMetadatax21=(SessionSerializationMetadata)x2.readObject();metadata.copyFieldsFrom(x21);session.readObjectData(x2);}catch(Throwablearg29){......}

二进制数据中保存了2个对象,分别是SessionSerializationMetadata和RedisSession,SessionSerializationMetadata里面保存的是Session中的attributes信息,RedisSession其实也有attributes数据,相当于这份数据保存了2份;

2.如何创建

同样RedisSessionManager重写了createSession方法,2个重要的点分别:sessionId的唯一性问题和session保存到redis中;

//Ensuregenerationofauniquesessionidentifier.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=keyset;0=keyalreadyexisted}

分布式环境下有可能出现生成的sessionId相同的情况,所以需要确保唯一性;保存session到redis中是最核心的一个方法,何时更新,何时过期都在此方法中处理;

3.何时更新到redis

具体看saveInternal方法

protectedbooleansaveInternal(Jedisjedis,Sessionsession,booleanforceSave)throwsIOException{Booleanerror=true;try{log.trace("Savingsession"+session+"intoRedis");RedisSessionredisSession=(RedisSession)session;if(log.isTraceEnabled()){log.trace("SessionContents["+redisSession.getId()+"]:");Enumerationen=redisSession.getAttributeNames();while(en.hasMoreElements()){log.trace(""+en.nextElement());}}byte[]binaryId=redisSession.getId().getBytes();BooleanisCurrentSessionPersisted;SessionSerializationMetadatasessionSerializationMetadata=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("Savewasdeterminedtobenecessary");if(null==sessionAttributesHash){sessionAttributesHash=serializer.attributesHashFrom(redisSession);}SessionSerializationMetadataupdatedSerializationMetadata=newSessionSerializationMetadata();updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash);jedis.set(binaryId,serializer.serializeFrom(redisSession,updatedSerializationMetadata));redisSession.resetDirtyTracking();currentSessionSerializationMetadata.set(updatedSerializationMetadata);currentSessionIsPersisted.set(true);}else{log.trace("Savewasdeterminedtobeunnecessary");}log.trace("Settingexpiretimeoutonsession["+redisSession.getId()+"]to"+getMaxInactiveInterval());jedis.expire(binaryId,getMaxInactiveInterval());error=false;returnerror;}catch(IOExceptione){log.error(e.getMessage());throwe;}finally{returnerror;}}

以上方法中大致有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内部是否有脏数据

publicBooleanisDirty(){returnBoolean.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是一个不错的选择。

标签

Tomcat Session管理分析
版权说明
免责声明:本文文章内容由技术导航发布,但不代表本站的观点和立场,具体内容可自行甄别.