spring-session(一)揭秘续篇
2021-06-17 22:04
标签:RoCE pat www src etc 滚动 自动 htm static 上一篇文章中介绍了Spring-Session的核心原理,Filter,Session,Repository等等,传送门:spring-session(一)揭秘。 这篇继上一篇的原理逐渐深入Spring-Session中的事件机制原理的探索。众所周知,Servlet规范中有对HttpSession的事件的处理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,可以查看Package javax.servlet 在Spring-Session中也有相应的Session事件机制实现,包括Session创建/过期/删除事件。 本文主要从以下方面探索Spring-Session中事件机制 Note: 先来看下Session事件抽象UML类图,整体掌握事件之间的依赖关系。 Session Event最顶层是ApplicationEvent,即Spring上下文事件对象。由此可以看出Spring-Session的事件机制是基于Spring上下文事件实现。 抽象的AbstractSessionEvent事件对象提供了获取Session(这里的是指Spring Session的对象)和SessionId。 基于事件的类型,分类为: Tips: 事件对象只是对事件本身的抽象,描述事件的属性,如: 下面再深入探索以上的Session事件是如何触发,从事件源到事件监听器的链路分析事件流转过程。 阅读本节前,读者应该了解Redis的Pub/Sub和KeySpace Notification,如果还不是很了解,传送门Redis Keyspace Notifications和Pub/Sub。 上节中也介绍Session Event事件基于Spring的ApplicationEvent实现。先简单认识spring上下文事件机制: 那么在Spring-Session中必然包含事件发布者ApplicationEventPublisher发布Session事件和ApplicationListener监听Session事件。 可以看出ApplicationEventPublisher发布一个事件: If the specified {@code event} is not an {@link ApplicationEvent},
* it is wrapped in a {@link PayloadApplicationEvent}.
* @param event the event to publish
* @since 4.2
* @see PayloadApplicationEvent
*/
void publishEvent(Object event);
} ApplicationListener用于监听相应的事件: Tips: Session事件的流程实现如下: 上图展示了Spring-Session事件流程图,事件源来自于Redis键空间通知,在spring-data-redis项目中抽象MessageListener监听Redis事件源,然后将其传播至spring应用上下文发布者,由发布者发布事件。在spring上下文中的监听器Listener即可监听到Session事件。 因为两者是Spring框架提供的对Spring的ApplicationEvent的支持。Session Event基于ApplicationEvent实现,必然也有其相应发布者和监听器的的实现。 Spring-Session中的RedisSession的SessionRepository是RedisOperationSessionRepository。所有关于RedisSession的管理操作都是由其实现,所以Session的产生源是RedisOperationSessionRepository。 在RedisOperationSessionRepository中持有ApplicationEventPublisher对象用于发布Session事件。 但是该ApplicationEventPublisher是空实现,实际实现是在应用启动时由Spring-Session自动配置。在spring-session-data-redis模块中RedisHttpSessionConfiguration中有关于创建RedisOperationSessionRepository Bean时将调用set方法将ApplicationEventPublisher配置。 在进行自动配置时,将上下文中的ApplicationEventPublisher的注入,实际上即ApplicationContext对象。 Note: 对于ApplicationListener是由应用开发者自行实现,注册成Bean即可。当有Session Event发布时,即可监听。 以上部分探索了Session事件的发布者和监听者,但是核心事件的触发发布则是由Redis的键空间通知机制触发,当有Session创建/删除/过期时,Redis键空间会通知Spring-Session应用。 RedisOperationsSessionRepository实现spring-data-redis中的MessageListener接口。 该监听器即用来监听redis发布的消息。RedisOperationsSessionRepositorys实现了该Redis键空间消息通知监听器接口,实现如下: 下续再深入每种事件产生的前世今生。 RedisOperationSessionRepository中保存一个Session时,判断Session是否新创建。 该save方法的调用是由HttpServletResponse提交时——即返回客户端响应调用,上篇文章已经详解,这里不再赘述。关于RedisOperationSessionRepository实现MessageListener上述已经介绍,这里同样不再赘述。 Note: Tips: 当调用HttpSession的invalidate方法让Session失效时,即会调用RedisOperationSessionRepository的deleteById方法删除Session的过期键。 上篇中介绍了包装Spring Session为HttpSession,这里不再赘述。这里重点分析deleteById内容: 后续流程同SessionCreateEvent流程。 Session的过期事件流程比较特殊,因为Redis的键空间通知的特殊性,Redis键空间通知不能保证过期键的通知的及时性。 定时任务每整分运行,执行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy实例,是RedisSession过期策略。 将时间戳滚动至整分 获取过期Session的集合 调用Redis的Exists命令,访问过期Session键,触发Redis键空间消息 至此Spring-Session的Session事件通知模块就已经很清晰: spring-session(一)揭秘续篇 标签:RoCE pat www src etc 滚动 自动 htm static 原文地址:https://www.cnblogs.com/lxyit/p/9719542.html
这里的事件触发机制只介绍基于RedissSession的实现。基于内存Map实现的MapSession不支持Session事件机制。其他的Session实现这里也不做关注。一.Session事件的抽象
Session销毁事件只是删除和过期事件的统一,并无实际含义。
二.事件的触发机制
@FunctionalInterface
public interface ApplicationEventPublisher {
/**
* Notify all matching listeners registered with this
* application of an application event. Events may be framework events
* (such as RequestHandledEvent) or application-specific events.
* @param event the event to publish
* @see org.springframework.web.context.support.RequestHandledEvent
*/
default void publishEvent(ApplicationEvent event) {
publishEvent((Object) event);
}
/**
* Notify all matching listeners registered with this
* application of an event.
*
@FunctionalInterface
public interface ApplicationListener
这里使用到了发布/订阅模式,事件监听器可以监听感兴趣的事件,发布者可以发布各种事件。不过这是内部的发布订阅,即观察者模式。private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
@Override
public void publishEvent(ApplicationEvent event) {
}
@Override
public void publishEvent(Object event) {
}
};
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
SchedulingConfigurer {
private ApplicationEventPublisher applicationEventPublisher;
@Bean
public RedisOperationsSessionRepository sessionRepository() {
RedisTemplate
考虑篇幅原因,以上的RedisHttpSessionConfiguration至展示片段。/**
* session事件监听器
*
* @author huaijin
*/
@Component
public class SessionEventListener implements ApplicationListener
/**
* Listener of messages published in Redis.
*
* @author Costin Leau
* @author Christoph Strobl
*/
public interface MessageListener {
/**
* Callback for processing received objects through Redis.
*
* @param message message must not be {@literal null}.
* @param pattern pattern matching the channel (if specified) - can be {@literal null}.
*/
void onMessage(Message message, @Nullable byte[] pattern);
}
public class RedisOperationsSessionRepository implements
FindByIndexNameSessionRepository
1.Session创建事件的触发
如果新创建,则向@Override
public void save(RedisSession session) {
session.saveDelta();
// 判断是否为新创建的session
if (session.isNew()) {
// 获取redis指定的channel:${namespace}:event:created:${sessionId},
// 如:session.example:event:created:82sdd-4123-o244-ps123
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
// 向该通道发布session数据
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
// 设置session为非新创建
session.setNew(false);
}
}
这里有点绕。个人认为RedisOperationSessionRepository发布创建然后再本身监听,主要是考虑分布式或者集群环境中SessionCreateEvent事件的处理。2.Session删除事件的触发
删除事件中使用到了Redis KeySpace Notification,建议先了解该技术。
/**
* Allows creating an HttpSession from a Session instance.
*
* @author Rob Winch
* @since 1.0
*/
private final class HttpSessionWrapper extends HttpSessionAdapter
{
HttpSessionWrapper(S session, ServletContext servletContext) {
super(session, servletContext);
}
@Override
public void invalidate() {
super.invalidate();
SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
setCurrentSession(null);
clearRequestedSessionCache();
// 调用删除方法
SessionRepositoryFilter.this.sessionRepository.deleteById(getId());
}
}@Override
public void deleteById(String sessionId) {
// 如果session为空则返回
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
cleanupPrincipalIndex(session);
this.expirationPolicy.onDelete(session);
// 获取session的过期键
String expireKey = getExpiredKey(session.getId());
// 删除过期键,redis键空间产生del事件消息,被MessageListener即
// RedisOperationSessionRepository监听
this.sessionRedisOperations.delete(expireKey);
session.setMaxInactiveInterval(Duration.ZERO);
save(session);
}
3.Session失效事件的触发
@Scheduled(cron = "0 * * * * *")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}
public void cleanExpiredSessions() {
// 获取当前时间戳
long now = System.currentTimeMillis();
// 时间滚动至整分,去掉秒和毫秒部分
long prevMin = roundDownMinute(now);
if (logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
}
// 根据整分时间获取过期键集合,如:spring:session:expirations:1439245080000
String expirationKey = getExpirationKey(prevMin);
// 获取所有的所有的过期session
Set
static long roundDownMinute(long timeInMs) {
Calendar date = Calendar.getInstance();
date.setTimeInMillis(timeInMs);
// 清理时间错的秒位和毫秒位
date.clear(Calendar.SECOND);
date.clear(Calendar.MILLISECOND);
return date.getTimeInMillis();
}
String getExpirationKey(long expires) {
return this.redisSession.getExpirationsKey(expires);
}
// 如:spring:session:expirations:1439245080000
String getExpirationsKey(long expiration) {
return this.keyPrefix + "expirations:" + expiration;
}
/**
* By trying to access the session we only trigger a deletion if it the TTL is
* expired. This is done to handle
* https://github.com/spring-projects/spring-session/issues/93
*
* @param key the key
*/
private void touch(String key) {
this.redis.hasKey(key);
}
总结