当前位置: 移动技术网 > 网络运营>服务器>tomcat > 浅谈Tomcat Session管理分析

浅谈Tomcat Session管理分析

2019年05月28日  | 移动技术网网络运营  | 我要评论
前言 在上文nginx+tomcat关于session的管理中简单介绍了如何使用redis来集中管理session,本文首先将介绍默认的管理器是如何管理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是一个不错的选择。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网