SpringSession:存儲機制設計

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

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

SpringSession:存儲機制設計

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

public interface SessionRepository {
//創建一個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,用於支持響應式編程模式。

2、MapSessionRepository

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

public class MapSessionRepository implements SessionRepository<mapsession> {
private Integer defaultMaxInactiveInterval;
private final Map<string> sessions;
//...
}
/<string>/<mapsession>

可以看到就是一個Map,那後面關於增刪查其實就是操作這個Map了。

createSession

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

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

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,一個當前id。originalId是第一次生成session對象時創建的,後面都不會在變化。通過源碼來看,對於originalId,只提供了get方法。對於id呢,其實是可以通過changeSessionId來改變的。

這裡的這個操作實際上是一種優化行為,及時的清除掉老的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返回。

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

3、FindByIndexNameSessionRepository

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

public interface FindByIndexNameSessionRepository
extends SessionRepository {

String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()
.concat(".PRINCIPAL_NAME_INDEX_NAME");
Map<string> findByIndexNameAndIndexValue(String indexName, String indexValue);
default Map<string> findByPrincipalName(String principalName) {
return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
}
}
/<string>/<string>

FindByIndexNameSessionRepository添加一個單獨的方法為指定用戶查詢所有會話。這是通過設置名為FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME的Session的屬性值為指定用戶的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> sessionIdToSession =
this.sessionRepository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,username);
/<string>

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

SpringSession:存儲機制設計

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

3.1 RedisOperationsSessionRepository

RedisOperationsSessionRepository的類圖結構如下,MessageListener是redis消息訂閱的監聽接口。

SpringSession:存儲機制設計

代碼有點長,就不在這裡面貼了,一些註釋可以在這個 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> delta = new HashMap<>();
// 是否是新的session對象
private boolean isNew;
// 原始主名稱
private String originalPrincipalName;
// 原始sessionId
private String originalSessionId;

/<string>

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 方法調用鏈如下:

SpringSession:存儲機制設計

那麼到這裡基本就清楚了,首先save這個方法,當主動調用save時就是將數據推到redis中去的,也就是ON_SAVE這種情況。那麼對於IMMEDIATE這種情況,只有調用了上面的四個方法,SpringSession 才會將數據推送到redis。所以delta裡面存的是當前一些變更的 key-val 鍵值對象,而這些變更是由setAttribute、removeAttribute、setMaxInactiveIntervalInSeconds、setLastAccessedTime這四個方法觸發的;比如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> 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;
}

/<object>

loadSession中構建MapSession:

private MapSession loadSession(String id, Map<object> entries) {
// 生成MapSession實例
MapSession loaded = new MapSession(id);
//遍歷數據
for (Map.Entry<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;
}

/<object>/<object>

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的設計,這個的作用是通過indexName和indexValue來返回當前用戶的所有會話。但是這裡需要考慮的一個事情是,通常情況下,一個用戶只會關聯到一個會話上面去,那這種設計很顯然,我的理解是為了支持單用戶多會話的場景。
  • 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) {
}
};

  • RedisFlushMode ,SpringSession中提供了兩種模式的推送,一種是ON_SAVE,另外一種是IMMEDIATE。默認是ON_SAVE,也就是常規的在請求處理結束時進行一次sessionCommit操作。RedisFlushMode 的設計感覺是為session數據持久化的時機提供了另外一種思路。

小結

存儲機制設計部分就一基於內存和基於Redis兩種來分析;另外基於jdbc和hazelcast有興趣的同學可以自己查看源碼。關注、轉發、評論頭條號每天分享java 知識,私信回覆“555”贈送一些Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式資料


分享到:


相關文章: