2014年8月12日星期二

Session机制和分布式Session

什么是session

    Session是指一个终端用户与交互系统进行通信的时间间隔,通常指从注册进入系统到注销退出系统之间所经过的时间。以及如果需要的话,可能还有一定的操作空间。
    在计算机系统中,Session又被称作“会话”。
    一个Session的概念需要包括特定的客户端,特定的服务器端以及不中断的操作时间。

什么是Cookie

    Cookie最早是网景公司的前雇员Lou Montulli在1993年3月的发明。
    Cookie是由服务器端生成,发送给User-Agent(一般是浏览器),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie)。Cookie名称和值可以由服务器端开发自己定义,对于JSP而言也可以直接写入jsessionid,这样服务器可以知道该用户是否合法用户以及是否需要重新登录等,服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器会话中的状态。

session原理

session 交互流程图
  1. 客户端请求服务器端,服务器会分配给客户端一个sessionID,这个sessionID会以cookie的形式保存在客户端浏览器中;
  2. 客户端此后的请求都会附带这个cookie请求服务器端接口和数据,当客户端请求登录时,服务器会校验用户信息,如果登录成功会更新本地的session数据,并返回客户端登录成功;
  3. 此后客户端就可以访问那些登录受限页面,获取用户数据,每次请求时,服务器端都会判断session是否过期,一旦过期,则返回错误页面,否则返回用户需要的数据;
  4. 当客户端cookie过期或服务器端session过期后,用户的session也就结束了。

分布式session

    由于最简单的session是以jvm为存储介质的,也就是一台JVM中存储了在这个系统访问的用户session信息,当处于分布式环境下,例如多个JVM提供服务时,这些session是无法共享数据的,此时就涉及了分布式session的概念。
    原理就是将session存储到一个公共的服务中,每个JVM都同时存取这些公共数据,保证数据可以互通;
    实现的方案有很多,例如以分布式缓存系统存储,例如redis,memcached,mongoDB等,另外一个思路是使用zookeeper存储sessionID信息,以Zookeeper的强一致性保证数据的唯一性和可用性;
    下面介绍一种非侵入式的分布式缓存系统,原理是重写tomcat的底层session,将session存储到redis中。这种方式的优点是上层业务逻辑代码可以不变,只需要配置tomcat即可达到目标;
    这是一个github上的开源项目:https://github.com/jcoleman/tomcat-redis-session-manager
    下载它对应tomcat7,jdk7的包,还有jedis2.0.0版本的jar包以及apache commons组件中的commons-pool-1.4.jar,将这三个jar拷贝到tomcat的lib目录。
    编辑conf目录下的context.xml文件,加入以下配置:
<Valve className="com.radiadesign.catalina.session.RedisSessionHandlerValve" />
<Manager className="com.radiadesign.catalina.session.RedisSessionManager"
         host=“192.168.10.11" 
         port="6379" 
         database="0" 
         maxInactiveInterval="60" />
    其中,host是redis的主机地址,port是redis服务的端口,database是redis的db名称,一般是0-15,默认redis有16个db。maxInactiveInterval是session过期时间,如果在代码层设置了过期时间,那么这个值会被覆盖,这个值的单位是秒;
    设置之后重启tomcat,通过redis-cli接入redis,查看keys:
keys *
可以通过这个命令查看redis的基本信息:
info
注:源码有一个bug,请查看RedisSessionManager的这段代码:
try{
   jedis = acquireConnection();
   // Ensure generation of a unique session identifier.
   do {
      if (null == sessionId) {
          sessionId = generateSessionId();
      }
      if (jvmRoute != null) {
         sessionId += '.' + jvmRoute;
      }
   } while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 1L); // 1 = key set; 0 = key already existed
请注意看jvmRoute这段,如果在tomcat中配置了这个参数,那么这段代码会进入死循环。假如jvmRoute不为空,那么sessionId每次循环都会变更,导致while条件一直为true,一旦触发,会导致redis的内存以非常快速的速度增长。
改动就是把代码if(jvmRoute !=null)挪到if(null == sessionId)内;
另外,这段代码是为了避免分布式sessionID重现重复而加的,当不考虑jvmRoute时,一旦生成了redis中已存在的sessionId, 那么setnx会返回0,导致while结束;但是一旦生成了重复的sessionId也会导致循环结束,因此最后的while循环也会导致sessionId重复的问题;
所以考虑修改这段代码为:
try{
    jedis = acquireConnection();
    // Ensure generation of a unique session identifier.
    if (null == sessionId) {
        sessionId = generateSessionId();
    }
    while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L){//contains key, re-generate
        sessionId = generateSessionId();
    }
}catch(Exception e){}

Tomcat容器如何管理session的

一般在代码中,我们使用下面这个方法获取session:
HttpSession session = request.getSession();
request这里类型一般是javax.servlet.http.HttpServletRequest,这是一个接口,扩展了ServletRequest接口,
public interface HttpServletRequest extends ServletRequest{
    ...
     /**
     *
     * Returns the current session associated with this request,
     * or if the request does not have a session, creates one.
     * 
     * @return  the HttpSession associated
     *   with this request
     *
     * @see #getSession(boolean)
     *
     */
    public HttpSession getSession();
}
接口说明:返回当前request相关的session对象,如果当前request没有session,则创建一个。
调用这个接口时,实际的实现类就到了tomcat容器org.apache.catalina.connector.RequestFacade中,这是一个门面封装类,它中间有实际getSession的实现方法:
@Override
    public HttpSession getSession(boolean create) {

        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }

        if (SecurityUtil.isPackageProtectionEnabled()){
            return AccessController.
                doPrivileged(new GetSessionPrivilegedAction(create));
        } else {
            return request.getSession(create);
        }
    }
根据这个方法的实现,最终是通过org.apache.catalina.connector.Request类中的getSession方法获取或创建当前session对象;
/**
     * 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) {
        Session session = doGetSession(create);
        if (session == null) {
            return null;
        }

        return session.getSession();
    }
Session在doGetSession中创建,
protected Session doGetSession(boolean create) {

        // 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
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        if (session != null) {
            return (session);
        }

        // Return the requested session if it exists and is valid
        Manager manager = null;
        if (context != null) {
            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 ((context != null) && (response != null) &&
            context.getServletContext().getEffectiveSessionTrackingModes().
                    contains(SessionTrackingMode.COOKIE) &&
            response.getResponse().isCommitted()) {
            throw new IllegalStateException
              (sm.getString("coyoteRequest.sessionCreateCommitted"));
        }

        // Attempt to reuse session id if one was submitted in a cookie
        // Do not reuse the session id if it is from a URL, to prevent possible
        // phishing attacks
        // Use the SSL session ID if one is present.
        if (("/".equals(context.getSessionCookiePath())
                && isRequestedSessionIdFromCookie()) || requestedSessionSSL ) {
            session = manager.createSession(getRequestedSessionId());
        } else {
            session = manager.createSession(null);
        }

        // 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池中查找session,如果当前request不含有sessionID或没有查到对应的session,则直接创建一个session,通过调用:org.apache.catalina.Manager.createSession()方法实现;
正常的方法实现如下:
/**
     * Construct and return a new session object, based on the default
     * settings specified by this Manager's properties.  The session
     * id specified will be used as the session id.  
     * If a new session cannot be created for any reason, return 
     * <code>null</code>.
     * 
     * @param sessionId The session id which should be used to create the
     *  new session; if <code>null</code>, a new session id will be
     *  generated
     * @exception IllegalStateException if a new session cannot be
     *  instantiated for any reason
     */
    @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(this.maxInactiveInterval);
        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);

    }
创建一个空的session对象,如果没有sessionId,则声称一个id(生成时会滤重,本地有一个Map负责存储所有已生成的sessionId值,一旦重复则重新生成,避免由于重复产生错误;也就是generateSessionId方法(代码本处略);
分布式session需要重写这个方法,判重要根据redis中的数据判断,而不是根据本地的Map;
重写的分布式session中的createSession方法:
@Override
    public Session createSession(String sessionId) {
        RedisSession session = (RedisSession) createEmptySession();

        // Initialize the properties of the new session and return it
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(getMaxInactiveInterval());

        String jvmRoute = getJvmRoute();

        Boolean error = true;
        Jedis jedis = null;

        try {
            jedis = acquireConnection();

            // Ensure generation of a unique session identifier.
            if (null == sessionId) {
                sessionId = generateSessionId();
            }
            while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 0L){//contains key, re-generate
                sessionId = generateSessionId();
            }
      /* Even though the key is set in Redis, we are not going to flag
         the current thread as having had the session persisted since
         the session isn't actually serialized to Redis yet.
         This ensures that the save(session) at the end of the request
         will serialize the session into Redis with 'set' instead of 'setnx'. */

            error = false;

            session.setId(sessionId);
            session.tellNew();

            currentSession.set(session);
            currentSessionId.set(sessionId);
            currentSessionIsPersisted.set(false);
        } finally {
            if (jedis != null) {
                returnConnection(jedis, error);
            }
        }

        return session;
    }
会判断sessionId是否在redis中存在,如果存在则重新生成新的sessionId;
关于session的Attribute,是存储在session对象的Map中,每次获取、更新、删除都操作这个map对象;
关于session的存储,是利用了tomcat的valve责任链模式,将自定义的valve实现类增加到责任链中,通过每次执行invoke来保存数据到redis,代码:
public class RedisSessionHandlerValve extends ValveBase {
  private final Log log = LogFactory.getLog(RedisSessionManager.class);
  private RedisSessionManager manager;

  public void setRedisSessionManager(RedisSessionManager manager) {
    this.manager = manager;
  }

  @Override
  public void invoke(Request request, Response response) throws IOException, ServletException {
    try {
      getNext().invoke(request, response);
    } finally {
      final Session session = request.getSessionInternal(false);
      storeOrRemoveSession(session);
      manager.afterRequest();
    }
  }

  private void storeOrRemoveSession(Session session) {
    try {
      if (session != null) {
        if (session.isValid()) {
          log.trace("Request with session completed, saving session " + session.getId());
          if (session.getSession() != null) {
            log.trace("HTTP Session present, saving " + session.getId());
            manager.save(session);
          } else {
            log.trace("No HTTP Session present, Not saving " + session.getId());
          }
        } else {
          log.trace("HTTP Session has been invalidated, removing :" + session.getId());
          manager.remove(session);
        }
      }
    } catch (Exception e) {
      // Do nothing.
    }
  }
}

invoke会调用本地的storeOrRemoveSession,通过manager中的save方法将session保存更新到redis中。

参考资料

没有评论:

发表评论