SpringSession系列-存儲機制之Redis&Map

在以前的文章中已經對SpringSession的功能結構,請求/響應重寫等作了介紹。本文將繼續來介紹下SpringSession中存儲部分的設計。存儲是分佈式session中算是最核心的部分,經過引入三方的存儲容器來實現session的存儲,從而有效的解決session共享的問題。html

一、SpringSession存儲的頂級抽象接口

SpringSession存儲的頂級抽象接口是org.springframework.session包下的SessionRepository這個接口。SessionRepository的類圖結構以下:html5

這裏先來看下SessionRepository這個頂層接口中定義了哪些方法:java

public interface SessionRepository<S extends Session> {
    //建立一個session
	S createSession();
	//保存session
	void save(S session);
	//經過ID查找session
	S findById(String id);
	//經過ID刪除一個session
	void deleteById(String id);
}
複製代碼

從代碼來看仍是很簡單的,就是增刪查。下面看具體實現。在2.0版本開始SpringSession中也提供了一個和SessionRepository具體相同能力的ReactiveSessionRepository,用於支持響應式編程模式。git

二、MapSessionRepository

基於HashMap實現的基於內存存儲的存儲器實現,這裏就主要看下對於接口中幾個方法的實現。github

public class MapSessionRepository implements SessionRepository<MapSession> {
	private Integer defaultMaxInactiveInterval;
	private final Map<String, Session> sessions;
	//...
}
複製代碼

能夠看到就是一個Map,那後面關於增刪查其實就是操做這個Map了。web

createSession

@Override
public MapSession createSession() {
	MapSession result = new MapSession();
	if (this.defaultMaxInactiveInterval != null) {
		result.setMaxInactiveInterval(
			Duration.ofSeconds(this.defaultMaxInactiveInterval));
	}
	return result;
}
複製代碼

這裏很直接,就是new了一個MapSession,而後設置了session的有效期。redis

save

@Override
public void save(MapSession session) {
	if (!session.getId().equals(session.getOriginalId())) {
		this.sessions.remove(session.getOriginalId());
	}
	this.sessions.put(session.getId(), new MapSession(session));
}
複製代碼

這裏面先判斷了session中的兩個ID,一個originalId,一個當前idoriginalId是第一次生成session對象時建立的,後面都不會在變化。經過源碼來看,對於originalId,只提供了get方法。對於id呢,實際上是能夠經過changeSessionId來改變的。spring

這裏的這個操做其實是一種優化行爲,及時的清除掉老的session數據來釋放內存空間。編程

findById

@Override
public MapSession findById(String id) {
	Session saved = this.sessions.get(id);
	if (saved == null) {
		return null;
	}
	if (saved.isExpired()) {
		deleteById(saved.getId());
		return null;
	}
	return new MapSession(saved);
}
複製代碼

這個邏輯也很簡單,先從Map中根據id取出session數據,若是沒有就返回null,若是有則再判斷下是否過時了,若是過時了就刪除掉,而後返回null。若是查到了,而且沒有過時的話,則構建一個MapSession返回。api

OK,基於內存存儲的實現系列就是這些了,下面繼續來看其餘存儲的實現。

三、FindByIndexNameSessionRepository

FindByIndexNameSessionRepository繼承了SessionRepository接口,用於擴展對第三方存儲的實現。

public interface FindByIndexNameSessionRepository<S extends Session> extends SessionRepository<S> {
		
	String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()
			.concat(".PRINCIPAL_NAME_INDEX_NAME");

	Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);

	default Map<String, S> findByPrincipalName(String principalName) {
		return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
	}
}
複製代碼

FindByIndexNameSessionRepository添加一個單獨的方法爲指定用戶查詢全部會話。這是經過設置名爲FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAMESession的屬性值爲指定用戶的username來完成的。開發人員有責任確保屬性被賦值,由於SpringSession不會在乎被使用的認證機制。官方文檔中給出的例子以下:

String username = "username";
this.session.setAttribute(
	FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
複製代碼

FindByIndexNameSessionRepository的一些實現會提供一些鉤子自動的索引其餘的session屬性。好比,不少實現都會自動的確保當前的Spring Security用戶名稱可經過索引名稱FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME進行索引。一旦會話被索引,就能夠經過下面的代碼檢索:

String username = "username";
Map<String, Session> sessionIdToSession = 
	this.sessionRepository.findByIndexNameAndIndexValue(
	FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,username);
複製代碼

下圖是FindByIndexNameSessionRepository接口的三個實現類:

FindByIndexNameSessionRepository

下面來分別分析下這三個存儲的實現細節。

3.1 RedisOperationsSessionRepository

RedisOperationsSessionRepository的類圖結構以下,MessageListenerredis消息訂閱的監聽接口。

在這裏插入圖片描述

代碼有點長,就不在這裏面貼了,一些註釋能夠在這個 SpringSession中文分支 來看。這裏仍是主要來看下對於那幾個方法的實現。

3.1.1 createSession

這裏和MapSessionRepository的實現基本同樣的,那區別就在於Session的封裝模型不同,這裏是RedisSession,實際上RedisSession的實現是對MapSession又包了一層。下面會分析RedisSession這個類。

@Override
public RedisSession createSession() { 
    // RedisSession,這裏和MapSession區別開
	RedisSession redisSession = new RedisSession();
	if (this.defaultMaxInactiveInterval != null) {
		redisSession.setMaxInactiveInterval(
				Duration.ofSeconds(this.defaultMaxInactiveInterval));
	}
	return redisSession;
}
複製代碼

在看其餘兩個方法以前,先來看下RedisSession這個類。

3.1.2 RedisSession

這個在模型上是對MapSession的擴展,增長了delta這個東西。

final class RedisSession implements Session {
       // MapSession 實例對象,主要存數據的地方
		private final MapSession cached;
		// 原始最後訪問時間
		private Instant originalLastAccessTime;
		private Map<String, Object> delta = new HashMap<>();
		// 是不是新的session對象
		private boolean isNew;
		// 原始主名稱
		private String originalPrincipalName;
		// 原始sessionId
		private String originalSessionId;
複製代碼

delta是一個Map結構,那麼這裏面究竟是放什麼的呢?具體細節見 saveDelta 這個方法。saveDelta 這個方法會在兩個地方被調用,一個是下面要說道的save方法,另一個是 flushImmediateIfNecessary 這個方法:

private void flushImmediateIfNecessary() {
	if (RedisOperationsSessionRepository.this.redisFlushMode == RedisFlushMode.IMMEDIATE) {
		saveDelta();
	}
}
複製代碼

RedisFlushMode提供了兩種推送模式:

  • ON_SAVE:只有在調用save方法時執行,在web環境中這樣作一般是儘快提交HTTP響應
  • IMMEDIATE:只要有變動就會直接寫到redis中,不會像ON_SAVE同樣,在最後commit時一次性寫入

追蹤flushImmediateIfNecessary 方法調用鏈以下:

在這裏插入圖片描述
那麼到這裏基本就清楚了,首先 save這個方法,當主動調用 save時就是將數據推到 redis中去的,也就是 ON_SAVE這種狀況。那麼對於 IMMEDIATE這種狀況,只有調用了上面的四個方法, SpringSession 纔會將數據推送到 redis

因此delta裏面存的是當前一些變動的 key-val 鍵值對象,而這些變動是由setAttributeremoveAttributesetMaxInactiveIntervalInSecondssetLastAccessedTime這四個方法觸發的;好比setAttribute(k,v),那麼這個k->v就會被保存到delta裏面。

3.1.3 save

在理解了saveDelta方法以後再來看save方法就簡單多了。save 對應的就是RedisFlushMode.ON_SAVE

@Override
public void save(RedisSession session) {
   // 直接調用 saveDelta推數據到redis
	session.saveDelta();
	if (session.isNew()) {
	   // sessionCreatedKey->channl
		String sessionCreatedKey = getSessionCreatedChannel(session.getId());
		// 發佈一個消息事件,新增 session,以供 MessageListener 回調處理。
		this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
		session.setNew(false);
	}
}
複製代碼

3.1.4 findById

查詢這部分和基於Map的差異比較大,由於這裏並非直接操做Map,而是與Redis 進行一次交互。

@Override
public RedisSession findById(String id) {
	return getSession(id, false);
}
複製代碼

調用getSession方法:

private RedisSession getSession(String id, boolean allowExpired) {
	// 根據ID從redis中取出數據
	Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
	if (entries.isEmpty()) {
		return null;
	}
	//轉換成MapSession
	MapSession loaded = loadSession(id, entries);
	if (!allowExpired && loaded.isExpired()) {
		return null;
	}
	//轉換成RedisSession
	RedisSession result = new RedisSession(loaded);
	result.originalLastAccessTime = loaded.getLastAccessedTime();
	return result;
}
複製代碼

loadSession中構建MapSession

private MapSession loadSession(String id, Map<Object, Object> entries) {
   // 生成MapSession實例
	MapSession loaded = new MapSession(id);
	//遍歷數據
	for (Map.Entry<Object, Object> entry : entries.entrySet()) {
		String key = (String) entry.getKey();
		if (CREATION_TIME_ATTR.equals(key)) {
		    // 設置建立時間
			loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
		}
		else if (MAX_INACTIVE_ATTR.equals(key)) {
			 // 設置最大有效時間
			loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
		}
		else if (LAST_ACCESSED_ATTR.equals(key)) {
			// 設置最後訪問時間
			loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
		}
		else if (key.startsWith(SESSION_ATTR_PREFIX)) {
		// 設置屬性
			loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()),
					entry.getValue());
		}
	}
	return loaded;
}
複製代碼

3.1.5 deleteById

根據sessionId刪除session數據。具體過程看代碼註釋。

@Override
public void deleteById(String sessionId) {
   // 獲取 RedisSession
	RedisSession session = getSession(sessionId, true);
	if (session == null) {
		return;
	}
   // 清楚當前session數據的索引
	cleanupPrincipalIndex(session);
	//執行刪除操做
	this.expirationPolicy.onDelete(session);
	String expireKey = getExpiredKey(session.getId());
	//刪除expireKey
	this.sessionRedisOperations.delete(expireKey);
	//session有效期設置爲0
	session.setMaxInactiveInterval(Duration.ZERO);
	save(session);
}
複製代碼

3.1.6 onMessage

最後來看下這個訂閱回調處理。這裏看下核心的一段邏輯:

boolean isDeleted = channel.equals(this.sessionDeletedChannel);
// Deleted 仍是 Expired ?
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
	// 此處省略無關代碼
	// Deleted
	if (isDeleted) {
	   // 發佈一個 SessionDeletedEvent 事件
		handleDeleted(session);
	}
	// Expired
	else {
		// 發佈一個 SessionExpiredEvent 事件
		handleExpired(session);
	}
}
複製代碼

3.2 Redis 存儲的一些思考

首先按照咱們本身常規的思路來設計的話,咱們會怎麼來考慮這個事情。這裏首先要聲明下,我對 Redis 這個東西不是很熟,沒有作過深刻的研究;那若是是我來作,可能也就僅僅限於存儲。

  • findByIndexNameAndIndexValue的設計,這個的做用是經過indexNameindexValue來返回當前用戶的全部會話。可是這裏須要考慮的一個事情是,一般狀況下,一個用戶只會關聯到一個會話上面去,那這種設計很顯然,個人理解是爲了支持單用戶多會話的場景。
    • indexName:FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
    • indexValue:username
  • 實現 MessageListener 接口,增長事件通知能力。經過監聽這些事件,能夠作一些session操做管控。可是實際上 SpringSession 中並無作任何事情,從代碼來看,publishEvent方法是空實現。等待回覆中 #issue 1287
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
	@Override
	public void publishEvent(ApplicationEvent event) {
	}
	@Override
	public void publishEvent(Object event) {
	}
};
複製代碼
  • RedisFlushModeSpringSession中提供了兩種模式的推送,一種是ON_SAVE,另一種是IMMEDIATE。默認是ON_SAVE,也就是常規的在請求處理結束時進行一次sessionCommit操做。RedisFlushMode 的設計感受是爲session數據持久化的時機提供了另一種思路。

小結

存儲機制設計部分就一基於內存和基於Redis兩種來分析;另外基於jdbchazelcast有興趣的同窗能夠本身查看源碼。

最後也歡迎訪問個人我的博客:www.glmapper.com

參考

相關文章
相關標籤/搜索