本文借鑒于:Spring-Session 原理簡析 - 知乎 (zhihu.com)
目錄
概述
使用方式
原理
總結
概述
Session的原理
Session是存在服務器的一種用來存放用戶數據的類哈希表結構,當瀏覽器第一次發送請求的時候服務器會生成一個hashtable和一個sessionid,sessionid來唯一標識這個hashtable,響應的時候會通過一個響應頭set-cookie返回給瀏覽器,瀏覽器再將這個sessionid存儲在一個名為JESSIONID的cookie中。 接著瀏覽器在發送第二次請求時,就會帶上這個cookie,這個cookie會存儲sessionid,并發送給服務器,服務器根據這個sessionid找到對應的用戶信息。
分布式下Session共享的問題
如果在不同域名環境下需要進行session共享,比如在auth.gulimall.com登錄成功之后要將登陸成功的用戶信息共享給gulimall.com服務,由于域名不同,普通的session就會不起作用。并且如果是同一個服務,復制多份,session也不會共享。
SpringSession
SpringSession 是一個 Spring 項目中用于管理和跟蹤會話的框架。它提供了一種抽象層,使得會話數據可以存儲在不同的后端數據結構中(例如內存、數據庫、Redis 等),并且支持跨多個請求的會話管理。
Spring Session 的核心功能包括:
(1)跨多個請求共享會話數據:SpringSession 使用一個唯一的會話標識符來跟蹤用戶的會話,并且可以在不同的請求中共享會話數據,無論是在同一個應用程序的不同服務器節點之間,還是在不同的應用程序之間。
(2)多種后端存儲支持:SpringSession 支持多種后端存儲,包括內存、數據庫和緩存系統(如 Redis)。你可以根據應用程序的需求選擇合適的存儲方式。
(3)會話過期管理:SpringSession 提供了會話過期管理的功能,可以根據一定的策略自動清理過期的會話數據。 會話事件監聽:Spring Session 允許開發人員注冊會話事件監聽器,以便在會話創建、銷毀或屬性更改時執行一些自定義邏輯。
(4)使用 SpringSession 可以使得在分布式環境中管理會話變得更加簡單和靈活,同時也提供了更多的擴展性和可定制性。
SpringSession 可以在多個微服務之間共享 session 數據。
使用方式
添加依賴
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
添加注解@EnableRedisHttpSession
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class RedisSessionConfig {}
maxInactiveIntervalInSeconds
: 設置 Session 失效時間,使用 Redis Session 之后,原 Spring Boot 的 server.session.timeout 屬性不再生效。也可以在yml文件上配置
spring: session:store-type: redistimeout: 30m #這個過期時間是在瀏覽器關閉之后才開始計時
經過上面的配置后,Session 調用就會自動去Redis存取。另外,想要達到 Session 共享的目的,只需要在其他的系統上做同樣的配置即可。
原理
看了上面的配置,我們知道開啟 Redis Session 的“秘密”在 @EnableRedisHttpSession 這個注解上。打開 @EnableRedisHttpSession 的源碼(shift+shift):
/*** 用于啟用基于Redis的HTTP會話管理的配置注解。* 該注解會將RedisHttpSessionConfiguration配置類導入到Spring配置中,* 從而支持將會話信息存儲在Redis中。** @author <NAME>* @see RedisHttpSessionConfiguration*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({RedisHttpSessionConfiguration.class})
@Configuration(proxyBeanMethods = false
)
public @interface EnableRedisHttpSession {/*** 會話的最大不活動間隔時間(秒)。默認值為1800秒(30分鐘)。* 該屬性定義了會話在Redis中存儲的有效期。* * @return 最大不活動間隔時間(秒)*/int maxInactiveIntervalInSeconds() default 1800;/*** Redis中用于存儲會話數據的命名空間。默認值為"spring:session"。* 通過設置不同的命名空間,可以實現多個應用會話的隔離。* * @return Redis存儲會話數據的命名空間*/String redisNamespace() default "spring:session";/*** 已棄用的Redis刷新模式設置。建議使用{@link #flushMode()}替代。* * @return Redis刷新模式,默認為ON_SAVE* @deprecated 使用{@link #flushMode()}替代*/@DeprecatedRedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;/*** 定義Redis中數據的刷新模式。可配置為在每次保存屬性時刷新(ON_SAVE),* 或者在請求結束時刷新(ON_COMMIT)。* * @return 刷新模式,默認為ON_SAVE*/FlushMode flushMode() default FlushMode.ON_SAVE;/*** 定義清理過期會話的CRON表達式。默認值為"0 * * * * *",即每分鐘執行一次。* 通過調整該表達式,可以控制清理過期會話的頻率。* * @return 清理過期會話的CRON表達式*/String cleanupCron() default "0 * * * * *";/*** 定義會話屬性保存的模式。可配置為僅在設置屬性時保存(ON_SET_ATTRIBUTE),* 或者在每次請求結束時保存(ALWAYS)。* * @return 會話屬性保存模式,默認為ON_SET_ATTRIBUTE*/SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
}
這個注解的主要作用是注冊一個 SessionRepositoryFilter,這個 Filter 會攔截所有的請求,對 Session 進行操作。
SessionRepositoryFilter 攔截到請求后,會先將 request 和 response 對象轉換成 Spring 內部的包裝類 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 對象。SessionRepositoryRequestWrapper 類重寫了原生的getSession
方法。代碼如下:
@Override
public HttpSessionWrapper getSession(boolean create) {//通過request的getAttribue方法查找CURRENT_SESSION屬性,有直接返回HttpSessionWrapper currentSession = getCurrentSession();if (currentSession != null) {return currentSession;}//查找客戶端中一個叫SESSION的cookie,通過sessionRepository對象根據SESSIONID去Redis中查找SessionS requestedSession = getRequestedSession();if (requestedSession != null) {if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {requestedSession.setLastAccessedTime(Instant.now());this.requestedSessionIdValid = true;currentSession = new HttpSessionWrapper(requestedSession, getServletContext());currentSession.setNew(false);//將Session設置到request屬性中setCurrentSession(currentSession);//返回Sessionreturn currentSession;}}else {// This is an invalid session id. No need to ask again if// request.getSession is invoked for the duration of this requestif (SESSION_LOGGER.isDebugEnabled()) {SESSION_LOGGER.debug("No session found by id: Caching result for getSession(false) for this HttpServletRequest.");}setAttribute(INVALID_SESSION_ID_ATTR, "true");}//不創建Session就直接返回nullif (!create) {return null;}if (SESSION_LOGGER.isDebugEnabled()) {SESSION_LOGGER.debug("A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "+ SESSION_LOGGER_NAME,new RuntimeException("For debugging purposes only (not an error)"));}//通過sessionRepository創建RedisSession這個對象,可以看下這個類的源代碼,如果//@EnableRedisHttpSession這個注解中的redisFlushMode模式配置為IMMEDIATE模式,會立即//將創建的RedisSession同步到Redis中去。默認是不會立即同步的。S session = SessionRepositoryFilter.this.sessionRepository.createSession();session.setLastAccessedTime(Instant.now());currentSession = new HttpSessionWrapper(session, getServletContext());setCurrentSession(currentSession);return currentSession;
}
當調用 SessionRepositoryRequestWrapper 對象的getSession
方法拿 Session 的時候,會先從當前請求的屬性中查找CURRENT_SESSION
屬性,如果能拿到直接返回,這樣操作能減少Redis操作,提升性能。
到現在為止我們發現如果redisFlushMode
配置為 ON_SAVE 模式的話,Session 信息還沒被保存到 Redis 中,那么這個同步操作到底是在哪里執行的呢?
仔細看代碼,我們發現 SessionRepositoryFilter 的doFilterInternal
方法最后有一個 finally 代碼塊,這個代碼塊的功能就是將 Session同步到 Redis。
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext);SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);try {filterChain.doFilter(wrappedRequest, wrappedResponse);}finally {//將Session同步到Redis,同時這個方法還會將當前的SESSIONID寫到cookie中去,同時還會發布一//SESSION創建事件到隊列里面去wrappedRequest.commitSession();}
}
總結
主要的核心類有:
- EnableRedisHttpSession:開啟 Session 共享功能;
- RedisHttpSessionConfiguration:配置類,一般不需要我們自己配置,主要功能是配置 SessionRepositoryFilter 和 RedisOperationsSessionRepository 這兩個Bean;
- SessionRepositoryFilter:攔截器,Spring-Session 框架的核心;
- RedisOperationsSessionRepository:可以認為是一個 Redis 操作的客戶端,有在 Redis 中進行增刪改查 Session 的功能;
- SessionRepositoryRequestWrapper:Request 的包裝類,主要是重寫了
getSession
方法 - SessionRepositoryResponseWrapper:Response的包裝類。
原理簡要總結:
當請求進來的時候,SessionRepositoryFilter 會先攔截到請求,將 request 和 response 對象轉換成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 。后續當第一次調用 request 的getSession方法時,會調用到 SessionRepositoryRequestWrapper 的getSession
方法。這個方法是被重寫過的,邏輯是先從 request 的屬性中查找,如果找不到,再查找一個key值是"SESSION"的 Cookie,通過這個 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接創建一個RedisSession 對象,同步到 Redis 中。
說的簡單點就是:攔截請求,將之前在服務器內存中進行 Session 創建、銷毀的動作,改成在 Redis 中進行。