session

最近在工作共使用session,却没有做session的一致性踩了一个坑,导致线上环境只能单节点跑,今天来复盘一下。

session与cookie得差异:

session存于服务器,cookie存于客户端,session相比cookie更加安全,容量更加大。当然session消耗服务器资源,cookie存储于客户端与服务器压力更小。session一次会话有效,cookie具体时间可以由你来定

tomcat中的session

​ 浏览器向服务器发送请求时会带有一个sessionId,tomcat通过这个sessionId进行判断此浏览器以获得存于服务器中的session。

tomcat中实现是javax.servle.http的类或接口有Request.class,ApplicationHttpRequest.Class....,

在servlet中获取session,通常是调用request的getSession方法。这个方法需要传入一个boolean参数,这个参数就是实现刚才说的,当jsessionid为空或从session池中获取不到相应的session对象时,选择创建一个新的session还是不创建。

Request.class的核心代码逻辑;

protected Session doGetSession(boolean create) {  
  
        ……  
        // 先获取所在context的manager对象  
        Manager manager = null;  
        if (context != null)  
            manager = context.getManager();  
        if (manager == null)  
            return (null);      // Sessions are not supported  
          
        //这个requestedSessionId就是从Http request中解析出来的  
        if (requestedSessionId != null) {  
            try {  
                //manager管理的session池中找相应的session对象  
                session = manager.findSession(requestedSessionId);  
            } catch (IOException e) {  
                session = null;  
            }  
            //判断session是否为空及是否过期超时  
            if ((session != null) && !session.isValid())  
                session = null;  
            if (session != null) {  
                //session对象有效,记录此次访问时间  
                session.access();  
                return (session);  
            }  
        }  
  
        // 如果参数是false,则不创建新session对象了,直接退出了  
        if (!create)  
            return (null);  
        if ((context != null) && (response != null) &&  
            context.getCookies() &&  
            response.getResponse().isCommitted()) {  
            throw new IllegalStateException  
              (sm.getString("coyoteRequest.sessionCreateCommitted"));  
        }  
  
        // 开始创建新session对象  
        if (connector.getEmptySessionPath()   
                && isRequestedSessionIdFromCookie()) {  
            session = manager.createSession(getRequestedSessionId());  
        } else {  
            session = manager.createSession(null);  
        }  
  
        // 将新session的jsessionid写入cookie,传给browser  
        if ((session != null) && (getContext() != null)  
               && getContext().getCookies()) {  
            Cookie cookie = new Cookie(Globals.SESSION_COOKIE_NAME,  
                                       session.getIdInternal());  
            configureSessionCookie(cookie);  
            response.addCookieInternal(cookie);  
        }  
        //记录session最新访问时间  
        if (session != null) {  
            session.access();  
            return (session);  
        } else {  
            return (null);  
        }  
    }  

以下是,ApplicationHttpRequest的getSession源码

 /**
     * Return the session associated with this Request, creating one
     * if necessary and requested.
     *
     * @param create Create a new session if one does not exist
     */
    @Override
    public HttpSession getSession(boolean create) {

        if (crossContext) {

            // There cannot be a session if no context has been assigned yet
            if (context == null)
                return (null);

            // Return the current session if it exists and is valid
            // 如果存在session则直接返回
            if (session != null && session.isValid()) {
                return (session.getSession());
            }

            HttpSession other = super.getSession(false);
            if (create && (other == null)) {
                // First create a session in the first context: the problem is
                // that the top level request is the only one which can
                // create the cookie safely
                other = super.getSession(true);
            }
            if (other != null) {
                Session localSession = null;
                try {
                    localSession =
                        context.getManager().findSession(other.getId());
                    if (localSession != null && !localSession.isValid()) {
                        localSession = null;
                    }
                } catch (IOException e) {
                    // Ignore
                }
                if (localSession == null && create) {
                    localSession =
                        context.getManager().createSession(other.getId());
                }
                if (localSession != null) {
                    // 更新访问时间
                    localSession.access();
                    session = localSession;
                    return session.getSession();
                }
            }
            return null;

        } else {
            return super.getSession(create);
        }

    }

StandardSession

从ApplicationHttpRequest中可以追溯到StandardSession,下面是StandardSession的部分重要源码,我摘抄下来方便大家理解,你可以看到他封装成了一个StandardSessionFacade,提供方法给外部使用,而且里面的attributes是一个concurrentHashMap。

    
    /**
     * The collection of user data attributes associated with this Session.
     */
    protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();	


	/**
     * Set the session identifier for this session.
     * 给这个session建立一个唯一的标志
     * @param id The new session identifier
     */
    @Override
    public void setId(String id) {
        setId(id, true);
    }


	/**
     * Return the <code>HttpSession</code> for which this object
     * is the facade.
     */
    @Override
    public HttpSession getSession() {

        if (facade == null){
            if (SecurityUtil.isPackageProtectionEnabled()){
                final StandardSession fsession = this;
                facade = AccessController.doPrivileged(
                        new PrivilegedAction<StandardSessionFacade>(){
                    @Override
                    public StandardSessionFacade run(){
                        return new StandardSessionFacade(fsession);
                    }
                });
            } else {
                facade = new StandardSessionFacade(this);
            }
        }
        return (facade);

    }

sessionId

setId(String id)

StandardSession 中的 setId(String id)) 方法时提供给制造session者创造唯一sessionId标志,用过调用链,可以追溯到

ManagerBase类,一下为ManagerBase的部分源码

 @Override
    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(getContext().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);

    }

查看其generateSessionId();下面使其生成的sessionId的源码,通过sessionIdGenerator.generateSessionId();生成sessionId,sessionIdGenerator是一个接口

      /**
     * The set of currently active Sessions for this Manager, keyed by
     * session identifier.
     */
    protected Map<String, Session> sessions = new ConcurrentHashMap<>();
  
 	 /**
     * Generate and return a new session identifier.
     * @return a new session id
     */
    protected String generateSessionId() {

        String result = null;

        do {
            if (result != null) {
                // Not thread-safe but if one of multiple increments is lost
                // that is not a big deal since the fact that there was any
                // duplicate is a much bigger issue.
                duplicates++;
            }

            result = sessionIdGenerator.generateSessionId();

        } while (sessions.containsKey(result));

        return result;
    }

session过期

​ 在核心源码中doGetSession中我们可以看到access()方法,其中会更新我们的session登录时间。StandardSession.Class中我们能看到 isValid()方法,用于判断此session是否过期

/**
     * Return the <code>isValid</code> flag for this session.
     */
    @Override
    public boolean isValid() {

        if (!this.isValid) {
            return false;
        }

        if (this.expiring) {
            return true;
        }

		// accessCount,在每次access的时候都是自增,endAccess时自减
        if (ACTIVITY_CHECK && accessCount.get() > 0) {
            return true;
        }

        //这就是判断距离上次访问是否超时的过程  
        if (maxInactiveInterval > 0) {
            // 判断上一次访问时间
            int timeIdle = (int) (getIdleTimeInternal() / 1000L);
            if (timeIdle >= maxInactiveInterval) {
                expire(true);
            }
        }

        return this.isValid;
    }

    /**
     * Return the idle time from last client access time without invalidation check
     * @see #getIdleTime()
     */
    @Override
    public long getIdleTimeInternal() {
        long timeNow = System.currentTimeMillis();
        long timeIdle;
        if (LAST_ACCESS_AT_START) {
            timeIdle = timeNow - lastAccessedTime;
        } else {
            timeIdle = timeNow - thisAccessedTime;
        }
        return timeIdle;
    }

上面源码中可以看到超过了时间后太会调用expire方法,粘贴重要源码

public void expire(boolean notify) {   
  
        synchronized (this) {  
            ......  
            //设立标志位  
            setValid(false);  
  
            //计算一些统计值,例如此manager下所有session平均存活时间等  
            long timeNow = System.currentTimeMillis();  
            int timeAlive = (int) ((timeNow - creationTime)/1000);  
            synchronized (manager) {  
                if (timeAlive > manager.getSessionMaxAliveTime()) {  
                    manager.setSessionMaxAliveTime(timeAlive);  
                }  
                int numExpired = manager.getExpiredSessions();  
                numExpired++;  
                manager.setExpiredSessions(numExpired);  
                int average = manager.getSessionAverageAliveTime();  
                average = ((average * (numExpired-1)) + timeAlive)/numExpired;  
                manager.setSessionAverageAliveTime(average);  
            }  
  
            // 将此session从manager对象的session池中删除  
            manager.remove(this);  
            ......  
        }  
    }