В предыдущих статьях ужеSpringSession
Представлена функциональная структура, переписывание запроса/ответа и т.д. Эта статья будет продолжать знакомитьSpringSession
Оформление секции хранения. хранилище распределеноsession
Это основная часть, которая реализуется за счет введения трехсторонних контейнеров для хранения.session
хранения, чтобы эффективно решатьsession
общая проблема.
1. Абстрактный интерфейс верхнего уровня хранилища SpringSession
SpringSession
Абстрактный интерфейс верхнего уровня для храненияorg.springframework.session
упакованныйSessionRepository
этот интерфейс.SessionRepository
Структура диаграммы классов выглядит следующим образом:
Давайте посмотрим здесьSessionRepository
Какие методы определены в этом интерфейсе верхнего уровня:
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.0SpringSession
также обеспечивает иSessionRepository
той же способностиReactiveSessionRepository
, для поддержки шаблонов реактивного программирования.
2. MapSessionRepository
Реализация памяти на основе хранилища памяти на основе реализации HashMap, здесь мы в основном рассматриваем реализацию нескольких методов в интерфейсе.
public class MapSessionRepository implements SessionRepository<MapSession> {
private Integer defaultMaxInactiveInterval;
private final Map<String, Session> sessions;
//...
}
Видно, чтоMap
, то последующая проверка добавления и удаления фактически является операцией этогоMap
.
createSession
@Override
public MapSession createSession() {
MapSession result = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveInterval(
Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return result;
}
здесь все простоnew
взял одинMapSession
, а затем установитеsession
срок годности.
save
@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
изменить.
Эта операция здесь на самом деле является поведением оптимизации, а старые вовремя удаляются.session
данные, чтобы освободить место в памяти.
findById
@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
вернуть.
Хорошо, это серия реализаций, основанных на хранении в памяти, давайте продолжим смотреть на реализацию других хранилищ.
3. FindByIndexNameSessionRepository
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
Три класса реализации интерфейса:
Давайте проанализируем детали реализации этих трех хранилищ по отдельности.
3.1 RedisOperationsSessionRepository
RedisOperationsSessionRepository
Структура диаграммы классов выглядит следующим образом:MessageListener
даredis
Интерфейс прослушивателя для подписки на сообщения.
Код длинноват, поэтому я не буду его здесь выкладывать, некоторые комментарии можно найти здесьКитайский филиал SpringSessionПриди и посмотри. Здесь мы в основном рассматриваем реализацию этих методов.
3.1.1 createSession
здесь и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
этот класс.
3.1.2 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
Это структура карты, так что же в ней содержится? Подробнее см.saveDeltaСюда.saveDelta
Этот метод будет вызываться в двух местах, одно из которых описано ниже.save
метод, другойflushImmediateIfNecessary
Сюда:
private void flushImmediateIfNecessary() {
if (RedisOperationsSessionRepository.this.redisFlushMode == RedisFlushMode.IMMEDIATE) {
saveDelta();
}
}
RedisFlushMode
Предусмотрено два режима push:
- ON_SAVE: только при звонке
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
в.
3.1.3 save
в понимании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);
}
}
3.1.4 findById
Запросите эту часть и на основе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;
}
3.1.5 deleteById
согласно с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);
}
3.1.6 onMessage
Наконец, давайте взглянем на обработку обратного вызова подписки. Вот посмотрите на основную логику:
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);
}
}
3.2 Некоторые мысли о хранилище Redis
Прежде всего, если мы проектируем в соответствии с нашим собственным традиционным мышлением, как мы будем рассматривать этот вопрос. Прежде всего, я хотел бы заявить, что яRedis
Эта вещь не очень знакома, и я не проводил глубоких исследований, если я это сделаю, это может быть ограничено только хранилищем.
-
findByIndexNameAndIndexValue
дизайн, эффект от этого черезindexName
а такжеindexValue
чтобы вернуть все сеансы текущего пользователя. Но здесь следует учитывать одну вещь: обычно пользователь будет связан только с одним сеансом.Очевидно, что я понимаю поддержку однопользовательского сценария с несколькими сеансами.- indexName: FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME
- indexValue: имя пользователя
- выполнить
MessageListener
интерфейс для увеличения возможностей уведомления о событиях. Слушая эти события, вы можете сделать некоторыеsession
Оперативный контроль. Но по фактуSpringSession
ничего не делает в коде, из кода,publishEvent
метод является пустой реализацией. жду ответа#issue 1287
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
@Override
public void publishEvent(ApplicationEvent event) {
}
@Override
public void publishEvent(Object event) {
}
};
-
RedisFlushMode
,SpringSession
В , есть два режима push , один из нихON_SAVE
, другойIMMEDIATE
. По умолчаниюON_SAVE
, то есть обычно выполняется один раз в конце обработки запросаsessionCommit
работать.RedisFlushMode
Дизайн похож наsession
Время сохраняемости данных дает еще один способ мышления.
резюме
Часть конструкции механизма хранения основана на памяти и основана наRedis
два для анализа; другой основан наjdbc
а такжеhazelcast
Заинтересованные студенты могут просмотреть исходный код самостоятельно.
Наконец, добро пожаловать в мой личный блог:www.glmapper.com