Spring Session動作原理


本文はvivoインターネット技術微信公衆番号に先発した.https://mp.weixin.qq.com/s/KCOFv0nRuymkX79-RZi9eg
作者:張正林
HTTPプロトコル自体は無状態であり,セッション情報を保存するためにブラウザCookieがセッション要求をSessionIDで識別し,サーバはSessionIDをkeyとしてセッション情報を格納する.単一インスタンスアプリケーションでは、アプリケーションプロセス自体のストレージが考慮され、アプリケーションのボリュームが増加するにつれて横方向の拡張が必要となり、マルチインスタンスsession共有の問題が伴う.
Spring Sessionはマルチプロセスsession共有の問題を解決するために,Spring Sessionの使用方法とSpring Sessionの動作原理について説明する.
1、導入背景
アプリケーションの配置がTomcatにある場合、sessionはTomcatメモリによって維持され、アプリケーションが複数のインスタンスを配置する場合、sessionは共有できません.Spring Sessionは,分散シーンにおけるsession共有の問題を解決するためである.
2、使い方
Spring SessionはHazelcast、Redis、MongoDB、リレーショナル・データベースに格納することをサポートし、本稿では主にsessionがRedisに格納されることを議論する.
web.xml構成:

  
    springSessionRepositoryFilter
    org.springframework.web.filter.DelegatingFilterProxy
  
  
    springSessionRepositoryFilter
    /*
  

Spring主な構成:

    
        
        p:poolConfig-ref="jedisPoolConfig"
    
 
    
    
        
        
    

3、ワークフロー
Tomcat web.xml解析手順:
contextInitialized(ServletContextEvent arg0); // Listener
init(FilterConfig filterConfig); // Filter
init(ServletConfig config); // Servlet

初期化順序:Listener>Filter>サーブレット.
1)TomcatのlistenerでSessionRepositoryFilterをSpringコンテナにロードします.
前のセクションSpringプロファイルでは、親SpringHttpSessionConfigurationでSessionRepositoryFilterが生成されたRedisHttpSessionConfigurationが宣言されています.

@Bean
public  SessionRepositoryFilter extends ExpiringSession> springSessionRepositoryFilter(
        SessionRepository sessionRepository) {
    ......
    return sessionRepositoryFilter;
}

RedisHttpSessionConfigurationクラス継承関係
2)filter初期化
web.xmlに配置されているfilterはDelegatingFilterProxyです.
DelegatingFilterProxyクラス継承関係
DelegatingFilterProxy初期化エントリは、親GenericFilterBeanにあります.
public final void init(FilterConfig filterConfig) throws ServletException {
        ......
        // Let subclasses do whatever initialization they like.
        initFilterBean();
        ......
    }

DelegatingFilterProxy Springコンテナに行って第1ステップ初期化されたspringSessionRepositoryFilterを取ります.
protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // If no target bean name specified, use filter name.
                if (this.targetBeanName == null) {
                    //targetBeanName  springSessionRepositoryFilter
                    this.targetBeanName = getFilterName();
                }
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }

これでsessionRepositoryFilterの初期化が完了し、DelegatingFilterProxyは実際にSessionRepositoryFilterをエージェントしました.
SessionRepositoryFilterワークコアプロセス:
protected void doFilterInternal(HttpServletRequest request,
           HttpServletResponse response, FilterChain filterChain)
           throws ServletException, IOException {
       request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
       //   HttpServletRequest,   HttpServletRequest  getSession(boolean create)  
       SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
               request, response, this.servletContext);
             ......
       try {
           filterChain.doFilter(strategyRequest, strategyResponse);
       }
       finally {
           //  session   
           wrappedRequest.commitSession();
       }
   }

4、キャッシュメカニズム
セッションごとに、Redisが実際にキャッシュしたデータは次のとおりです.
spring:session:sessions:1b8b2340-da25-4ca6-864c-4af28f033327
spring:session:sessions:expires:1b8b2340-da25-4ca6-864c-4af28f033327
spring:session:expirations:1557389100000
Spring:session:sessionsはhash構造で、Spring Sessionの主な内容を格納します.
hgetall spring:session:sessions:1b8b2340-da25-4ca6-864c-4af28f033327
 1) "creationTime"
 2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01j\x9b\x83\x9d\xfd"
 3) "maxInactiveInterval"
 4) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
 5) "lastAccessedTime"
 6) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01j\x9b\x83\x9d\xfd"

Spring:session:sessions:expiresはstring構造であり、空の値を格納します.
Spring:session:expirationsはset構造で、1557389100000時点で期限切れのspring:session:sessions:expiresキー値を格納します.
smembers spring:session:expirations:1557389100000
1) "\xac\xed\x00\x05t\x00,expires:1b8b2340-da25-4ca6-864c-4af28f033327"

RedisSessionExpirationPolicy、3つのキー値生成プロセス:
public void onExpirationUpdated(Long originalExpirationTimeInMilli,
            ExpiringSession session) {
        String keyToExpire = "expires:" + session.getId();
        long toExpire = roundUpToNextMinute(expiresInMillis(session));
                ......
        // spring:session:sessions:expires   spring:session:expirations   key  
        String expireKey = getExpirationKey(toExpire);
        BoundSetOperations expireOperations = this.redis
                .boundSetOps(expireKey);
        expireOperations.add(keyToExpire);
 
        long fiveMinutesAfterExpires = sessionExpireInSeconds
                + TimeUnit.MINUTES.toSeconds(5);
        //spring:session:expirations   key     xml         
        expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
        if (sessionExpireInSeconds == 0) {
            this.redis.delete(sessionKey);
        }
        else {
            //spring:session:sessions:expires   key     xml     
            this.redis.boundValueOps(sessionKey).append("");
            this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds,
                    TimeUnit.SECONDS);
        }
        //spring:session:sessions   key     xml         
        this.redis.boundHashOps(getSessionKey(session.getId()))
                .expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
    }

Redis期限切れキーには、タイミング削除、不活性削除、定期削除の3つの削除ポリシーがあります.
  • タイミング削除:タイマーをメンテナンスすることで、期限が切れたらすぐに削除するのが最も有効ですが、cpu時間を最も浪費します.
  • 不活性削除:プログラムはキーを取り出したときに期限が切れたかどうかを判断し、期限が切れてから削除する.この方法はcpu時間に友好的で、メモリに友好的ではない.
  • 定期削除:一定時間ごとに期限切れキーを削除する操作を実行し、削除するたびの実行時間と頻度を制限する折衷です.

  • Redisは、不活性な削除と定期的な削除のポリシーを採用しています.このことから,Redisに依存する期限切れポリシーは,期限切れkeyをリアルタイムで削除することは信頼できないことが分かる.
    一方、Spring Sessionが期限切れになった後にビジネスロジック処理を行う可能性があります.また、sessionの中の情報が必要です.spring:session:sessionsキー値が1つしかないと、Redis削除は削除され、ビジネスはsession情報を取得できません.
    Spring:session:expirationsキーにはspring:session:sessions:expiresキーが格納されていますが、spring:session:sessions:expiresキーの有効期限はspring:session:expirationsキーとspring:sessionsキー(実際のSpring Sessionが有効期限切れイベント処理に購読するspring:session:sessions:expiresキーより5分早く、次のセクションで具体的に説明します)です.これにより、期限切れのイベントに購読するとspring:session:sessionsキー値も取得できます.
    Redis自体のクリーンアップメカニズムでspring:session:sessions:expiresがタイムリーにクリアされていない場合は、spring Sessionが提供するタイミングタスクポケットを通じてspring:session:sessions:expiresクリアを保証できます.
    RedisSessionExpirationPolicy,sessionスケジュールタスクのクリーンアップ
    public void cleanExpiredSessions() {
            long now = System.currentTimeMillis();
            long prevMin = roundDownMinute(now);
            ......
            //   spring:session:expirations 
            String expirationKey = getExpirationKey(prevMin);
            //               session
            Set sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
            //   :      spring:session:expirations ,     session   !
            this.redis.delete(expirationKey);
            for (Object session : sessionsToExpire) {
                String sessionKey = getSessionKey((String) session);
                //    spring:session:sessions:expires 
                touch(sessionKey);
            }
        }
     
        /**
         * By trying to access the session we only trigger a deletion if it the TTL is
         * expired. This is done to handle
         * https://github.com/spring-projects/spring-session/issues/93
         *
         * @param key the key
         */
        private void touch(String key) {
            //        key,      key,        spring:session:sessions:expires     ,
            //                 ,key     spring:session:expirations    ,
            // spring:session:sessions:expires   ttl    
            this.redis.hasKey(key);
        }

    5、イベント購読
    デフォルトでは、少なくともサブスクリプション・キースペース通知gxEイベント(http://redisdoc.com/topic/notification.html).
    コンフィギュレーション
    public void configure(RedisConnection connection) {
           String notifyOptions = getNotifyOptions(connection);
           String customizedNotifyOptions = notifyOptions;
           if (!customizedNotifyOptions.contains("E")) {
               customizedNotifyOptions += "E";
           }
           boolean A = customizedNotifyOptions.contains("A");
           if (!(A || customizedNotifyOptions.contains("g"))) {
               customizedNotifyOptions += "g";
           }
           if (!(A || customizedNotifyOptions.contains("x"))) {
               customizedNotifyOptions += "x";
           }
           if (!notifyOptions.equals(customizedNotifyOptions)) {
               connection.setConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS, customizedNotifyOptions);
           }
       }
    

    RedisHttpSessionConfiguration、リスニングイベントの登録:
    @Bean
     public RedisMessageListenerContainer redisMessageListenerContainer(
             RedisConnectionFactory connectionFactory,
             RedisOperationsSessionRepository messageListener) {
                 ......
         //psubscribe del expired  
         container.addMessageListener(messageListener,
                 Arrays.asList(new PatternTopic("__keyevent@*:del"),
                         new PatternTopic("__keyevent@*:expired")));
         //psubscribe created  
         container.addMessageListener(messageListener, Arrays.asList(new PatternTopic(
                 messageListener.getSessionCreatedChannelPrefix() + "*")));
         return container;
     }
    

    RedisOperationsSessionRepository、イベント処理:
    public void onMessage(Message message, byte[] pattern) {
          ......
          if (channel.startsWith(getSessionCreatedChannelPrefix())) {
              ...
              //  spring:session created  
              handleCreated(loaded, channel);
              return;
          }
     
          // spring:session:sessions:expires      
          String body = new String(messageBody);
          if (!body.startsWith(getExpiredKeyPrefix())) {
              return;
          }
     
          boolean isDeleted = channel.endsWith(":del");
          if (isDeleted || channel.endsWith(":expired")) {
              ......
              if (isDeleted) {
                  //  spring:session:sessions:expires del  
                  handleDeleted(sessionId, session);
              }
              else {
                  //  spring:session:sessions:expires expired  
                  handleExpired(sessionId, session);
              }
              ......
              return;
          }
      }
    

    イベント購読サンプル:
    @Component
    public class SessionExpiredListener implements ApplicationListener {
        @Override
        public void onApplicationEvent(SessionExpiredEvent event) {
            ......
        }
    }
    

    6、まとめ
    Spring Sessionは私たちに良い分布式環境の下で資源共有問題の解決構想を提供して、それはサーブレット規範に基づいて実現して、業務の使用時に簡単な配置だけでsession共有を実現することができて、業務と低い結合を実現して、これはすべて後で私たちのプロジェクト開発の中で署名することができる設計理念です.
    詳細はvivoインターネット技術微信公衆番号に注目してください
    注:転載記事はまずマイクロ信号:labs 2020に連絡してください.