The functional structure of SpringSession, request/response rewriting, and so on have been covered in previous articles. This article continues with the design of the storage portion of the SpringSession. Storage is the core part of distributed session. It can effectively solve the problem of session sharing by introducing storage containers of three parties to realize session storage.

1. The top-level abstract interface for SpringSession storage

SpringSession org is top abstract interface of the storage, springframework. Session package SessionRepository under this interface. The structure of the SessionRepository class diagram is as follows:

Let’s take a look at the methods defined in the SessionRepository interface:

public interface SessionRepository<S extends Session> {
    // Create a session
	S createSession(a);
	/ / save the session
	void save(S session);
	// Find session by ID
	S findById(String id);
	// Delete a session by ID
	void deleteById(String id);
}
Copy the code

From the code or very simple, is to add and delete check. Let’s look at the implementation. In version 2.0 started SpringSession also provides a specific same ability and SessionRepository ReactiveSessionRepository, used to support the response programming model.

2, MapSessionRepository

Based on HashMap implementation based on memory storage memory implementation, here is mainly to see the implementation of several methods in the interface.

public class MapSessionRepository implements SessionRepository<MapSession> {
	private Integer defaultMaxInactiveInterval;
	private final Map<String, Session> sessions;
	/ /...
}
Copy the code

It can be seen that it is a Map, and then the operation on the add, delete and check is actually the Map.

createSession

@Override
public MapSession createSession(a) {
	MapSession result = new MapSession();
	if (this.defaultMaxInactiveInterval ! =null) {
		result.setMaxInactiveInterval(
			Duration.ofSeconds(this.defaultMaxInactiveInterval));
	}
	return result;
}
Copy the code

So this is pretty straightforward, just new a MapSession, and then set the session validity period.

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));
}
Copy the code

Session ID: originalId: current ID: originalId: current ID The originalId is created when the Session object is first generated and does not change. From the source code, only the GET method is provided for originalId. In the case of an ID, you can actually change it with a Changessession ID.

This operation is actually an optimization, clearing old session data in time to free up memory.

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);
}
Copy the code

This logic is also very simple: first fetch session data from the Map based on the ID, return NULL if there is no session data, check whether it has expired, delete it if it has expired, and then return NULL. If it does, build a MapSession and return it.

OK, so that’s the series of memory-based implementations. Let’s move on to other implementations.

3, FindByIndexNameSessionRepository

FindByIndexNameSessionRepository inherited SessionRepository interface, used to extend the storage to a third party.

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) {
		returnfindByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName); }}Copy the code

Method of adding a single FindByIndexNameSessionRepository query all sessions for a specified user. This is by setting called FindByIndexNameSessionRepository. PRINCIPAL_NAME_INDEX_NAME Session attribute value for the specified user’s username. It is the developer’s responsibility to ensure that attributes are assigned because SpringSession does not care about the authentication mechanism being used. Examples given in the official documentation are as follows:

String username = "username";
this.session.setAttribute(
	FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
Copy the code

FindByIndexNameSessionRepository some implementation may provide some hooks automatic index of other session attributes. For example, many implementation will automatically ensure that the current Spring Security. User name can be through the index name FindByIndexNameSessionRepository PRINCIPAL_NAME_INDEX_NAME indexes. Once the session is indexed, it can be retrieved with the following code:

String username = "username";
Map<String, Session> sessionIdToSession = 
	this.sessionRepository.findByIndexNameAndIndexValue(
	FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,username);
Copy the code

Below are three FindByIndexNameSessionRepository interface implementation class:

Let’s look at the implementation details of each of the three stores.

3.1 RedisOperationsSessionRepository

RedisOperationsSessionRepository class diagram of the structure is as follows, the monitoring interface is redis MessageListener news subscription.

The code is too long to post here, but some comments can be found in the Chinese branch of the SpringSession. Here we’ll focus on the implementation of those methods.

3.1.1 createSession

The implementation of MapSessionRepository is basically the same as the implementation of MapSessionRepository. The difference is that the encapsulation model of the Session is different. In this case, RedisSession is implemented by wrapping MapSession with another layer. The RedisSession class will be examined next.

@Override
public RedisSession createSession(a) { 
    // RedisSession is different from MapSession
	RedisSession redisSession = new RedisSession();
	if (this.defaultMaxInactiveInterval ! =null) {
		redisSession.setMaxInactiveInterval(
				Duration.ofSeconds(this.defaultMaxInactiveInterval));
	}
	return redisSession;
}
Copy the code

Before we look at the other two methods, let’s look at the RedisSession class.

3.1.2 RedisSession

This is an extension of MapSession in the model, adding something called delta.

final class RedisSession implements Session {
       // MapSession instance object, where the main data is stored
		private final MapSession cached;
		// The original last access time
		private Instant originalLastAccessTime;
		private Map<String, Object> delta = new HashMap<>();
		// Is a new session object
		private boolean isNew;
		// The original primary name
		private String originalPrincipalName;
		/ / the original sessionId
		private String originalSessionId;
Copy the code

Delta is a Map structure, so what’s inside it? See saveDelta’s method for details. SaveDelta are called this method will be in two places, one is said to be under the save method, another is flushImmediateIfNecessary this method:

private void flushImmediateIfNecessary(a) {
	if (RedisOperationsSessionRepository.this.redisFlushMode == RedisFlushMode.IMMEDIATE) { saveDelta(); }}Copy the code

RedisFlushMode provides two push modes:

  • ON_SAVE: only on callsaveMethod is executed whenwebThe usual way to do this in an environment is to submit HTTP responses as quickly as possible
  • IMMEDIATE: Writes directly to the database whenever changes are maderedisMedium, not likeON_SAVEAgain, at the endcommitWrite once when

Tracking flushImmediateIfNecessary method invocation chain is as follows:

save
save
redis
ON_SAVE
IMMEDIATE
SpringSession
redis

So delta holds the key-val object of some of the current changes, And these changes are the setAttribute, removeAttribute, setMaxInactiveIntervalInSeconds, setLastAccessedTime triggered the four methods; For example, setAttribute(k,v), k->v will be stored in the delta.

3.1.3 the save

It’s much easier to look at the Save method once you understand the saveDelta method. Save corresponds to redisflushmode.on_save.

@Override
public void save(RedisSession session) {
   // Call saveDelta directly to push data to Redis
	session.saveDelta();
	if (session.isNew()) {
	   // sessionCreatedKey->channl
		String sessionCreatedKey = getSessionCreatedChannel(session.getId());
		// Publish a message event to add a session for the MessageListener callback to handle.
		this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
		session.setNew(false); }}Copy the code

3.1.4 findById

The query part is quite different from the Map-based part, because instead of working directly on the Map, you interact with Redis once.

@Override
public RedisSession findById(String id) {
	return getSession(id, false);
}
Copy the code

Call the getSession method:

private RedisSession getSession(String id, boolean allowExpired) {
	// Retrieve data from Redis by ID
	Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
	if (entries.isEmpty()) {
		return null;
	}
	// Convert to MapSession
	MapSession loaded = loadSession(id, entries);
	if(! allowExpired && loaded.isExpired()) {return null;
	}
	// Convert to RedisSession
	RedisSession result = new RedisSession(loaded);
	result.originalLastAccessTime = loaded.getLastAccessedTime();
	return result;
}
Copy the code

Build MapSession from loadSession:

private MapSession loadSession(String id, Map<Object, Object> entries) {
   // Generate a MapSession instance
	MapSession loaded = new MapSession(id);
	// Iterate over the data
	for (Map.Entry<Object, Object> entry : entries.entrySet()) {
		String key = (String) entry.getKey();
		if (CREATION_TIME_ATTR.equals(key)) {
		    // Set the creation time
			loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
		}
		else if (MAX_INACTIVE_ATTR.equals(key)) {
			 // Set the maximum validity period
			loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
		}
		else if (LAST_ACCESSED_ATTR.equals(key)) {
			// Set the last access time
			loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
		}
		else if (key.startsWith(SESSION_ATTR_PREFIX)) {
		// Set the propertiesloaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue()); }}return loaded;
}
Copy the code

3.1.5 deleteById

Delete session data based on the sessionId. See the code comment for details.

@Override
public void deleteById(String sessionId) {
   / / get RedisSession
	RedisSession session = getSession(sessionId, true);
	if (session == null) {
		return;
	}
   // Clear the index of the current session data
	cleanupPrincipalIndex(session);
	// Perform the delete operation
	this.expirationPolicy.onDelete(session);
	String expireKey = getExpiredKey(session.getId());
	/ / remove expireKey
	this.sessionRedisOperations.delete(expireKey);
	// Session validity is set to 0
	session.setMaxInactiveInterval(Duration.ZERO);
	save(session);
}
Copy the code

3.1.6 onMessage

Finally, take a look at the subscription callback processing. Here’s the core logic:

boolean isDeleted = channel.equals(this.sessionDeletedChannel);
// Deleted or Expired?
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
	// Omit irrelevant code here
	// Deleted
	if (isDeleted) {
	   // Issue a SessionDeletedEvent event
		handleDeleted(session);
	}
	// Expired
	else {
		// Issue a SessionExpiredEvent eventhandleExpired(session); }}Copy the code

3.2 Some thoughts on Redis storage

First of all, if we design according to our own conventional ideas, how will we consider this matter. First of all, I want to make it clear that I am not very familiar with Redis and have not done in-depth research on it. So if I do it, it’s probably just storage.

  • findByIndexNameAndIndexValueThe design of this is throughindexNameandindexValueTo return all sessions of the current user. But one thing to consider here is that normally a user is only associated with one session, so this design is obviously designed to support a single-user, multi-session scenario.
    • IndexName: FindByIndexNameSessionRepository PRINCIPAL_NAME_INDEX_NAME
    • IndexValue: username
  • implementationMessageListenerInterface to increase event notification capability. By listening for these events, you can do somethingsessionOperation control. But in factSpringSessionI’m not doing anything, and from the code,publishEventThe method is an empty implementation. Waiting for reply#issue 1287
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
	@Override
	public void publishEvent(ApplicationEvent event) {}@Override
	public void publishEvent(Object event) {}};Copy the code
  • RedisFlushModeSpringSession, provides two modes of push, one isON_SAVEThe other isIMMEDIATE. The default isON_SAVE, which is normally done once at the end of the request processingsessionCommitOperation.RedisFlushModeThe design feel is forsessionThe timing of data persistence provides another way of thinking.

summary

The design of storage mechanism is analyzed based on memory and Redis. Also based on JDBC and Hazelcast interested students can view the source code.

Finally, please visit my personal blog: www.glmapper.com

reference

  • Blog.csdn.net/zyhlwzy/art…
  • Docs. Spring. IO/spring – sess…