在以前的文章中已经对SpringSession
的功能结构,请求/响应重写等作了介绍。本文将继续来介绍下SpringSession
中存储部分的设计。存储是分布式session
中算是最核心的部分,经过引入三方的存储容器来实现session
的存储,从而有效的解决session
共享的问题。html
SpringSession
存储的顶级抽象接口是org.springframework.session
包下的SessionRepository
这个接口。SessionRepository
的类图结构以下:html5
这里先来看下SessionRepository
这个顶层接口中定义了哪些方法:java
public interface SessionRepository<S extends Session> {
//建立一个session
S createSession();
//保存session
void save(S session);
//经过ID查找session
S findById(String id);
//经过ID删除一个session
void deleteById(String id);
}
复制代码
从代码来看仍是很简单的,就是增删查。下面看具体实现。在2.0版本开始SpringSession
中也提供了一个和SessionRepository
具体相同能力的ReactiveSessionRepository
,用于支持响应式编程模式。git
基于HashMap实现的基于内存存储的存储器实现,这里就主要看下对于接口中几个方法的实现。github
public class MapSessionRepository implements SessionRepository<MapSession> {
private Integer defaultMaxInactiveInterval;
private final Map<String, Session> sessions;
//...
}
复制代码
能够看到就是一个Map
,那后面关于增删查其实就是操做这个Map
了。web
@Override
public MapSession createSession() {
MapSession result = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return result;
}
复制代码
这里很直接,就是new
了一个MapSession
,而后设置了session
的有效期。redis
@Override
public void save(MapSession session) {
if (!session.getId().equals(session.getOriginalId())) {
this.sessions.remove(session.getOriginalId());
}
this.sessions.put(session.getId(), new MapSession(session));
}
复制代码
这里面先判断了session
中的两个ID
,一个originalId
,一个当前id
。originalId
是第一次生成session
对象时建立的,后面都不会在变化。经过源码来看,对于originalId
,只提供了get
方法。对于id
呢,实际上是能够经过changeSessionId
来改变的。spring
这里的这个操做其实是一种优化行为,及时的清除掉老的session
数据来释放内存空间。编程
@Override
public MapSession findById(String id) {
Session saved = this.sessions.get(id);
if (saved == null) {
return null;
}
if (saved.isExpired()) {
deleteById(saved.getId());
return null;
}
return new MapSession(saved);
}
复制代码
这个逻辑也很简单,先从Map
中根据id
取出session
数据,若是没有就返回null
,若是有则再判断下是否过时了,若是过时了就删除掉,而后返回null
。若是查到了,而且没有过时的话,则构建一个MapSession
返回。api
OK,基于内存存储的实现系列就是这些了,下面继续来看其余存储的实现。
FindByIndexNameSessionRepository
继承了SessionRepository
接口,用于扩展对第三方存储的实现。
public interface FindByIndexNameSessionRepository<S extends Session> extends SessionRepository<S> {
String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName()
.concat(".PRINCIPAL_NAME_INDEX_NAME");
Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
default Map<String, S> findByPrincipalName(String principalName) {
return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
}
}
复制代码
FindByIndexNameSessionRepository
添加一个单独的方法为指定用户查询全部会话。这是经过设置名为FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
的Session
的属性值为指定用户的username
来完成的。开发人员有责任确保属性被赋值,由于SpringSession
不会在乎被使用的认证机制。官方文档中给出的例子以下:
String username = "username";
this.session.setAttribute(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
复制代码
FindByIndexNameSessionRepository
的一些实现会提供一些钩子自动的索引其余的session
属性。好比,不少实现都会自动的确保当前的Spring Security
用户名称可经过索引名称FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
进行索引。一旦会话被索引,就能够经过下面的代码检索:
String username = "username";
Map<String, Session> sessionIdToSession =
this.sessionRepository.findByIndexNameAndIndexValue(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,username);
复制代码
下图是FindByIndexNameSessionRepository
接口的三个实现类:
下面来分别分析下这三个存储的实现细节。
RedisOperationsSessionRepository
的类图结构以下,MessageListener
是redis
消息订阅的监听接口。
代码有点长,就不在这里面贴了,一些注释能够在这个 SpringSession中文分支 来看。这里仍是主要来看下对于那几个方法的实现。
这里和MapSessionRepository
的实现基本同样的,那区别就在于Session
的封装模型不同,这里是RedisSession
,实际上RedisSession
的实现是对MapSession
又包了一层。下面会分析RedisSession
这个类。
@Override
public RedisSession createSession() {
// RedisSession,这里和MapSession区别开
RedisSession redisSession = new RedisSession();
if (this.defaultMaxInactiveInterval != null) {
redisSession.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return redisSession;
}
复制代码
在看其余两个方法以前,先来看下RedisSession
这个类。
这个在模型上是对MapSession
的扩展,增长了delta
这个东西。
final class RedisSession implements Session {
// MapSession 实例对象,主要存数据的地方
private final MapSession cached;
// 原始最后访问时间
private Instant originalLastAccessTime;
private Map<String, Object> delta = new HashMap<>();
// 是不是新的session对象
private boolean isNew;
// 原始主名称
private String originalPrincipalName;
// 原始sessionId
private String originalSessionId;
复制代码
delta
是一个Map结构,那么这里面究竟是放什么的呢?具体细节见 saveDelta 这个方法。saveDelta
这个方法会在两个地方被调用,一个是下面要说道的save
方法,另一个是 flushImmediateIfNecessary
这个方法:
private void flushImmediateIfNecessary() {
if (RedisOperationsSessionRepository.this.redisFlushMode == RedisFlushMode.IMMEDIATE) {
saveDelta();
}
}
复制代码
RedisFlushMode
提供了两种推送模式:
save
方法时执行,在web
环境中这样作一般是尽快提交HTTP响应redis
中,不会像ON_SAVE
同样,在最后commit
时一次性写入追踪flushImmediateIfNecessary
方法调用链以下:
save
这个方法,当主动调用
save
时就是将数据推到
redis
中去的,也就是
ON_SAVE
这种状况。那么对于
IMMEDIATE
这种状况,只有调用了上面的四个方法,
SpringSession
才会将数据推送到
redis
。
因此delta
里面存的是当前一些变动的 key-val
键值对象,而这些变动是由setAttribute
、removeAttribute
、setMaxInactiveIntervalInSeconds
、setLastAccessedTime
这四个方法触发的;好比setAttribute(k,v)
,那么这个k->v
就会被保存到delta
里面。
在理解了saveDelta
方法以后再来看save
方法就简单多了。save
对应的就是RedisFlushMode.ON_SAVE
。
@Override
public void save(RedisSession session) {
// 直接调用 saveDelta推数据到redis
session.saveDelta();
if (session.isNew()) {
// sessionCreatedKey->channl
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
// 发布一个消息事件,新增 session,以供 MessageListener 回调处理。
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
复制代码
查询这部分和基于Map
的差异比较大,由于这里并非直接操做Map
,而是与Redis
进行一次交互。
@Override
public RedisSession findById(String id) {
return getSession(id, false);
}
复制代码
调用getSession
方法:
private RedisSession getSession(String id, boolean allowExpired) {
// 根据ID从redis中取出数据
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
//转换成MapSession
MapSession loaded = loadSession(id, entries);
if (!allowExpired && loaded.isExpired()) {
return null;
}
//转换成RedisSession
RedisSession result = new RedisSession(loaded);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
复制代码
loadSession
中构建MapSession
:
private MapSession loadSession(String id, Map<Object, Object> entries) {
// 生成MapSession实例
MapSession loaded = new MapSession(id);
//遍历数据
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
String key = (String) entry.getKey();
if (CREATION_TIME_ATTR.equals(key)) {
// 设置建立时间
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (MAX_INACTIVE_ATTR.equals(key)) {
// 设置最大有效时间
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
}
else if (LAST_ACCESSED_ATTR.equals(key)) {
// 设置最后访问时间
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (key.startsWith(SESSION_ATTR_PREFIX)) {
// 设置属性
loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()),
entry.getValue());
}
}
return loaded;
}
复制代码
根据sessionId
删除session
数据。具体过程看代码注释。
@Override
public void deleteById(String sessionId) {
// 获取 RedisSession
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
// 清楚当前session数据的索引
cleanupPrincipalIndex(session);
//执行删除操做
this.expirationPolicy.onDelete(session);
String expireKey = getExpiredKey(session.getId());
//删除expireKey
this.sessionRedisOperations.delete(expireKey);
//session有效期设置为0
session.setMaxInactiveInterval(Duration.ZERO);
save(session);
}
复制代码
最后来看下这个订阅回调处理。这里看下核心的一段逻辑:
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
// Deleted 仍是 Expired ?
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
// 此处省略无关代码
// Deleted
if (isDeleted) {
// 发布一个 SessionDeletedEvent 事件
handleDeleted(session);
}
// Expired
else {
// 发布一个 SessionExpiredEvent 事件
handleExpired(session);
}
}
复制代码
首先按照咱们本身常规的思路来设计的话,咱们会怎么来考虑这个事情。这里首先要声明下,我对 Redis
这个东西不是很熟,没有作过深刻的研究;那若是是我来作,可能也就仅仅限于存储。
findByIndexNameAndIndexValue
的设计,这个的做用是经过indexName
和indexValue
来返回当前用户的全部会话。可是这里须要考虑的一个事情是,一般状况下,一个用户只会关联到一个会话上面去,那这种设计很显然,个人理解是为了支持单用户多会话的场景。
MessageListener
接口,增长事件通知能力。经过监听这些事件,能够作一些session
操做管控。可是实际上 SpringSession
中并无作任何事情,从代码来看,publishEvent
方法是空实现。等待回复中 #issue 1287private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
@Override
public void publishEvent(ApplicationEvent event) {
}
@Override
public void publishEvent(Object event) {
}
};
复制代码
RedisFlushMode
,SpringSession
中提供了两种模式的推送,一种是ON_SAVE
,另一种是IMMEDIATE
。默认是ON_SAVE
,也就是常规的在请求处理结束时进行一次sessionCommit
操做。RedisFlushMode
的设计感受是为session
数据持久化的时机提供了另一种思路。存储机制设计部分就一基于内存和基于Redis
两种来分析;另外基于jdbc
和hazelcast
有兴趣的同窗能够本身查看源码。
最后也欢迎访问个人我的博客:www.glmapper.com