# 概述
# springSession解决的问题
传统单机Web应用,用户的session由容器进行管理。当网站逐渐演变,分布式应用和集群开始成为主流,传统的web容器管理用户会话session的方式行不通;
springSession应运而生;
# springSession设计的核心思想
- 将session从web容器剥离;
- 与servlet规范无缝结合,仍然使用HttpServletRequest获取session,获取到的仍然是HttpSession类型(适配器模式)
- Session不再存储在web容器内,外化存储(装饰器模式)
# 源码
# springSession装配流程设计
@Import(SpringHttpSessionConfiguration.class)
public @interface EnableSpringHttpSession {
}
@Configuration(proxyBeanMethods = false)
/*xxx: spring-session配置 */
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
private List<HttpSessionListener> httpSessionListeners = new ArrayList<>();
/*xxx: 默认从cookie中解析解析sessionId*/
private CookieHttpSessionIdResolver defaultHttpSessionIdResolver = new CookieHttpSessionIdResolver();
/*xxx: sessionId的解析器*/
private HttpSessionIdResolver httpSessionIdResolver = this.defaultHttpSessionIdResolver;
@Bean
/*xxx: session存储器过滤器,
当前的过滤器,设置了过滤器的顺序,默认为 Integer.min_value+50 */
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
@Bean
/*xxx: session事件监听适配器 */
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
}
@Order(Integer.MIN_VALUE + 50)
/*xxx: session存储器过滤器*/
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
/*xxx: session外置存储器*/
private final SessionRepository<S> sessionRepository;
public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
/*xxx: 将当前的 request,response 请求对象进行包装*/
/*xxx: 包装之后,主要是覆写了 获取session 的逻辑*/
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
/*xxx: 在当前过滤器执行完成后,会自动持久化session信息,通过各自的 session存储策略 */
wrappedRequest.commitSession();
}
}
}
# springSession获取session流程
/*xxx: 通过sessionRepository进行管理session的请求包装器*/
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
@Override
/*xxx: 覆写了 getSession()方法,改变了 session的生成逻辑*/
public HttpSessionWrapper getSession() {
return getSession(true);
}
@Override
/*xxx: 覆写了 getSession(boolean)方法*/
/*xxx: 默认情况下,它是 通过 servlet规范的 Manager 进行操作的*/
public HttpSessionWrapper getSession(boolean create) {
/*xxx: 获取当前的session,通过 attribute获取: SessionRepository.CURRENT_SESSION*/
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
/*xxx: 从请求中 获取 session信息*/
S requestedSession = getRequestedSession();
/*xxx: 获取到session后,需要验证其合法性*/
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
/*xxx: 刷新session时间*/
}
}else{
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
/*xxx: 本次是否创建*/
if (!create) {
return null;
}
/*xxx: 通过sessionRepository创建session */
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
/*xxx: 设置好相应的信息后,返回新创建的session实例*/
return currentSession;
}
}
# springSession保存session流程
/*xxx: session存储器过滤器*/
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
/*xxx: 将当前的 request,response 请求对象进行包装*/
/*xxx: 包装之后,主要是覆写了 获取session 的逻辑*/
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
/*xxx: 在当前过滤器执行完成后,会自动持久化session信息,通过各自的 session存储策略 */
wrappedRequest.commitSession();
}
}
}
/*xxx: 通过sessionRepository进行管理session的请求包装器*/
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
/*xxx: 该方法会在过滤器执行完成后,处理session的状态*/
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
S session = wrappedSession.getSession();
/*xxx: 保存session至外置存储器中*/
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
/*xxx: 当请求的sessionId非法,或者请求的sessionId 与当前的sessionId不一致,需要重写cookie*/
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
public interface HttpSessionIdResolver {
/*xxx: 解析sessionId*/
List<String> resolveSessionIds(HttpServletRequest request);
/*xxx: 设置sessionId*/
void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId);
/*xxx:sessionId失效*/
void expireSession(HttpServletRequest request, HttpServletResponse response);
}
/*xxx: cookieHttpSession解析器,默认依赖于cookie序列化器实现*/
public final class CookieHttpSessionIdResolver implements HttpSessionIdResolver {
@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId));
}
}
# springSession读写cookie
/*xxx: 默认的cookie序列化器*/
public class DefaultCookieSerializer implements CookieSerializer {
private boolean useBase64Encoding = true;
@Override
/*xxx: 写入cookie */
public void writeCookieValue(CookieValue cookieValue) {
StringBuilder sb = new StringBuilder();
sb.append(this.cookieName).append('=');
String value = getValue(cookieValue);
/*xxx: 省略其它抽象...*/
}
private String getValue(CookieValue cookieValue) {
String requestedCookieValue = cookieValue.getCookieValue();
String actualCookieValue = requestedCookieValue;
/*xxx: 原生容器下的 session,没有这个限制 */
if (this.useBase64Encoding) {
actualCookieValue = base64Encode(actualCookieValue);
}
return actualCookieValue;
}
@Override
/*xxx: 从cookie中,读取sessionId */
public List<String> readCookieValues(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
/*xxx: 注意,有可能匹配到多个 sessionId*/
List<String> matchingCookieValues = new ArrayList<>();
if (cookies != null) {
for (Cookie cookie : cookies) {
/*xxx: 如果Cookie的名称为 SESSION*/
if (this.cookieName.equals(cookie.getName())) {
/*xxx: 则将其值进行 base解码,作为sessionId*/
String sessionId = (this.useBase64Encoding ? base64Decode(cookie.getValue()) : cookie.getValue());
matchingCookieValues.add(sessionId);
}
}
return matchingCookieValues;
}
}
}
# 传统容器获取session流程(比对)
//package org.apache.catalina.connector;
public class Request implements HttpServletRequest {
protected Session session = null;
@Override
public HttpSession getSession(boolean create) {
Session session = doGetSession(create);
return session.getSession();
}
protected Session doGetSession(boolean create) {
/*xxx: 如果之前已经获取过session,并且session是合法的,则直接返回*/
if (session != null) {
return session;
}
/*xxx: StandardManager实例是从 context中获取的,换言之,session是存在内存中的,并且以Context为单位进行隔离*/
Manager manager = context.getManager();
if (requestedSessionId != null) {
/*xxx: 获取session,实际上是根据 coyoteAdaptor阶段,恢复的sessionId,通过该Session去 Manager中,寻找与之映射的 session实例 */
session = manager.findSession(requestedSessionId);
if (session != null) {
/*xxx: 刷新session的最近访问时间*/
session.access();
return session;
}
}
/*xxx: 本次是否创建session*/
if (!create) {
return null;
}
String sessionId = getRequestedSessionId();
/*xxx: session的创建,最终是通过 session管理器完成的。 */
session = manager.createSession(sessionId);
/*xxx: 当创建session成功后,同时会将当前的session,写入到 response头部中 */
if (session != null && trackModesIncludesCookie) {
Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
session.access();
return session;
}
}
# springSession外置存储-逻辑设计
/*xxx: 抽象session*/
public interface Session {
String getId();
<T> T getAttribute(String attributeName);
void setAttribute(String attributeName, Object attributeValue);
Instant getCreationTime();
void setLastAccessedTime(Instant lastAccessedTime);
/*xxx: session有效时长*/
void setMaxInactiveInterval(Duration interval);
boolean isExpired();
}
/*xxx: session映射,默认包含一个 id */
/*xxx: 注意,该类是最终类,不可再被扩展*/
public final class MapSession implements Session, Serializable {
/*xxx: 默认时长为30分钟*/
private Duration maxInactiveInterval = Duration.ofSeconds(1800);
public MapSession() {
this(generateId());
}
public MapSession(String id) {
this.id = id;
this.originalId = id;
}
/*xxx: 生成 sessionId,默认通过uuid实现*/
private static String generateId() {
return UUID.randomUUID().toString();
}
}
/*xxx: jdbc技术实现的session*/
final class JdbcSession implements Session {
private final Session delegate;
private void save() {
/*xxx: 如果是新增session信息,则进行保存*/
if (this.isNew) {
JdbcIndexedSessionRepository.this.transactionOperations.executeWithoutResult((status) -> {
/*xxx: 执行两条语句:*/
/*xxx: INSERT INTO %TABLE_NAME%(PRIMARY_ID, SESSION_ID, CREATION_TIME, LAST_ACCESS_TIME, MAX_INACTIVE_INTERVAL, EXPIRY_TIME, PRINCIPAL_NAME) VALUES (?, ?, ?, ?, ?, ?, ?)*/
/*xxx: 注: 最后一个字段: principal_name为 从session中获取的 "org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME"属性的值,或者是 spring-security-context的authentication的name*/
/*xxx: INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) SELECT PRIMARY_ID, ?, ? FROM %TABLE_NAME% WHERE SESSION_ID = ?*/
});
}else{
/*xxx: 更新session,并对属性做 增删改操作*/
/*xxx: UPDATE %TABLE_NAME% SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? WHERE PRIMARY_ID = ?*/
/*xxx: INSERT INTO %TABLE_NAME%_ATTRIBUTES(SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES) SELECT PRIMARY_ID, ?, ? FROM %TABLE_NAME% WHERE SESSION_ID = ?*/
/*xxx: UPDATE %TABLE_NAME%_ATTRIBUTES SET ATTRIBUTE_BYTES = ? WHERE SESSION_PRIMARY_ID = ? AND ATTRIBUTE_NAME = ?*/
/*xxx: DELETE FROM %TABLE_NAME%_ATTRIBUTES WHERE SESSION_PRIMARY_ID = ? AND ATTRIBUTE_NAME = ?*/
}
}
private void flushIfRequired() {
if (JdbcIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
}
final class RedisSession implements Session {
private final MapSession cached;
private Map<String, Object> delta = new HashMap<>();
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
/*xxx: 省略其它抽象...*/
}
private void flushImmediateIfNecessary() {
if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
private void save() {
/*xxx: 保存 sessionId,以及 所有的 attributes */
/*xxx: 在这个操作过程中,会将 信息 保存至 redis*/
saveChangeSessionId();
saveDelta();
}
private void saveChangeSessionId() {
/*xxx: 执行更新操作*/
if (!this.isNew) {
/*xxx: 修改session键名: 即修改 spring:session:sessions:当前的sessionId*/
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
/*xxx: 修改过期键: spring:session:sessions:expires:当前的sessionId*/
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
}
}
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
/*xxx: 保存属性时,会将所有的信息保存到 hash类型下, 键名为 namespace + sessionId */
getSessionBoundHashOperations(sessionId).putAll(this.delta);
/*xxx: 存储 principalRedisKey 键 spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:当前的principalName名称*/
/*xxx: 设置失效时间...*/
/*xxx: 设置了三个失效时间,以30分钟为例*/
/*xxx: sessionExpire为30分钟自动删除, expirations,session 将在35分钟自动删除*/
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
/*xxx: 存储过期时长的键: spring:session:expirations:有效时长, 这个属性是用于通过 定时器进行清除session */
}
}
/*xxx: session存储器,主要包括session的增、删、查、创建 操作*/
public interface SessionRepository<S extends Session> {
/*xxx: 创建 session*/
S createSession();
/*xxx: 保存session */
void save(S session);
/*xxx: 通过id查找session */
S findById(String id);
/*xxx: 通过id 删除session */
void deleteById(String id);
}
/*xxx: 通过索引名查找的session仓库*/
public interface FindByIndexNameSessionRepository<S extends Session> extends SessionRepository<S> {
/*xxx: 通过索引名,以及索引值进行查找 */
Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
default Map<String, S> findByPrincipalName(String principalName) {
return findByIndexNameAndIndexValue("org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME", principalName);
}
}
/*xxx: jdbc实现的 session仓库 */
/*xxx: 涉及到的表包括: SPRING_SESSION,SPRING_SESSION_ATTRIBUTES*/
public class JdbcIndexedSessionRepository
implements FindByIndexNameSessionRepository<JdbcIndexedSessionRepository.JdbcSession> {
@Override
/*xxx: jdbc实现的 session仓库,
创建session实例 */
public JdbcSession createSession() {
/*xxx: 首先 创建 mapSession实例*/
MapSession delegate = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
delegate.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
/*xxx: 通过 mapSession 创建 jdbcSession实例 */
JdbcSession session = new JdbcSession(delegate, UUID.randomUUID().toString(), true);
session.flushIfRequired();
return session;
}
@Override
/*xxx: 保存 jdbcSession*/
public void save(final JdbcSession session) {
/*xxx: 调用 jdbcSession进行保存 */
session.save();
}
@Override
public JdbcSession findById(final String id) {
/*xxx: 执行sql查询*/
/*xxx: SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES FROM %TABLE_NAME% S LEFT OUTER JOIN %TABLE_NAME%_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID WHERE S.SESSION_ID = ?*/
}
@Override
/*xxx: 失效后的session剔除*/
public void deleteById(final String id) {
/*xxx: 执行sql更新*/
/*xxx: DELETE FROM %TABLE_NAME% WHERE SESSION_ID = ?*/
}
}
/*xxx: redis实现的 session仓库*/
public class RedisIndexedSessionRepository
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
@Override
/*xxx: 保存 RedisSession */
public void save(RedisSession session) {
session.save();
/*xxx: 如果session是新建的,会同时发布session创建事件,集群下的其它节点,会收到该事件的广播*/
if (session.isNew) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
/*xxx: 发布session创建事件*/
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.isNew = false;
}
}
@Override
public RedisSession findById(String id) {
return getSession(id, false);
}
/*xxx: 获取session,从redis中加载属性 */
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
MapSession loaded = loadSession(id, entries);
RedisSession result = new RedisSession(loaded, false);
return result;
}
private MapSession loadSession(String id, Map<Object, Object> entries) {
MapSession loaded = new MapSession(id);
/*xxx: 将entries的属性进行设置*/
return loaded;
}
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getSessionKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
@Override
public void deleteById(String sessionId) {
RedisSession session = getSession(sessionId, true);
cleanupPrincipalIndex(session);
this.expirationPolicy.onDelete(session);
String expireKey = getExpiredKey(session.getId());
this.sessionRedisOperations.delete(expireKey);
session.setMaxInactiveInterval(Duration.ZERO);
/*xxx: 最后保存了一个空的session*/
save(session);
}
}
# redisSession失效机制
# 通过定时任务剔除(属于定期删除,下文有介绍)
/*xxx: redisSession 的配置*/
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
private final RedisIndexedSessionRepository sessionRepository;
/*xxx: 每分钟执行一次清理*/
private String cleanupCron = "0 * * * * *";
@EnableScheduling
@Configuration(proxyBeanMethods = false)
class SessionCleanupConfiguration implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
/*xxx: 定期清理的调度任务*/
taskRegistrar.addCronTask(this.sessionRepository::cleanupExpiredSessions,
RedisHttpSessionConfiguration.this.cleanupCron);
}
}
}
# 通过redis本身机制自动剔除
final class RedisSessionExpirationPolicy {
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
/*xxx: expireKey: spring:session:expirations:有效时长*/
String expireKey = getExpirationKey(toExpire);
BoundSetOperations<Object, Object> expireOperations = this.redis.boundSetOps(expireKey);
expireOperations.add(keyToExpire);
/*xxx: 默认将在 sessionExpire失效后的五分钟才失效*/
expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
/*xxx: session本身,也将在 sessionExpire失效后的五分钟,才失效*/
this.redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
/*xxx: 此时sessionKey表示的是: spring:session:sessions:expires:当前的sessionId*/
/*xxx: sessionExpire将会在设置的时间失效 */
this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
}
}
# 对于session状态变更的监听
@Configuration(proxyBeanMethods = false)
/*xxx: redisSession 的配置*/
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
@Bean
/*xxx: 定义 基于 redis的消息监听器*/
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
/*xxx: 订阅 __keyevent@0__:del, __keyevent@0__:expired, spring:session:event:0:created:* 事件 */
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
return container;
}
}
/*xxx: redis实现的 session仓库*/
public class RedisIndexedSessionRepository
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
@Override
/*xxx: 监听事件*/
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel());
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer.deserialize(message.getBody());
/*xxx: 处理session创建的事件*/
handleCreated(loaded, channel);
return;
}
/*xxx: session过期,或者 session被删除,需要执行的动作*/
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
/*xxx: 当 redis的sessionExpire 删除后,需要通知应用程序*/
/*xxx: 正常情况下,此时还是能获取到session信息的*/
RedisSession session = getSession(sessionId, true);
cleanupPrincipalIndex(session);
if (isDeleted) {
/*xxx: 处理session删除*/
handleDeleted(session);
}
else {
/*xxx: 处理session过期*/
handleExpired(session);
}
}
}
private void handleCreated(Map<Object, Object> loaded, String channel) {
String id = channel.substring(channel.lastIndexOf(":") + 1);
Session session = loadSession(id, loaded);
/*xxx: 广播session创建事件*/
publishEvent(new SessionCreatedEvent(this, session));
}
private void handleDeleted(RedisSession session) {
/*xxx: 广播session删除事件*/
publishEvent(new SessionDeletedEvent(this, session));
}
private void handleExpired(RedisSession session) {
/*xxx: 广播session过期事件*/
publishEvent(new SessionExpiredEvent(this, session));
}
}
# session的更新模式
- session的更新模式分为两种立即更新,以及保存时更新
public enum FlushMode {
ON_SAVE,
/*xxx: 有关session的任何变动,都会立即保存到外置存储*/
IMMEDIATE
}
# redisSession专题
# redisSession的四个key
class Demo{
/*xxx: sessionKey: 存储session本身,java表现为Map,redis表现为hash*/
/*xxx: 该键本身也存在过期时间, 为 expires的时间 + 5分钟*/
String getSessionKey(String sessionId) {
return this.namespace + "sessions:" + sessionId;
}
/*xxx: principalKey: 存储principal -> sessionId的映射关系,属于控制同用户数登录个数的参数,java表现为集合,redis表现为set*/
String getPrincipalKey(String principalName) {
return this.namespace + "index:" + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + ":"
+ principalName;
}
/*xxx: expirationKey: 存储 时间戳(以分钟为单位) -> sessionId的映射关系, 用于定时器清除, 一个时间戳可能对应多个sessionId 定时器默认每分钟扫描一起(如果开了的话)*/
/*xxx: 该键本身也存在过期时间, 为 expires的时间 + 5分钟*/
String getExpirationsKey(long expiration) {
return this.namespace + "expirations:" + expiration;
}
/*xxx: expiresKey: session的实际过期时间,未配置的情况下,默认为 30分钟。 也就是实际配置的session过期时间*/
private String getExpiredKey(String sessionId) {
return getExpiredKeyPrefix() + sessionId;
}
}
sessionKey
- 存储session本身,java表现为Map,redis表现为hash
- 该键本身也存在过期时间, 为 expires的时间 + 5分钟
principalKey
- 存储principal -> sessionId的映射关系,属于控制同用户数登录个数的参数,java表现为集合,redis表现为set
expirationKey
- 存储 时间戳(以分钟为单位) -> sessionId的映射关系, 用于定时器清除, 一个时间戳可能对应多个sessionId 定时器默认每分钟扫描一起(如果开了的话)
- 该键本身也存在过期时间, 为 expires的时间 + 5分钟
expiresKey
- session的实际过期时间,未配置的情况下,默认为 30分钟。 也就是实际配置的session过期时间
- 配置该参数的名称为:maxInactiveInterval,也就是这个 间隔 时间段没有被访问,则认为会话失效
# redisSession的key创建过程(常用三个)
/*xxx: redis实现的 session仓库*/
public class RedisIndexedSessionRepository
implements FindByIndexNameSessionRepository<RedisIndexedSessionRepository.RedisSession>, MessageListener {
@Override
/*xxx: 保存 RedisSession */
public void save(RedisSession session) {
session.save();
/*xxx: 如果session是新建的,会同时发布session创建事件,集群下的其它节点,会收到该事件的广播*/
if (session.isNew) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
/*xxx: 发布session创建事件*/
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.isNew = false;
}
}
final class RedisSession implements Session {
private void save() {
/*xxx: 保存 sessionId,以及 所有的 attributes */
/*xxx: 在这个操作过程中,会将 信息 保存至 redis*/
saveChangeSessionId();
saveDelta();
}
private void saveDelta() {
//....
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
//...
//xxx:如果session中,存在 org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME这个键名,则需要进行并发控制
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
//xxx: 并发控制键的写入
String principalRedisKey = getPrincipalKey(principal);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
.add(sessionId);
}
}
//xxx: 过期时间设置: 上次访问时间+有效时间间隔 即为过期时间
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
/*xxx: 进行过期时间设置*/
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
}
}
/*xxx: redis超时策略类*/
final class RedisSessionExpirationPolicy {
void onExpirationUpdated(Long originalExpirationTimeInMilli, Session session) {
//xxx: 这个超时的key,没有用统一的前缀,离谱
String keyToExpire = "expires:" + session.getId();
//xxx: 将需要超时的时间,转为分钟(时间戳)
long toExpire = roundUpToNextMinute(expiresInMillis(session));
//... 一个容错处理,略
/*xxx: maxInactiveInterval 代表超时时间,也就是这个 间隔 时间段没有被访问,则认为会话失效 */
long sessionExpireInSeconds = session.getMaxInactiveInterval().getSeconds();
/*xxx: 此处是生成 expires的方法, 它没有用 统一的生成key的方法...,名字也起的模糊 离谱 */
String actualExpiresKey = getSessionKey(keyToExpire);
/*xxx: 如果超时时间设置为小于0,则session永不失效,则对其进行持久化*/
if (sessionExpireInSeconds < 0) {
this.redis.boundValueOps(actualExpiresKey).append("");
this.redis.boundValueOps(actualExpiresKey).persist();
this.redis.boundHashOps(getSessionKey(session.getId())).persist();
return;
}
/*xxx: 将expiresKey 加入到 expirationKey 集合里面 */
String expirationKey = getExpirationKey(toExpire);
BoundSetOperations<Object, Object> expireOperations = this.redis.boundSetOps(expirationKey);
expireOperations.add(keyToExpire);
long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5);
/*xxx: expiration 过期 时间为 实际过期的五分钟之后 */
expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
//xxx: 如果超时时间设置为0 ,说明 不存储session
if (sessionExpireInSeconds == 0) {
this.redis.delete(actualExpiresKey);
}
else {
this.redis.boundValueOps(actualExpiresKey).append("");
/*xxx: 如果超时时间大于0 ,则以该时间作为超时时间 */
this.redis.boundValueOps(actualExpiresKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS);
}
/*xxx: 实际session的过期时间,在该键过期时间五分钟之后*/
this.redis.boundHashOps(getSessionKey(session.getId())).expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
}
}
- 附: principal并发控制键的说明(即springSession与SpringSecurity集成时,都会有该属性)
public class PrincipalNameIndexResolver<S extends Session> extends SingleIndexResolver<S> {
public String resolveIndexValueFor(S session) {
//xxx: 获取principal的策略,如果 键: org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME有值,则直接返回
String principalName = session.getAttribute(getIndexName());
if (principalName != null) {
return principalName;
}
//xxx: 否则,从springSecurity的安全上下文中,获取
Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT);
if (authentication != null) {
//xxx: 具体就是获取 securityContext.authentication.getName(); 该方法绝大部分时候,就是获取 userDetails的userName
return expression.getValue(authentication, String.class);
}
return null;
}
}
# redisSession的失效机制
redis过期键有三种删除策略(这是对redis本身而言);redisSession采用惰性删除 和 定期删除 的策略
- 定时删除
- 通过定时器,过期马上删除。
- 在设置某个key 的过期时间同时,为每个设置过期时间的key都创造一个定时器;当key过期时间到达时,由定时器任务立即执行对键的删除操作
- 最有效,但最浪费CPU
- 惰性删除
- 访问的时候判断是否过期
- 对cpu友好,对内存不友好
- 定期删除
- 每隔一段时间删除过期键,同时限制每次操作的执行时常和频率
- 是一种折中方案
# 存储三个键,以及失效时间不一致的原因
- 业务系统可能会在session失效后做业务逻辑处理,如果只有一个session键,则redis删除后,无法再获取session信息;
- expires键最先过期,其过期后会发布过期事件,其他订阅了过期事件的节点,依然能获取到session信息
# 不可靠的清理,定时任务兜底
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware {
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
private String cleanupCron = DEFAULT_CLEANUP_CRON;
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
@EnableScheduling
@Configuration(proxyBeanMethods = false)
/*xxx: redisSession自动带有该特性*/
class SessionCleanupConfiguration implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
/*xxx: 定期清理的调度任务*/
taskRegistrar.addCronTask(this.sessionRepository:: cleanupExpiredSessions,
RedisHttpSessionConfiguration.this.cleanupCron);
}
}
}
/*xxx: redis超时策略类*/
final class RedisSessionExpirationPolicy {
void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);
//xxx: 从expirationKey中获取,即某个时间点,需要过去的键 信息
String expirationKey = getExpirationKey(prevMin);
Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
//xxx: 将键本身删除
this.redis.delete(expirationKey);
for (Object session : sessionsToExpire) {
//xxx: 主动去获取一遍相应的键,包括expires,此时expires将会失效,触发后续流程
String sessionKey = getSessionKey((String) session);
touch(sessionKey);
}
}
private void touch(String key) {
this.redis.hasKey(key);
}
}
# redisSession过期时间更新机制
- 只要程序内部调用了getSession方法,都会触发更新机制
- 更新机制会更新
lastAccessTime
,以及所有键的TTL时间
# 时间更新机制源码
/*xxx: 通过sessionRepository进行管理session的请求包装器*/
class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
@Override
/*xxx: 覆写了 getSession(boolean)方法*/
/*xxx: 默认情况下,它是 通过 servlet规范的 Manager 进行操作的*/
public HttpSessionWrapper getSession(boolean create) {
/*xxx: 获取当前的session,通过 attribute获取: SessionRepository.CURRENT_SESSION*/
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
/*xxx: 从请求中 获取 session信息*/
S requestedSession = getRequestedSession();
/*xxx: 获取到session后,需要验证其合法性*/
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;
}
}
//省略其它抽象...
return null;
}
private void setCurrentSession(HttpSessionWrapper currentSession) {
if (currentSession == null) {
removeAttribute(CURRENT_SESSION_ATTR);
}
else {
setAttribute(CURRENT_SESSION_ATTR, currentSession);
}
}
}
class SessionRepositoryResponseWrapper extends OnCommittedResponseWrapper {
@Override
/*xxx: responseCommit后,会处理session状态*/
protected void onResponseCommitted() {
this.request.commitSession();
}
class request{
/*xxx: 该方法会在过滤器执行完成后,处理session的状态*/
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
}
else {
S session = wrappedSession.getSession();
clearRequestedSessionCache();
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
}
}
class RedisSession implements Session {
private void save() {
/*xxx: 保存 sessionId,以及 所有的 attributes */
/*xxx: 在这个操作过程中,会将 信息 保存至 redis*/
saveChangeSessionId();
saveDelta();
}
private void saveDelta() {
/*xxx: 进行过期时间设置,会更新所有的键的ttl,以及lastAccessTime*/
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
}
# 特别说明
- 当springSession与springSecurity结合时,同一个会话的任何的网页(存在sessionId)都会刷新session更新时长
- 这将导致,任意一个轮询接口,都将导致session长期生效