浅谈Tomcat Session管理分析
前言
在上文nginx+tomcat关于session的管理中简单介绍了如何使用redis来集中管理session,本文首先将介绍默认的管理器是如何管理session的生命周期的,然后在此基础上对redis集中式管理session进行分析。
tomcat manager介绍
上文中在tomcat的context.xml中配置了session管理器redissessionmanager,实现了通过redis来存储session的功能;tomcat本身提供了多种session管理器,如下类图:
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方法:
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分析
在上文中使用来管理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是一个不错的选择。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
Shiro权限管理框架(四):深入分析Shiro中的Session管理
-
浅谈c++资源管理以及对[STL]智能指针auto_ptr源码分析,左值与右值
-
Tomcat中的session是如何管理的?
-
Tomcat源码分析 (十)----- 彻底理解 Session机制
-
Nginx+Tomcat关于Session的管理的实现
-
浅谈Tomcat Session管理分析
-
Tomcat 是如何管理Session的方法示例
-
Shiro权限管理框架(四):深入分析Shiro中的Session管理
-
Unbuntu server1504 Nginx18 + tomcat7集群+redis3 Session共享管理配置
-
tomcat 配置用memcache 管理tomcat的session