Серия SpringSession — Redis и карта механизма хранения

Spring

В предыдущих статьях уже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Три класса реализации интерфейса:

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

Ссылаться на