Tomcat多实例Session共享的原理

在这个互联网高度发达的时代,许多应用的用户动辄成百上千万,甚至上亿。为了支持海量用户的访问,应用服务器集群这种水平扩展的方式是最常用的。这种情形下,就会涉及到许多单机环境下完全不需要考虑的问题,这其中session的创建、共享和存储是最常见之一。

在单机环境中,Session的创建和存储都是由同一个应用服务器实例来完成,而存储也仅是内存中,最多会在正常的停止服务器的时候,把当前活动的Session钝化到本地,再次启动时重新加载。

而多个实例之间,Session数据是完全隔离的。而为了实现Session的高可用,多实例间数据共享是必然的,下面我们以Redis 的SessionManager实现多Tomcat实例Session共享的配置为例,我们来梳理下一般session共享的流程:

  1. 添加具体要使用的manager的Jar文件及其依赖

  • redis session manager依赖jedis, commons-pool, commons-pool2

  • 对应版本的redis session manager的jar文件

  • 在TOMCAT_HOME/conf/context.xml中增加如下配置

    <Valve className="com.radiadesign.catalina.session.RedisSessionHandlerValve" />
    <Manager className="com.radiadesign.catalina.session.RedisSessionManager"
             host="localhost"   
             port="6379" database="0" 
             maxInactiveInterval="30" />
    

    其中host和port等替换为对应的配置信息

    1. 启动多个Tomcat实例,以自带的examples应用为例进行验证

    2. 访问examples应用的servlets/servlet/SessionExample,

    3. 在页面中添加数据到session中,并查看页面上对应的session信息

    4. 访问另一个实例上相同应用的页面,查看session信息,两者应该是一致的

    5. 使用redis-cli查看redis中存储的对应数据,相应的sessionId对应的数据已经保存了下来

    以上是一个基本的配置过程,而在这些配置与验证的步骤中,第二步是核心逻辑实现。 前面的文章,曾介绍过Tomcat的Valve,在请求处理时,Pipeline中的各个Valve的invoke方法会依次执行。Tomcat的AccessLogValve介绍

    此处的session处理,就是以一个自定义Valve的形式进行的。关于Session的文章,前面也写过几篇,会附在结尾处

    以下是RedisSessionhandlerValve的invoke方法,我们看,主要是在Valve执行后进行Session的存储或移除。

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

    而session的保存和移除又是通过manager执行的。 manager.save(session); manager.remove(session);

    这里,manager就是前面定义的RedisSessionManager。默认单实例情况下,我们使用的都是StandardManager,对比一下两者,标准的Manager对于session的创建和删除,都会调到其父类ManagerBase中相应的方法,

    public void add(Session session) {
    
            sessions.put(session.getIdInternal(), session);
            int size = getActiveSessions();
            if( size > maxActive ) {
                synchronized(maxActiveUpdateLock) {
                    if( size > maxActive ) {
                        maxActive = size;
                    }
                }
            }
        }
        
    public void remove(Session session, boolean update) {   
    if (session.getIdInternal() != null) {
                sessions.remove(session.getIdInternal());
            }
        }   
    

    我们来看,由于其只保存在内存的Map中protected Map<String, Session> sessions = new ConcurrentHashMap<>(),每个Tomcat实例都对于不同的map,多个实例间无法共享数据。

    对应到RedisSessionManager对于session的处理,都是直接操作redis,基本代码是下面这个样:

    public void save(Session session) throws IOException {
        Jedis jedis = null;
        Boolean error = true;
        try {
          RedisSession redisSession = (RedisSession) session;
    
          Boolean sessionIsDirty = redisSession.isDirty();
          redisSession.resetDirtyTracking();
          byte[] binaryId = redisSession.getId().getBytes();
    
          jedis = acquireConnection();
          if (sessionIsDirty || currentSessionIsPersisted.get() != true) {
            jedis.set(binaryId, serializer.serializeFrom(redisSession));
          }
          currentSessionIsPersisted.set(true);
          jedis.expire(binaryId, getMaxInactiveInterval());
        } }
    

    移除时的操作是这样的

    public void remove(Session session, boolean update) {
        Jedis jedis = null;
        Boolean error = true;
    
        log.trace("Removing session ID : " + session.getId());
    
        try {
          jedis = acquireConnection();
          jedis.del(session.getId());
          error = false;
        } finally {
          if (jedis != null) {
            returnConnection(jedis, error);
          }
        }
      }
    

    而此时,多个Tomcat实例都读取相同的Redis,session数据是共享的,其它实例的初始请求过来时,由于会执行findSession的操作,此时会从Redis中加载session,

    public Session findSession(String id) throws IOException {
        RedisSession session;
    
        if (id == null) {
          session = null;
          currentSessionIsPersisted.set(false);
        } else if (id.equals(currentSessionId.get())) {
          session = currentSession.get();
        } else {
          session = loadSessionFromRedis(id); // 看这里,会从redis中load
    
          if (session != null) {
            currentSessionIsPersisted.set(true);
          }
        }
    
        currentSession.set(session);
        currentSessionId.set(id);
    
        return session;
      }
    

    从而可以保证在一个实例被切换后,另外的实例可以继续响应同一个session的请求。

    以上即为Redis实现session共享高可用的一些关键内容。有兴趣的朋友可以看下通过Memcached实现高可用,也是这个原理。顺着这个思路,如果你有将Session存储在其它地方的需求时,完全可以写一个出来,自己动手,丰衣足食。

    总结一下,我们是通过自定义的Valve来实现请求后session的拦截,同时,使用自定义的SessionManager,来满足不同的session创建与存储的需求。而至于是存储在Redis/Memcached中,还是存储在DB中,只是位置的区别。原理,是一致的。



    转载地址:https://zhuanlan.zhihu.com/p/20945322?refer=tomcat

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值