1、在pom中添加如下依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2、在yml中配置redis
注:此處配置完全是spring-boot-starter-data-redis相關配置參數(shù),跟spring-session-data-redis無關。后者依賴前者
#redis
#spring.redis.host=localhost
#spring.redis.port=6379
#spring.redis.sentinel.master = sentinel-name
#spring.redis.sentinel.nodes = 192.168.1.2:6389,192.168.1.3:6390,192.168.1.4:6387
##公共配置
#spring.redis.password = 123456
# ; 連接池最大連接數(shù),默認8個,(使用負值表示沒有限制)
#spring.redis.pool.max-active = 21
# ; 連接池最大阻塞等待時間(使用負值表示沒有限制)
#spring.redis.pool.max-wait = -1
# ; 連接池中的最大空閑連接
#spring.redis.pool.max-idle = 8
# ; 連接池中的最小空閑連接
#spring.redis.pool.min-idle = 0
# ; 連接超時時間(毫秒)
#spring.redis.timeout = 1000
spring:
redis:
password: 123456
pool:
max-active: 21
max-idle: 8
max-wait: -1
min-idle: 0
sentinel:
master: sentinel-name
nodes: 192.168.1.2:6389,192.168.1.3:6390,192.168.1.4:6387
timeout: 1000
3、在項目啟動類上添加如下注解
@EnableRedisHttpSession
EnableRedisHttpSession源碼解析
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
//Session默認過期時間,秒為單位,默認30分鐘
int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
//配置key的namespace,默認的是spring:session,如果不同的應用共用一個redis,應該為應用配置不同的namespace,這樣才能區(qū)分這個Session是來自哪個應用的
String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
//配置刷新Redis中Session的方式,默認是ON_SAVE模式,只有當Response提交后才會將Session提交到Redis
//這個模式也可以配置成IMMEDIATE模式,這樣的話所有對Session的更改會立即更新到Redis
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
//清理過期Session的定時任務默認一分鐘一次。
String cleanupCron() default RedisHttpSessionConfiguration.DEFAULT_CLEANUP_CRON;
}
原理
上面的注解的主要作用是注冊一個SessionRepositoryFilter,這個Filter會攔截到所有的請求,對Session進行操作,具體的操作細節(jié)會在后面講解,這邊主要了解這個注解的作用是注冊SessionRepositoryFilter就行了。注入SessionRepositoryFilter的代碼在RedisHttpSessionConfiguration這個類中。
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
SchedulingConfigurer {
...
}
RedisHttpSessionConfiguration繼承了SpringHttpSessionConfiguration,SpringHttpSessionConfiguration中注冊了SessionRepositoryFilter。見下面代碼。
@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
...
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
...
}
我們發(fā)現(xiàn)注冊SessionRepositoryFilter時需要一個SessionRepository參數(shù),這個參數(shù)是在RedisHttpSessionConfiguration中被注入進入的。
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
SchedulingConfigurer {
...
@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);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
return sessionRepository;
}
...
}
請求進來的時候攔截器會先將request和response攔截住,然后將這兩個對象轉換成Spring內(nèi)部的包裝類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對象根據(jù)SESSIONID去Redis中查找Session
S 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);
//返回Session
return currentSession;
}
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (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");
}
//不創(chuàng)建Session就直接返回null
if (!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創(chuàng)建RedisSession這個對象,可以看下這個類的源代碼,如果
//@EnableRedisHttpSession這個注解中的redisFlushMode模式配置為IMMEDIATE模式,會立即
//將創(chuàng)建的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操作,提升性能。
到現(xiàn)在為止我們發(fā)現(xiàn)如果redisFlushMode配置為ON_SAVE模式的話,Session信息還沒被保存到Redis中,那么這個同步操作到底是在哪里執(zhí)行的呢?我們發(fā)現(xiàn)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中去,同時還會發(fā)布一
//SESSION創(chuàng)建事件到隊列里面去
wrappedRequest.commitSession();
}
}
總結
主要的核心類有:
- @EnableRedisHttpSession:開啟Session共享功能
- RedisHttpSessionConfiguration:配置類,一般不需要我們自己配置。主要功能是配置SessionRepositoryFilter和RedisOperationsSessionRepository這兩個Bean
- SessionRepositoryFilter:攔截器
- RedisOperationsSessionRepository:可以認為是一個Redis操作的客戶端,有在Redis中增刪改查Session的功能
- SessionRepositoryRequestWrapper:Request的包裝類,主要是重寫了getSession方法
- SessionRepositoryResponseWrapper:Response的包裝類。
原理簡要總結:
當請求進來的時候,SessionRepositoryFilter會先攔截到請求,將request和Response對象轉換成SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。后續(xù)當?shù)谝淮握{用request的getSession方法時,會調用到SessionRepositoryRequestWrapper的getSession方法。這個方法的邏輯是先從request的屬性中查找,如果找不到;再查找一個key值是"SESSION"的cookie,通過這個cookie拿到sessionId去redis中查找,如果查不到,就直接創(chuàng)建一個RedisSession對象,同步到Redis中(同步的時機根據(jù)配置來)。