依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.6.8</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.6.3</version>
</dependency>
</dependencies>
启动过程
- 加载 redis session 相关配置
// org.springframework.boot.autoconfigure.session.RedisSessionConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RedisTemplate.class, RedisIndexedSessionRepository.class })
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@Conditional(ServletSessionCondition.class)
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {
@Bean
@ConditionalOnMissingBean
ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {
switch (redisSessionProperties.getConfigureAction()) {
case NOTIFY_KEYSPACE_EVENTS:
return new ConfigureNotifyKeyspaceEventsAction();
case NONE:
return ConfigureRedisAction.NO_OP;
}
throw new IllegalStateException(
"Unsupported redis configure action '" + redisSessionProperties.getConfigureAction() + "'.");
}
@Configuration(proxyBeanMethods = false)
public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {
@Autowired
public void customize(SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
ServerProperties serverProperties) {
Duration timeout = sessionProperties
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout());
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}
setRedisNamespace(redisSessionProperties.getNamespace());
setFlushMode(redisSessionProperties.getFlushMode());
setSaveMode(redisSessionProperties.getSaveMode());
setCleanupCron(redisSessionProperties.getCleanupCron());
}
}
}
- Redis Session 配置加载
// org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration
// 实际用来创建 session 和 管理session的对象
// 这个类 并不能由我们自己去创建, 方法上只加了 @Bean,并没有条件注解,我们能做的处理就是修改 序列化类和在增加 org.springframework.session.config.SessionRepositoryCustomizer 定制化 操作。
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
// 父类(org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration)中初始化 SessionRepositoryFilter,这个会在拦截每一次请求,请求完成之后,都回去提交 session,session过期或创建一个新的session 返回去
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
- 实现了 org.springframework.session.Session 的 RedisSession 和 持有RedisSession 的包装类org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper
// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession
// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper
// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper#getSession(boolean)
@Override
public HttpSessionWrapper getSession(boolean create) {
// 当前请求中的 Attribute 属性中是否保存了session实体,避免与redis的多次交互,增加性能
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
// 通过请求Header Cookie 中 的session id 获取 session,获取到之后转换为包装类,放入 attribute 中
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.markNotNew();
setCurrentSession(currentSession);
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");
}
if (!create) {
return null;
}
if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver
&& this.response.isCommitted()) {
throw new IllegalStateException("Cannot create a session after the response has been committed");
}
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)"));
}
// 创建新的session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
- HttpSessionWrapper 持有 RedisSession ,重点看一下 提交session的方法
// org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper#commitSession
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
}
else {
S session = wrappedSession.getSession();
clearRequestedSessionCache();
// sessionRepository 就是 RedisIndexedSessionRepository
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
// org.springframework.session.data.redis.RedisIndexedSessionRepository#save
@Override
public void save(RedisSession session) {
session.save();
if (session.isNew) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.isNew = false;
}
}
// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession#save
private void save() {
// 该方法主要是看是不是要更换 session ID,
saveChangeSessionId();
// 主要是更新 session 失效时间,根据最后访问时间 lastAccessedTime
saveDelta();
}
// org.springframework.session.data.redis.RedisIndexedSessionRepository.RedisSession#saveDelta
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(this.delta);
// 省略部分代码
......
this.delta = new HashMap<>(this.delta.size());
// 根据在 org.springframework.session.data.redis.RedisIndexedSessionRepository#getSession 中设置的 originalLastAccessTime 去更新缓存失效时间
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
// org.springframework.session.data.redis.RedisSessionExpirationPolicy#onExpirationUpdated
// RedisSessionExpirationPolicy 过期策略,只有这一个实现
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
String keyToExpire = SESSION_EXPIRES_PREFIX + session.getId();
long toExpire = roundUpToNextMinute(expiresInMillis(session));
if (originalExpirationTimeInMilli != null) {
long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
if (toExpire != originalRoundedUp) {
String expireKey = getExpirationKey(originalRoundedUp);
this.redis.boundSetOps(expireKey).remove(keyToExpire);
}
}
long sessionExpireInSeconds = session.getMaxInactiveInterval().getSeconds();
String sessionKey = getSessionKey(keyToExpire);
if (sessionExpireInSeconds < 0) {
this.redis.boundValueOps(sessionKey).append("");
this.redis.boundValueOps(sessionKey).persist();
this.redis.boundHashOps(getSessionKey(session.getId())).persist();
return;
}
String expireKey = getExpirationKey(toExpire);
BoundSetOperations<Object, Object> expireOperations = this.redis.boundSetOps(expireKey);
expireOperations.add(keyToExpire);
long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);
expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
if (sessionExpireInSeconds == 0) {
this.redis.delete(sessionKey);
}
else {
this.redis.boundValueOps(sessionKey).append("");
this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
}
// 刷新 session 失效时间,在原有基础上还加了 5分钟
this.redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
}
结语
到这里,从请求开始 获取 session,到最后刷新session 失效时间,一整个流程就结束了。根据研究以及天马行空的想法,有一个骚操作,记录如下:
/**
* 项目中很多情况下都会有站内信功能,实时刷新的站内信 通常是轮询或者websocket,而在项目中又一般会设置 一定时间内不操作,自动登出(也就是session失效了)。
* 此处的骚操作就是,将这些方法在某次请求中不去刷新缓存失效的时间。
* 例如: 我在项目中设置session过期时间是 30分钟,但是由于我的项目在不停的请求消息列表接口,导致永远不会登出,以下是结合 spring-redis 作为session 的定制。
* 不刷新session的过期时间,可以用来过滤一些 轮询请求的接口
*/
public static void noFlushSessionExpireTime() {
try {
HttpSession session = getHttpServletRequest().getSession();
Field f = session.getClass().getSuperclass().getDeclaredField("session");
f.setAccessible(true);
Session realSession = (Session) f.get(session);
Field delta = realSession.getClass().getDeclaredField("delta");
delta.setAccessible(true);
delta.set(realSession, new HashMap<>());
} catch (Exception e) {
// 捕获抛出的异常,不影响原有的业务逻辑
}
}