spring-session(一)揭秘續篇


上一篇文章中介紹了Spring-Session的核心原理,Filter,Session,Repository等等,傳送門:spring-session(一)揭秘

這篇繼上一篇的原理逐漸深入Spring-Session中的事件機制原理的探索。眾所周知,Servlet規范中有對HttpSession的事件的處理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,可以查看Package javax.servlet

在Spring-Session中也有相應的Session事件機制實現,包括Session創建/過期/刪除事件。

本文主要從以下方面探索Spring-Session中事件機制

  • Session事件的抽象
  • 事件的觸發機制

Note:
這里的事件觸發機制只介紹基於RedissSession的實現。基於內存Map實現的MapSession不支持Session事件機制。其他的Session實現這里也不做關注。

一.Session事件的抽象

先來看下Session事件抽象UML類圖,整體掌握事件之間的依賴關系。

Session Event最頂層是ApplicationEvent,即Spring上下文事件對象。由此可以看出Spring-Session的事件機制是基於Spring上下文事件實現。

抽象的AbstractSessionEvent事件對象提供了獲取Session(這里的是指Spring Session的對象)和SessionId。

基於事件的類型,分類為:

  1. Session創建事件
  2. Session刪除事件
  3. Session過期事件

Tips:
Session銷毀事件只是刪除和過期事件的統一,並無實際含義。

事件對象只是對事件本身的抽象,描述事件的屬性,如:

  1. 獲取事件產生的源:getSource獲取事件產生源
  2. 獲取相應事件特性:getSession/getSessoinId獲取時間關聯的Session

下面再深入探索以上的Session事件是如何觸發,從事件源到事件監聽器的鏈路分析事件流轉過程。

二.事件的觸發機制

閱讀本節前,讀者應該了解Redis的Pub/Sub和KeySpace Notification,如果還不是很了解,傳送門Redis Keyspace NotificationsPub/Sub

上節中也介紹Session Event事件基於Spring的ApplicationEvent實現。先簡單認識spring上下文事件機制:

  • ApplicationEventPublisher實現用於發布Spring上下文事件ApplicationEvent
  • ApplicationListener實現用於監聽Spring上下文事件ApplicationEvent
  • ApplicationEvent抽象上下文事件

那么在Spring-Session中必然包含事件發布者ApplicationEventPublisher發布Session事件和ApplicationListener監聽Session事件。

可以看出ApplicationEventPublisher發布一個事件:

@FunctionalInterface
public interface ApplicationEventPublisher {

	/**
	 * Notify all <strong>matching</strong> listeners registered with this
	 * application of an application event. Events may be framework events
	 * (such as RequestHandledEvent) or application-specific events.
	 * @param event the event to publish
	 * @see org.springframework.web.context.support.RequestHandledEvent
	 */
	default void publishEvent(ApplicationEvent event) {
		publishEvent((Object) event);
	}

	/**
	 * Notify all <strong>matching</strong> listeners registered with this
	 * application of an event.
	 * <p>If the specified {@code event} is not an {@link ApplicationEvent},
	 * it is wrapped in a {@link PayloadApplicationEvent}.
	 * @param event the event to publish
	 * @since 4.2
	 * @see PayloadApplicationEvent
	 */
	void publishEvent(Object event);

}

ApplicationListener用於監聽相應的事件:

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

	/**
	 * Handle an application event.
	 * @param event the event to respond to
	 */
	void onApplicationEvent(E event);

}

Tips:
這里使用到了發布/訂閱模式,事件監聽器可以監聽感興趣的事件,發布者可以發布各種事件。不過這是內部的發布訂閱,即觀察者模式。

Session事件的流程實現如下:

上圖展示了Spring-Session事件流程圖,事件源來自於Redis鍵空間通知,在spring-data-redis項目中抽象MessageListener監聽Redis事件源,然后將其傳播至spring應用上下文發布者,由發布者發布事件。在spring上下文中的監聽器Listener即可監聽到Session事件。

因為兩者是Spring框架提供的對Spring的ApplicationEvent的支持。Session Event基於ApplicationEvent實現,必然也有其相應發布者和監聽器的的實現。

Spring-Session中的RedisSession的SessionRepository是RedisOperationSessionRepository。所有關於RedisSession的管理操作都是由其實現,所以Session的產生源是RedisOperationSessionRepository。

在RedisOperationSessionRepository中持有ApplicationEventPublisher對象用於發布Session事件。

private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
	@Override
	public void publishEvent(ApplicationEvent event) {
	}
	@Override
	public void publishEvent(Object event) {
	}
};

但是該ApplicationEventPublisher是空實現,實際實現是在應用啟動時由Spring-Session自動配置。在spring-session-data-redis模塊中RedisHttpSessionConfiguration中有關於創建RedisOperationSessionRepository Bean時將調用set方法將ApplicationEventPublisher配置。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
		SchedulingConfigurer {

	private ApplicationEventPublisher applicationEventPublisher;

	@Bean
	public RedisOperationsSessionRepository sessionRepository() {
		RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
		RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
				redisTemplate);
		// 注入依賴
		sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
		if (this.defaultRedisSerializer != null) {
			sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
		}
		sessionRepository
				.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
		if (StringUtils.hasText(this.redisNamespace)) {
			sessionRepository.setRedisKeyNamespace(this.redisNamespace);
		}
		sessionRepository.setRedisFlushMode(this.redisFlushMode);
		return sessionRepository;
	}

	// 注入上下文中的ApplicationEventPublisher Bean
	@Autowired
	public void setApplicationEventPublisher(
			ApplicationEventPublisher applicationEventPublisher) {
		this.applicationEventPublisher = applicationEventPublisher;
	}

}

在進行自動配置時,將上下文中的ApplicationEventPublisher的注入,實際上即ApplicationContext對象。

Note:
考慮篇幅原因,以上的RedisHttpSessionConfiguration至展示片段。

對於ApplicationListener是由應用開發者自行實現,注冊成Bean即可。當有Session Event發布時,即可監聽。

/**
 * session事件監聽器
 *
 * @author huaijin
 */
@Component
public class SessionEventListener implements ApplicationListener<SessionDeletedEvent> {

    private static final String CURRENT_USER = "currentUser";

    @Override
    public void onApplicationEvent(SessionDeletedEvent event) {
        Session session = event.getSession();
        UserVo userVo = session.getAttribute(CURRENT_USER);
        System.out.println("Current session's user:" + userVo.toString());
    }
}

以上部分探索了Session事件的發布者和監聽者,但是核心事件的觸發發布則是由Redis的鍵空間通知機制觸發,當有Session創建/刪除/過期時,Redis鍵空間會通知Spring-Session應用。

RedisOperationsSessionRepository實現spring-data-redis中的MessageListener接口。

/**
 * Listener of messages published in Redis.
 *
 * @author Costin Leau
 * @author Christoph Strobl
 */
public interface MessageListener {

	/**
	 * Callback for processing received objects through Redis.
	 *
	 * @param message message must not be {@literal null}.
	 * @param pattern pattern matching the channel (if specified) - can be {@literal null}.
	 */
	void onMessage(Message message, @Nullable byte[] pattern);
}

該監聽器即用來監聽redis發布的消息。RedisOperationsSessionRepositorys實現了該Redis鍵空間消息通知監聽器接口,實現如下:

public class RedisOperationsSessionRepository implements
		FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>,
		MessageListener {

	@Override
	@SuppressWarnings("unchecked")
	public void onMessage(Message message, byte[] pattern) {
		// 獲取該消息發布的redis通道channel
		byte[] messageChannel = message.getChannel();
		// 獲取消息體內容
		byte[] messageBody = message.getBody();

		String channel = new String(messageChannel);

		// 如果是由Session創建通道發布的消息,則是Session創建事件
		if (channel.startsWith(getSessionCreatedChannelPrefix())) {
			// 從消息體中載入Session
			Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer
					.deserialize(message.getBody());
			// 發布創建事件
			handleCreated(loaded, channel);
			return;
		}

		// 如果消息體不是以過期鍵前綴,直接返回。因為spring-session在redis中的key命名規則:
		// "${namespace}:sessions:expires:${sessionId}",如:
		// session.example:sessions:expires:a5236a19-7325-4783-b1f0-db9d4442db9a
		// 所以判斷過期或者刪除的鍵是否為spring-session的過期鍵。如果不是,可能是應用中其他的鍵的操作,所以直接return
		String body = new String(messageBody);
		if (!body.startsWith(getExpiredKeyPrefix())) {
			return;
		}

		// 根據channel判斷鍵空間的事件類型del或者expire時間
		boolean isDeleted = channel.endsWith(":del");
		if (isDeleted || channel.endsWith(":expired")) {
			int beginIndex = body.lastIndexOf(":") + 1;
			int endIndex = body.length();
			// Redis鍵空間消息通知內容即操作的鍵,spring-session鍵中命名規則:
			// "${namespace}:sessions:expires:${sessionId}",以下是根據規則解析sessionId
			String sessionId = body.substring(beginIndex, endIndex);

			// 根據sessionId加載session
			RedisSession session = getSession(sessionId, true);

			if (session == null) {
				logger.warn("Unable to publish SessionDestroyedEvent for session "
						+ sessionId);
				return;
			}

			if (logger.isDebugEnabled()) {
				logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
			}

			cleanupPrincipalIndex(session);

			// 發布Session delete事件
			if (isDeleted) {
				handleDeleted(session);
			}
			else {
				// 否則發布Session expire事件
				handleExpired(session);
			}
		}
	}
}

下續再深入每種事件產生的前世今生。

1.Session創建事件的觸發
  1. 由RedisOperationSessionRepository向Redis指定通道${namespace}:event:created:${sessionId}發布一個message
  2. MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道${namespace}:event:created:${sessionId}的消息
  3. 將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher發布SessionCreateEvent
  5. ApplicationListener監聽SessionCreateEvent,執行相應邏輯

RedisOperationSessionRepository中保存一個Session時,判斷Session是否新創建。
如果新創建,則向

@Override
public void save(RedisSession session) {
	session.saveDelta();
	// 判斷是否為新創建的session
	if (session.isNew()) {
		// 獲取redis指定的channel:${namespace}:event:created:${sessionId},
		// 如:session.example:event:created:82sdd-4123-o244-ps123
		String sessionCreatedKey = getSessionCreatedChannel(session.getId());
		// 向該通道發布session數據
		this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
		// 設置session為非新創建
		session.setNew(false);
	}
}

該save方法的調用是由HttpServletResponse提交時——即返回客戶端響應調用,上篇文章已經詳解,這里不再贅述。關於RedisOperationSessionRepository實現MessageListener上述已經介紹,這里同樣不再贅述。

Note:
這里有點繞。個人認為RedisOperationSessionRepository發布創建然后再本身監聽,主要是考慮分布式或者集群環境中SessionCreateEvent事件的處理。

2.Session刪除事件的觸發

Tips:
刪除事件中使用到了Redis KeySpace Notification,建議先了解該技術。

  1. 由RedisOperationSessionRepository刪除Redis鍵空間中的指定Session的過期鍵,Redis鍵空間會向__keyevent@*:del的channel發布刪除事件消息
  2. MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道__keyevent@*:del的消息
  3. 將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher發布SessionDeleteEvent
  5. ApplicationListener監聽SessionDeleteEvent,執行相應邏輯

當調用HttpSession的invalidate方法讓Session失效時,即會調用RedisOperationSessionRepository的deleteById方法刪除Session的過期鍵。

/**
 * Allows creating an HttpSession from a Session instance.
 *
 * @author Rob Winch
 * @since 1.0
 */
private final class HttpSessionWrapper extends HttpSessionAdapter<S> {
	HttpSessionWrapper(S session, ServletContext servletContext) {
		super(session, servletContext);
	}

	@Override
	public void invalidate() {
		super.invalidate();
		SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
		setCurrentSession(null);
		clearRequestedSessionCache();
		// 調用刪除方法
		SessionRepositoryFilter.this.sessionRepository.deleteById(getId());
	}
}

上篇中介紹了包裝Spring Session為HttpSession,這里不再贅述。這里重點分析deleteById內容:

@Override
public void deleteById(String sessionId) {
	// 如果session為空則返回
	RedisSession session = getSession(sessionId, true);
	if (session == null) {
		return;
	}

	cleanupPrincipalIndex(session);
	this.expirationPolicy.onDelete(session);
	// 獲取session的過期鍵
	String expireKey = getExpiredKey(session.getId());
	// 刪除過期鍵,redis鍵空間產生del事件消息,被MessageListener即
	// RedisOperationSessionRepository監聽
	this.sessionRedisOperations.delete(expireKey);
	session.setMaxInactiveInterval(Duration.ZERO);
	save(session);
}

后續流程同SessionCreateEvent流程。

3.Session失效事件的觸發

Session的過期事件流程比較特殊,因為Redis的鍵空間通知的特殊性,Redis鍵空間通知不能保證過期鍵的通知的及時性。

  1. RedisOperationsSessionRepository中有個定時任務方法每整分運行訪問整分Session過期鍵集合中的過期sessionId,如:spring:session:expirations:1439245080000。觸發Redis鍵空間會向__keyevent@*:expired的channel發布過期事件消息
  2. MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道__keyevent@*:expired的消息
  3. 將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher發布SessionDeleteEvent
  5. ApplicationListener監聽SessionDeleteEvent,執行相應邏輯
@Scheduled(cron = "0 * * * * *")
public void cleanupExpiredSessions() {
	this.expirationPolicy.cleanExpiredSessions();
}

定時任務每整分運行,執行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy實例,是RedisSession過期策略。

public void cleanExpiredSessions() {
    // 獲取當前時間戳
	long now = System.currentTimeMillis();
	// 時間滾動至整分,去掉秒和毫秒部分
	long prevMin = roundDownMinute(now);
	if (logger.isDebugEnabled()) {
		logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
	}
	// 根據整分時間獲取過期鍵集合,如:spring:session:expirations:1439245080000
	String expirationKey = getExpirationKey(prevMin);
	// 獲取所有的所有的過期session
	Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
	// 刪除過期Session鍵集合
	this.redis.delete(expirationKey);
	// touch訪問所有已經過期的session,觸發Redis鍵空間通知消息
	for (Object session : sessionsToExpire) {
		String sessionKey = getSessionKey((String) session);
		touch(sessionKey);
	}
}

將時間戳滾動至整分

static long roundDownMinute(long timeInMs) {
	Calendar date = Calendar.getInstance();
	date.setTimeInMillis(timeInMs);
	// 清理時間錯的秒位和毫秒位
	date.clear(Calendar.SECOND);
	date.clear(Calendar.MILLISECOND);
	return date.getTimeInMillis();
}

獲取過期Session的集合

String getExpirationKey(long expires) {
	return this.redisSession.getExpirationsKey(expires);
}

// 如:spring:session:expirations:1439245080000
String getExpirationsKey(long expiration) {
	return this.keyPrefix + "expirations:" + expiration;
}

調用Redis的Exists命令,訪問過期Session鍵,觸發Redis鍵空間消息

/**
 * 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) {
	this.redis.hasKey(key);
}

總結

至此Spring-Session的Session事件通知模塊就已經很清晰:

  1. Redis鍵空間Session事件源:Session創建通道/Session刪除通道/Session過期通道
  2. Spring-Session中的RedisOperationsSessionRepository消息監聽器監聽Redis的事件類型
  3. RedisOperationsSessionRepository負責將其傳播至ApplicationEventPublisher
  4. ApplicationEventPublisher將其包裝成ApplicationEvent類型的Session Event發布
  5. ApplicationListener監聽Session Event,處理相應邏輯


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM