Как работает весенняя сессия

Spring

Эта статья была впервые опубликована в публичном аккаунте vivo Internet Technology WeChat.Tickets.WeChat.QQ.com/Yes/KC of V0n Ru…
Автор: Чжан Чжэнлинь

Сам протокол HTTP не имеет состояния.Чтобы сохранить информацию о сеансе, файл cookie браузера идентифицирует запрос сеанса через SessionID, а сервер использует SessionID в качестве ключа для хранения информации о сеансе. В одноэкземплярном приложении может быть рассмотрено хранение самого процесса приложения.С ростом объема приложения требуется горизонтальное расширение, и последует проблема мультиэкземплярного разделения сеансов.

Spring Session призван решить проблему совместного использования сеансов несколькими процессами.В этой статье рассказывается, как использовать Spring Session и как работает Spring Session.

1. Представьте фон

Когда приложение развертывается в Tomcat, сеанс поддерживается памятью Tomcat.Если приложение развертывает несколько экземпляров, сеанс нельзя использовать совместно. Spring Session призван решить проблему совместного использования сеансов в распределенных сценариях.

2. Как использовать

Spring Session поддерживает хранение в Hazelcast, Redis, MongoDB и реляционных базах данных.В этой статье в основном обсуждается хранение сеансов в Redis.

Конфигурация web.xml:

<!-- spring session -->
  <filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

Весенняя основная конфигурация:

<!--创建了一个RedisConnectionFactory,它将Spring会话连接到Redis服务器-->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!--配置Redis连接池 ,可以不配置,使用默认就行!-->
        p:poolConfig-ref="jedisPoolConfig"
    </bean>
 
    <!--创建一个Spring Bean的名称springSessionRepositoryFilter实现过滤器-->
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
        <!--默认session时效30分钟-->
        <property name="maxInactiveIntervalInSeconds" value="60" />
    </bean>

3. Рабочий процесс

Шаги синтаксического анализа Tomcat web.xml:

contextInitialized(ServletContextEvent arg0); // Listener
init(FilterConfig filterConfig); // Filter
init(ServletConfig config); // Servlet

Последовательность инициализации: Слушатель > Фильтр > Сервлет.

1) Загрузите SessionRepositoryFilter в контейнер Spring через прослушиватель Tomcat.

RedisHttpSessionConfiguration объявлен в файле конфигурации Spring в предыдущем разделе, а SessionRepositoryFilter создается в его родительском классе SpringHttpSessionConfiguration:

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

Отношение наследования класса RedisHttpSessionConfiguration

2) инициализация фильтра

Фильтр, настроенный в файле web.xml, называется DelegatingFilterProxy.

Отношение наследования класса DelegatingFilterProxy

Начальная запись DelegatingFilterProxy находится в его родительском классе GenericFilterBean:

public final void init(FilterConfig filterConfig) throws ServletException {
        ......
        // Let subclasses do whatever initialization they like.
        initFilterBean();
        ......
    }

DelegatingFilterProxy переходит к контейнеру Spring и берет springSessionRepositoryFilter, инициализированный на шаге 1:

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 — это хэш-структура, в которой хранится основное содержимое 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 — это строковая структура, в которой хранится нулевое значение.

spring:session:expirations — это заданная структура, в которой хранится значение ключа spring:session:sessions:expires, срок действия которого истекает в момент времени 1557389100000:

smembers spring:session:expirations:1557389100000
1) "\xac\xed\x00\x05t\x00,expires:1b8b2340-da25-4ca6-864c-4af28f033327"

RedisSessionExpirationPolicy, три процесса генерации ключ-значение:

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<Object, Object> 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 с истекшим сроком действия, а именно запланированное удаление, отложенное удаление и обычное удаление.

  1. Удаление по времени: при использовании таймера удаление сразу после истечения срока действия является наиболее эффективным, но также и самым расточительным использованием процессорного времени.
  2. Ленивое удаление: программа определяет, истек ли срок действия ключа, когда он извлекается, и удаляет его после истечения срока действия.Этот метод удобен для процессорного времени, но не для памяти.
  3. Периодическое удаление: выполнение операции по удалению ключей с истекшим сроком действия через регулярные промежутки времени и ограничение времени выполнения и частоты каждой операции удаления является компромиссом.

Redis использует стратегию ленивого удаления и периодического удаления. Видно, что ненадежно полагаться на политику истечения срока действия Redis для удаления ключей с истекшим сроком действия в режиме реального времени.

С другой стороны, бизнес может выполнять обработку бизнес-логики после истечения срока действия Spring Session, и ему нужна информация в сеансе.Если есть только одно значение ключа spring:session:sessions, Redis удаляет его и удаляет, а бизнес не может получить информацию о сеансе.

Ключ spring:session:sessions:expires хранится в ключе spring:session:expirations, а срок действия ключа spring:session:sessions:expires истекает на пять минут раньше, чем ключ spring:session:expirations и ключ spring:session:sessions (фактическое событие Spring Session for The expired обрабатывает ключ spring:session:sessions:expires подписки, который будет описан в следующем разделе), так что значение ключа spring:session:sessions можно получить при подписке на просроченный событие.

Если spring:session:sessions:expires не очищается вовремя с помощью собственного механизма очистки Redis, вы можете использовать временные задачи, предоставляемые Spring Session, чтобы убедиться, что spring:session:sessions:expires очищен.

RedisSessionExpirationPolicy, задача синхронизации очистки сеанса

public void cleanExpiredSessions() {
        long now = System.currentTimeMillis();
        long prevMin = roundDownMinute(now);
        ......
        //获取到spring:session:expirations键
        String expirationKey = getExpirationKey(prevMin);
        // 取出当前这一分钟应当过期的 session
        Set<Object> 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 уведомления о пространстве ключей (Redis doc.com/topic/не, если…

НастройтеNotifyKeyspaceEventsAction, чтобы включить уведомления о пространстве ключей:

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<SessionExpiredEvent> {
    @Override
    public void onApplicationEvent(SessionExpiredEvent event) {
        ......
    }
}

6. Резюме

Spring Session предоставляет нам хорошее решение проблемы совместного использования ресурсов в распределенной среде.Он реализован на основе спецификации сервлета.При использовании бизнеса требуется только простая конфигурация для достижения совместного использования сеанса, чтобы добиться низкой связи с бизнес.Это то, что мы будем делать в будущем.Концепция дизайна, которую можно позаимствовать в разработке проекта.

Больше контента, пожалуйста, обратите вниманиеживые интернет-технологииПубличный аккаунт WeChat

Примечание. Чтобы перепечатать статью, сначала свяжитесь с учетной записью WeChat:labs2020соединять.