Spring-Session 2.1.x中对spring-session-data-redis中分布式会话设计的一些理解
文章目录
Redis存储结构
Session信息存储
存储格式:Hash
键值格式:
{keyNameSpace}sessions:{sessionId}
-> <creationTime, value> <maxInactiveInterval, value> <lastAccessedTime, value> <sessionAttr:{name}, value>
Principal与Session之间的映射
存储结构:Set
键值格式:{keyNameSpace}index:{PRINCIPAL_NAME_INDEX_NAME}:{principalName}
-> [sessionId0, sessionId1...]
同一时刻过期会话集合
该集合为同一分钟中将要过期的会话集合:
expirationMinute - 1 … .expiredAt … .expirationMinute
存储结构:Set
键值格式:{keyNameSpace}expirations:{expirationMinute}
-> [expires:{sessionId0}, expires:{sessionId1}...]
比如:
spring:session:expirations:1586674740000 -> […]
在2020-04-12 14:58:00 ~ 14:59:00 过期的会话列表
该设计值得参考!
会话有效key
理论上,该key存在时,表明session有效;否则,已过期
存储结构:KV
键值格式:{keyNameSpace}sessions:expires:{sessionId}
->""
{keyNameSpace}:index:{}:{sessionId}:{principleName}
会话事件
PUB/SUB
{keyNameSpace}:event:{database}:created:{sessionId}
Keyspace event notifications
__keyevent@{database}__:del
__keyevent@{database}__:expired
RedisSession
关键成员变量:
private final MapSession cached;
private Instant originalLastAccessTime;
private Map<String, Object> delta = new HashMap<>();
private boolean isNew;
private String originalPrincipalName;
private String originalSessionId;
cached–当前访问Session的Java对象表示,用于访问相关属性
originalLastAccessTime–上次访问时间
delta–修改后的属性会在该处缓存
isNew–该Session是否为新创建的Session
originalPrincipalName–principal name
originalSessionId–原Session Id,保存的是一开始初始化时传入的session的id;如果调用了changeSessionId,则该值会在save之后被修改为新生成的sessionId。
RedisSession在更新属性值时,会同时更新delta和cache中的值,如果SaveMode为IMMEDIATE时,RedisSession会在修改属性值之后立即同步至Redis
同步流程:
1、同步sessionId的更新:
a)判断RedisSession是否修改Session Id(判断originalSessionId和cached.getId()是否相等),如果修改,需要更新相关key:更新会话有效key、更新会话信息key
b)同步originalSessionId
2、同步属性值:
a)如果存在principal,需要建立principal的索引
b)同步delta中的属性值
c)触发过期策略中的onExpirationUpdated
RedisOperationsSessionRepository
该类为Session管理的核心类,一般跟SessionRepositoryFilter搭配使用。该类实现了对会话信息的创建、存储、删除、查询。
deleteById
根据会话id删除相关会话信息
流程:
1、清除principal索引信息
2、调用过期策略中的onDelete方法
3、删除会话有效Key
4、将maxInactiveInterval设置为0
5、保存会话
会话过期策略(RedisSessionExpirationPolicy)
onDelete
该方法将该被删除的会话id从同一时刻过期会话集合中移除,因为此方法的调用意味着会话的立即删除,而不需要借助cleanExpiredSessions
onExpirationUpdated
1、根据maxInactiveInterval的值采取操作:
如果maxInactiveInterval < 0,持久化相关会话信息;
如果maxInactiveInterval = 0,立即删除会话有效key;
如果maxInactiveInterval > 0,设置会话有效key,过期时间为(lastAccessTime + maxInactiveInterval) seconds;刷新过期会话集合过期时间(lastAccessTime + maxInactiveInterval + 300) seconds;
2、刷新会话信息过期时间为(lastAccessTime + maxInactiveInterval + 300) seconds;
Note:我们注意到,代码中设置会话信息和过期会话集合的过期时间比原过期时间多5min,这是因为需要保证在实际过期之后还能访问到会话信息,我们需要为后面的处理预留一些时间,比如会话过期事件或者会话删除事件的处理。
cleanExpiredSessions
清除上一分钟的过期会话集合中的所有会话信息。该方法被用于定时任务,用于定时清理一些过期的会话,默认cron = 0 * * * * *
。
SessionRepositoryFilter
该Filter是设置在所有大部分过滤器之前,将request、reponse进行包装,将request
关于session的操作代理至RedisOperationsSessionRepository
类的相关操作上。但是这种方法其实只是实现了会话的共享。但是服务器本身的会话还是存储在本地的(JSESSIONID),这个我觉得就没有太大的必要了。我觉得可以直接借用RedisOperationsSessionRepository
的相关方法去实现一个Tomcat的Manager
,并使用WebServerCutimizer
将其注册至Tomcat中,因此统一会话存储。
@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 {
wrappedRequest.commitSession();
}
}
使用装饰者模式进行功能增强,值得借鉴!