In this paper, starting from vivo Internet technology WeChat public mp.weixin.qq.com/s/KCOFv0nRu number… Author: Zhang Zhenglin

The HTTP protocol itself is stateless. To save session information, the browser Cookie identifies the session request with the SessionID, and the server uses the SessionID as the key to store session information. In single-instance applications, the storage of application processes can be considered. As the application volume increases, it needs to be expanded horizontally, resulting in the problem of multi-instance session sharing.

Spring Session is to solve the problem of multi-process Session sharing. This article will introduce how to use Spring Session and how Spring Session works.

1. Introduce context

When an application is deployed in Tomcat, sessions are maintained by Tomcat memory. If multiple application instances are deployed, sessions cannot be shared. Spring Session is designed to solve the problem of Session sharing in distributed scenarios.

2. Method of use

Spring Sessions can be stored in Hazelcast, Redis, MongoDB, and relational databases. This article focuses on Session storage in Redis.

The web.xml configuration:

<! -- 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>Copy the code


Spring main configuration:

<! Create a RedisConnectionFactory that connects the Spring session to the Redis server --> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <! -- Redis connection pool configuration, can not configure, use the default line! --> p:poolConfig-ref="jedisPoolConfig"</bean> <! - create a Spring Bean name springSessionRepositoryFilter implementation filters - > < Bean class ="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <! -- Default session duration is 30 minutes --> <property name="maxInactiveIntervalInSeconds" value="60" />
    </bean>
Copy the code


3. Workflow

Tomcat web. XML parsing steps:

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


Initialization sequence: Listener > Filter > Servlet.

1) Load the SessionRepositoryFilter to the Spring container using the Tomcat listener.

Previous section inside the Spring configuration file declares the RedisHttpSessionConfiguration, it is in his father’s class SpringHttpSessionConfiguration generated SessionRepositoryFilter:

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


RedisHttpSessionConfiguration class inheritance

2) Filter initialization

The filter configured in web. XML is DelegatingFilterProxy.

DelegatingFilterProxy Class inheritance relationship

The DelegatingFilterProxy initialization entry is in its parent GenericFilterBean:

public final void init(FilterConfig filterConfig) throws ServletException {
        ......
        // Let subclasses dowhatever initialization they like. initFilterBean(); . }Copy the code


DelegatingFilterProxy to Spring container take an initialization step 1 good 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 enclosing targetBeanName = getFilterName (); } WebApplicationContext wac = findWebApplicationContext();if(wac ! = null) { this.delegate = initDelegate(wac); }}}}Copy the code


At this point, the sessionRepositoryFilter initialization is complete, and DelegatingFilterProxy actually proxies sessionRepositoryFilter.

SessionRepositoryFilter Core process:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); // Wrap HttpServletRequest, Fu wrote it getSession (Boolean create) method SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); . try { filterChain.doFilter(strategyRequest, strategyResponse); } the finally {/ / that the session persistence wrappedRequest.com mitSession (); }}Copy the code


4. Caching mechanism

For each session, Redis actually caches the following data:

spring:session:sessions:1b8b2340-da25-4ca6-864c-4af28f033327

spring:session:sessions:expires:1b8b2340-da25-4ca6-864c-4af28f033327

spring:session:expirations:1557389100000

Spring: Session :sessions Is a hash structure that stores the main contents of a 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\x0 0xp\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.N umber\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\x0 0xp\x00\x00\x01j\x9b\x83\x9d\xfd"
Copy the code


Spring: session: sessions: expires string structure, and store a null value.

Spring: the session: expirations to set structure, storage time expired spring: 1557389100000 session: sessions: expires key values:

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


RedisSessionExpirationPolicy, three key value generating process:

public void onExpirationUpdated(Long originalExpirationTimeInMilli,
            ExpiringSession session) {
        String keyToExpire = "expires:"+ session.getId(); long toExpire = roundUpToNextMinute(expiresInMillis(session)); . / / the spring: session: sessions: expires to join spring: session: expirations beginning key String inside expireKey = getExpirationKey (toExpire); BoundSetOperations<Object, Object> expireOperations = this.redis .boundSetOps(expireKey); expireOperations.add(keyToExpire); long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5); / / spring: session: expirations at the beginning of key time expiration time for XML configuration expireOperations. Five minutes after the expire (fiveMinutesAfterExpires, TimeUnit. SECONDS);if (sessionExpireInSeconds == 0) {
            this.redis.delete(sessionKey);
        }
        else{/ / spring: session: sessions: expires at the beginning of key time expiration time for XML configuration this. Redis. BoundValueOps (sessionKey). Append (""); this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS); } / / spring: session: sessions at the beginning of key time expiration time for XML configuration after five minutes this. Redis. BoundHashOps (getSessionKey (session. The getId ())) .expire(fiveMinutesAfterExpires, TimeUnit.SECONDS); }Copy the code


The Redis expiration key has three deletion strategies, namely periodic deletion, lazy deletion and periodic deletion.

  1. Scheduled deletion: Maintaining a timer to delete the data immediately after it expires is the most efficient, but also the most wasteful of CPU time.
  2. Lazy delete: The program determines whether a key is expired only when it is retrieved, and deletes it only when it is expired. This method is CPU-time-friendly and memory-unfriendly.
  3. Periodic deletion: Deleting expired keys at regular intervals and limiting the duration and frequency of each deletion is a compromise.

Redis uses a strategy of lazy deletion and periodic deletion. Therefore, it is not reliable to rely on Redis’ expiration policy to delete expired keys in real time.

On the other hand, services may perform business logic processing after the Spring Session expires and need the information in the Session. If there is only one Spring: Session: Sessions key value, the Redis deletion will be deleted and the service cannot obtain the Session information.

Spring: the session: expirations keys stored in the spring: session: sessions: expires key, And spring: session: sessions: expires key expired five minutes before the spring: session: expirations key and the spring: session: key sessions (actual spring Spring Session for overdue event handling subscription: the Session: sessions: expires, the next section will specifically), when the subscription to the event date so can get spring: Session: sessions key values.

If cleaning mechanism by Redis itself is not seasonable clear spring: session: sessions: expires, can through the spring session to provide out the timing of tasks, ensure that the spring: session: sessions: clear expires.

RedisSessionExpirationPolicy, clear the session timer task

public void cleanExpiredSessions() {
        long now = System.currentTimeMillis();
        long prevMin= roundDownMinute(now); . / / get the spring: session: the expirations key String expirationKey = getExpirationKey (prevMin); // Retrieve session Set<Object> sessionsToExpire = this.redis.boundsetops (expirationKey).members(); / / note: delete here is spring: session: expirations key, not delete the session itself! this.redis.delete(expirationKey);for(Object session : sessionsToExpire) { String sessionKey = getSessionKey((String) session); / / traverse the spring: session: sessions: expires key touch (sessionKey); } } /** * By trying to access the session we only trigger a deletionif it the TTL is
     * expired. This is doneto handle * https://github.com/spring-projects/spring-session/issues/93 * * @param key the key */ private void Touch (String key) {/ / not delete key directly, but only access key, through the inert delete ensure spring: session: sessions: expires real-time delete key, / / but also guarantee the multi-thread concurrent renew scenarios, Key move to a different spring: session: expirations keys inside, / / in the spring: session: sessions: key actual TTL time expires this. Redis. HasKey (key); }Copy the code


5. Event subscriptions

By default, at least subscribe to key space notification of gxE events (redisdoc.com/topic/notif…

ConfigureNotifyKeyspaceEventsAction, open the key space inform:

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);
       }
   }
Copy the code


RedisHttpSessionConfiguration, register to monitor events:

@Bean public RedisMessageListenerContainer redisMessageListenerContainer( RedisConnectionFactory connectionFactory, RedisOperationsSessionRepository messageListener) { ...... / / psubscribe del and expired events container. AddMessageListener (messageListener, Arrays. AsList (new PatternTopic ("__keyevent@*:del"),
                     new PatternTopic("__keyevent@*:expired"))); / / psubscribe container created events. AddMessageListener (messageListener, Arrays.asList(new PatternTopic( messageListener.getSessionCreatedChannelPrefix() +"*")));
     return container;
 }
Copy the code


RedisOperationsSessionRepository, event processing:

public void onMessage(Message message, byte[] pattern) {
      ......
      if(channel.startsWith(getSessionCreatedChannelPrefix())) { ... // Handle the Spring :session created event handleCreated(loaded, channel);return; } / / the spring: session: sessions: expires events do not deal with String body = new String (messageBody);if(! body.startsWith(getExpiredKeyPrefix())) {return;
      }
 
      boolean isDeleted = channel.endsWith(":del");
      if (isDeleted || channel.endsWith(":expired")) {...if(isDeleted) {/ / processing spring: session: sessions: expires handleDeleted del events (sessionId, session); }else{/ / processing spring: session: sessions: expires handleExpired expired events (sessionId, session); }...return; }}Copy the code


Example of event subscription:

@Component public class SessionExpiredListener implements ApplicationListener<SessionExpiredEvent> { @Override public void onApplicationEvent(SessionExpiredEvent event) { ...... }}Copy the code


6, summary

Spring Session provides us with a good idea to solve the problem of resource sharing in the distributed environment. It is implemented based on the Servlet specification, and Session sharing can be realized only after simple configuration in the use of business, so as to achieve low coupling with business. These are all design concepts that can be borrowed in our future project development.

For more content, please pay attention to vivo Internet technology wechat public account

Note: To reprint the article, please contact our wechat account: LABs2020.