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.
- 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.
- 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.
- 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.