- 浏览: 194040 次
- 性别:
- 来自: 南京
文章分类
最新评论
1.实现原理
1.1 得益于HttpSession和HttpServletRequest都是接口,这意味着我们可以提供自己的实现类来重写其中的方法,Spring session正是利用这个特性实现的。
首先是实现了HttpServletRequest的SessionRepositoryRequestWrapper类,伪代码如下
实现HttpSession的类HttpSessionWrapper,此类内部包含一个具体的Spring session实现类,并将调用HttpSession的方法都委托给这个具体的Spring session实现
伪代码如下
1.2 通过SessionRepositoryFilter将HttpServletRequest替换成我们自己的SessionRepositoryRequestWrapper类
伪代码如下
因为HttpServletRequest在这个filter中被替换,所以SessionRepositoryFilter必须要在所有需要获取session的filter前面执行。
2.UML图
3.Session在redis中的存储结构
在介绍Spring session在redis的存储结构之前,先简单介绍spring session中利用的几个redis特性
spring session 一个sessionId默认情况下会在redis形成三个键,如下:
现在来说明下问什么spring要这样设计对应的存储。
3.1 一般来讲,我们利用session,主要逻辑就是第一次访问时产生一个session,一段时间内不访问(默认30分钟),session失效。 这样利用redis提供的key实效功能,我们只用spring:session:sessions:{sessionId}貌似就可以实现这个功能,为什么spring不这样做呢,按照spring官方的说法
大概意思就是在redis中虽然一个key过期了,但是redis并不能保证这个key的过期事件会被触发,除非我们明确的访问下这个key。因为在redis中清理过期key是一个低优先级的任务。
实际上redis清理key的任务不仅优先级低,并且每次清理时也不会查看所有的key来删除过期数据。
所以,我们不能单纯依赖redis,只设计一个key就完成session的存储和过期。那spring怎么做的呢,下面来看下spring从redis中获取session的代码段
就是获取session,如果能获取到,还要再执行一次判断,如果是过期session也不返回。
3.2 前面说过,spring:session:sessions:{sessionId}key对应的ttl时间是2100,但是spring session的默认过期时间是1800(30分钟),为什么ttl的时间要比过期时间多5分钟呢,这个主要是考虑到如果业务需要在session过期后做一些清理操作,就必须在session过期后还能获取到session信息,所以session的时间存储时间多了5分钟。那spring是怎么获取到那些session过期了呢,这里用到了redis提供的另一个功能Notifications,具体配置在ConfigureNotifyKeyspaceEventsAction这个类中。具体的实现则引入了第二个key【spring:session:sessions:expires:{sessionId}】,这个key只是真正的session的一个饮用,在spring创建session时,同步的创建,这个key的ttl时间正是真正的spring session过期时间1800,这个key过期时会给订阅者发送如下一个事件消息
spring 在获取这个消息后会发布SessionExpiredEvent事件,具体代码如下
这样如果我们业务如果需要做些清理工作,在收到SessionExpiredEvent(SessionDestroyedEvent)事件后可以根据相应的session信息做对应的清理工作。
3.3 类似于session的过期不能完全依赖redis一样,spring:session:sessions:expires:{sessionId}的过期也不能依赖redis,那spring是怎么做的呢,这个就是第三个key spring:session:expirations:{时间戳}的功能了。spring生成session后、或者更新session的最后访问时间后,都会根据session的存活时间算出一个时间戳,就是这个session在那一分钟过期,然后把【spring:session:sessions:expires:{sessionId}】这个key 存到对应的spring:session:expirations:{时间戳}中,如果更新了一个session,会同步的把这个key从旧的spring:session:expirations:{时间戳}中移除,然后追加到新计算出来的spring:session:expirations:{时间戳}中。具体代码如下
然后spring又启动了一个定时任务,每分钟执行一次,将这一分钟过期的key【spring:session:expirations:{时间戳}】中所有的key都删除
此处还有一个小窍门,因为在如下场景下会出现对session的并发更新
请求1开始访问,设定的session最后访问时间是100
请求2开始访问,设定session的最后访问时间是200
正常情况下 这个session的会被放到spring:session:expirations:200这个set中,但是现在因为请求1响应太慢,在请求2结束后才真正结束,此时会导致 这个session对应的espire信息在两个set中都有。然后时间到达100对应的set开始清除,我们就删除了这个session信息,而实际上这个session还没有到达严格意义上的过期,所以spring 在清除这个key时,不是直接删除,而只是访问了一下这个key,我们之前也说过,一个key如果过期后,只要以任何形式访问一下,redis就会自己把这个过期key删除。具体代码
1.1 得益于HttpSession和HttpServletRequest都是接口,这意味着我们可以提供自己的实现类来重写其中的方法,Spring session正是利用这个特性实现的。
首先是实现了HttpServletRequest的SessionRepositoryRequestWrapper类,伪代码如下
public class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper { public SessionRepositoryRequestWrapper(HttpServletRequest original) { super(original); } public HttpSessionWrapper getSession(String sessionId) { // 调用sessionRepository获取存储具体介质中存储的session,并封装成自定义类的实现了HttpSession的类 sessionRepository.getSession(sessionId); } public HttpSessionWrapper getSession(boolean createNew) { // create an HttpSession implementation from Spring Session } void commitSession() { // 调用sessionRepository将session信息存储到具体的介质中 sessionRepository.save(session) } // ... other methods delegate to the original HttpServletRequest ... }
实现HttpSession的类HttpSessionWrapper,此类内部包含一个具体的Spring session实现类,并将调用HttpSession的方法都委托给这个具体的Spring session实现
伪代码如下
class HttpSessionWrapper extends ExpiringSessionHttpSession<S> { }
class ExpiringSessionHttpSession<S extends ExpiringSession> implements HttpSession { ExpiringSessionHttpSession(S session, ServletContext servletContext) { this.session = session; this.servletContext = servletContext; } // 获取id委托给spring session的具体实现 public String getId() { return this.session.getId(); } // 获取最后访问时间委托给spring session的具体实现 public long getLastAccessedTime() { checkState(); return this.session.getLastAccessedTime(); } // 其他需要实现的方法 }
1.2 通过SessionRepositoryFilter将HttpServletRequest替换成我们自己的SessionRepositoryRequestWrapper类
伪代码如下
public class SessionRepositoryFilter implements Filter { public doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest httpRequest = (HttpServletRequest) request; // 替换成我们自己的实现,后续的filter、及业务逻辑再调用HttpServletRequest.getSession就会调用我们自己的实现 SessionRepositoryRequestWrapper customRequest = new SessionRepositoryRequestWrapper(httpRequest); try{ chain.doFilter(customRequest, response, chain); } finally { // 将变更后的session更新到redis中 } } // ... }
因为HttpServletRequest在这个filter中被替换,所以SessionRepositoryFilter必须要在所有需要获取session的filter前面执行。
2.UML图
3.Session在redis中的存储结构
在介绍Spring session在redis的存储结构之前,先简单介绍spring session中利用的几个redis特性
- 在redis中可以给每个key设定过期时间(TTL)
- 通过配置,可以让redis在发生key的删除、过期、新增等事件时通知订阅者(具体可参考 Notifications
spring session 一个sessionId默认情况下会在redis形成三个键,如下:
key | 数据结构 | 存储数据 | 默认过期时间 |
spring:session:sessions:{sessionId} | hash | 具体的session信息包括creationTime、maxInactiveInterval、lastAccessedTime以及sessionAttr信息 | 2100 |
spring:session:sessions:expires:{sessionId} | String | sessionId的一个引用,对应的value是空值 | 1800 |
spring:session:expirations:{时间戳} | Set | 存储对应时间戳过期的sessionId引用 | 2100 |
现在来说明下问什么spring要这样设计对应的存储。
3.1 一般来讲,我们利用session,主要逻辑就是第一次访问时产生一个session,一段时间内不访问(默认30分钟),session失效。 这样利用redis提供的key实效功能,我们只用spring:session:sessions:{sessionId}貌似就可以实现这个功能,为什么spring不这样做呢,按照spring官方的说法
引用
One problem with relying on Redis expiration exclusively is that Redis makes no guarantee of when the expired event will be fired if the key has not been accessed. Specifically the background task that Redis uses to clean up expired keys is a low priority task and may not trigger the key expiration. For additional details see Timing of expired events section in the Redis documentation.
大概意思就是在redis中虽然一个key过期了,但是redis并不能保证这个key的过期事件会被触发,除非我们明确的访问下这个key。因为在redis中清理过期key是一个低优先级的任务。
实际上redis清理key的任务不仅优先级低,并且每次清理时也不会查看所有的key来删除过期数据。
所以,我们不能单纯依赖redis,只设计一个key就完成session的存储和过期。那spring怎么做的呢,下面来看下spring从redis中获取session的代码段
private RedisSession getSession(String id, boolean allowExpired) { Map<Object, Object> entries = getSessionBoundHashOperations(id).entries(); if (entries.isEmpty()) { return null; } MapSession loaded = loadSession(id, entries); // 判断session是否过期 if (!allowExpired && loaded.isExpired()) { return null; } RedisSession result = new RedisSession(loaded); result.originalLastAccessTime = loaded.getLastAccessedTime(); return result; }
就是获取session,如果能获取到,还要再执行一次判断,如果是过期session也不返回。
3.2 前面说过,spring:session:sessions:{sessionId}key对应的ttl时间是2100,但是spring session的默认过期时间是1800(30分钟),为什么ttl的时间要比过期时间多5分钟呢,这个主要是考虑到如果业务需要在session过期后做一些清理操作,就必须在session过期后还能获取到session信息,所以session的时间存储时间多了5分钟。那spring是怎么获取到那些session过期了呢,这里用到了redis提供的另一个功能Notifications,具体配置在ConfigureNotifyKeyspaceEventsAction这个类中。具体的实现则引入了第二个key【spring:session:sessions:expires:{sessionId}】,这个key只是真正的session的一个饮用,在spring创建session时,同步的创建,这个key的ttl时间正是真正的spring session过期时间1800,这个key过期时会给订阅者发送如下一个事件消息
spring 在获取这个消息后会发布SessionExpiredEvent事件,具体代码如下
public void onMessage(Message message, byte[] pattern) { byte[] messageChannel = message.getChannel(); byte[] messageBody = message.getBody(); if (messageChannel == null || messageBody == null) { return; } String channel = new String(messageChannel); if (channel.startsWith(getSessionCreatedChannelPrefix())) { // TODO: is this thread safe? Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer .deserialize(message.getBody()); handleCreated(loaded, channel); return; } String body = new String(messageBody); if (!body.startsWith(getExpiredKeyPrefix())) { return; } boolean isDeleted = channel.endsWith(":del"); if (isDeleted || channel.endsWith(":expired")) { int beginIndex = body.lastIndexOf(":") + 1; int endIndex = body.length(); String sessionId = body.substring(beginIndex, endIndex); RedisSession session = getSession(sessionId, true); if (logger.isDebugEnabled()) { logger.debug("Publishing SessionDestroyedEvent for session " + sessionId); } cleanupPrincipalIndex(session); if (isDeleted) { handleDeleted(sessionId, session); } // 发布事件的方法 else { handleExpired(sessionId, session); } return; } } //.... private void handleExpired(String sessionId, RedisSession session) { if (session == null) { publishEvent(new SessionExpiredEvent(this, sessionId)); } else { publishEvent(new SessionExpiredEvent(this, session)); } }
这样如果我们业务如果需要做些清理工作,在收到SessionExpiredEvent(SessionDestroyedEvent)事件后可以根据相应的session信息做对应的清理工作。
3.3 类似于session的过期不能完全依赖redis一样,spring:session:sessions:expires:{sessionId}的过期也不能依赖redis,那spring是怎么做的呢,这个就是第三个key spring:session:expirations:{时间戳}的功能了。spring生成session后、或者更新session的最后访问时间后,都会根据session的存活时间算出一个时间戳,就是这个session在那一分钟过期,然后把【spring:session:sessions:expires:{sessionId}】这个key 存到对应的spring:session:expirations:{时间戳}中,如果更新了一个session,会同步的把这个key从旧的spring:session:expirations:{时间戳}中移除,然后追加到新计算出来的spring:session:expirations:{时间戳}中。具体代码如下
public void onExpirationUpdated(Long originalExpirationTimeInMilli, ExpiringSession session) { String keyToExpire = "expires:" + session.getId(); long toExpire = roundUpToNextMinute(expiresInMillis(session)); if (originalExpirationTimeInMilli != null) { long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli); if (toExpire != originalRoundedUp) { // 从旧的里面移除 String expireKey = getExpirationKey(originalRoundedUp); this.redis.boundSetOps(expireKey).remove(keyToExpire); } } long sessionExpireInSeconds = session.getMaxInactiveIntervalInSeconds(); String sessionKey = getSessionKey(keyToExpire); if (sessionExpireInSeconds < 0) { this.redis.boundValueOps(sessionKey).append(""); this.redis.boundValueOps(sessionKey).persist(); this.redis.boundHashOps(getSessionKey(session.getId())).persist(); return; } String expireKey = getExpirationKey(toExpire); BoundSetOperations<Object, Object> expireOperations = this.redis .boundSetOps(expireKey); // 加入到新的里面 expireOperations.add(keyToExpire); long fiveMinutesAfterExpires = sessionExpireInSeconds + TimeUnit.MINUTES.toSeconds(5); expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS); if (sessionExpireInSeconds == 0) { this.redis.delete(sessionKey); } else { this.redis.boundValueOps(sessionKey).append(""); // 更新对应key的过期时间 this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds, TimeUnit.SECONDS); } this.redis.boundHashOps(getSessionKey(session.getId())) .expire(fiveMinutesAfterExpires, TimeUnit.SECONDS); }
然后spring又启动了一个定时任务,每分钟执行一次,将这一分钟过期的key【spring:session:expirations:{时间戳}】中所有的key都删除
// 启动定时任务,每分钟执行一次 @Scheduled(cron = "${spring.session.cleanup.cron.expression: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)); } String expirationKey = getExpirationKey(prevMin); Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members(); // 清除过期的key this.redis.delete(expirationKey); for (Object session : sessionsToExpire) { // 将过期的session清除掉 String sessionKey = getSessionKey((String) session); touch(sessionKey); } }
此处还有一个小窍门,因为在如下场景下会出现对session的并发更新
请求1开始访问,设定的session最后访问时间是100
请求2开始访问,设定session的最后访问时间是200
正常情况下 这个session的会被放到spring:session:expirations:200这个set中,但是现在因为请求1响应太慢,在请求2结束后才真正结束,此时会导致 这个session对应的espire信息在两个set中都有。然后时间到达100对应的set开始清除,我们就删除了这个session信息,而实际上这个session还没有到达严格意义上的过期,所以spring 在清除这个key时,不是直接删除,而只是访问了一下这个key,我们之前也说过,一个key如果过期后,只要以任何形式访问一下,redis就会自己把这个过期key删除。具体代码
/** * 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); }
发表评论
-
spring-security
2018-01-08 09:05 0FilterSecurityInterceptor是一个fil ... -
Spring cloud config利用gitlab webhook自动刷新
2017-02-10 20:14 27一、简介 Spring Cloud Config 提供serv ... -
Spring @ConfigurationProperties
2017-02-10 20:27 8131.配置文件类 package chengf.spring ... -
Spring 利用@Value注入properties文件属性
2017-01-21 09:05 1433本编文章是对Spring利用@Value来直接注入proper ... -
Spring ws 小示例
2016-12-10 15:43 4268一、简介 Spring Web Service 致力于开发 ... -
Spring4+WebSocket小示例
2016-11-27 11:09 2570一、简介 WebSocket ... -
Spring+Schedule(定时任务)小示例
2016-11-18 17:21 1402本篇文章简单介绍一个Spring的schedule小例子。此定 ... -
Spring+thymeleaf小示例
2016-11-18 14:01 1040之前一篇文章写了个简单的Spring mvc例子,界面表示层用 ... -
SpringMVC小示例
2016-11-16 20:23 685***因为在内网环境下, ...
相关推荐
赠送jar包:spring-session-data-redis-2.0.4.RELEASE.jar; 赠送原API文档:spring-session-data-redis-2.0.4.RELEASE-javadoc.jar; 赠送源代码:spring-session-data-redis-2.0.4.RELEASE-sources.jar; 赠送...
赠送jar包:spring-session-data-redis-2.0.4.RELEASE.jar; 赠送原API文档:spring-session-data-redis-2.0.4.RELEASE-javadoc.jar; 赠送源代码:spring-session-data-redis-2.0.4.RELEASE-sources.jar; 赠送...
标题中的“Spring-session2整合spring5+redis”指的是在Spring框架的第五个主要版本(Spring 5)中,集成Spring Session 2与Redis数据库来管理Web应用的会话(Session)。Spring Session是一个开源项目,旨在提供一...
commons-pool2-2.3.jar,jedis-2.8.0.jar,spring-data-redis-1.6.0.RELEASE.jar,spring-session-1.1.1.RELEASE.jar,Spring-data-redis(Version 1.6.0.RC1)中文版.pdf
分布式环境下的session 共享-基于spring-session组件和Redis实现
标题 "nginx+spring-session+redis 实现session共享" 涉及到的是在分布式系统中如何处理会话(session)共享的问题。在分布式环境中,由于用户请求可能被路由到不同的服务器节点,传统的session存储方式(如JVM内存...
### Spring-Session与Redis结合实现Session共享 在分布式系统中,单点登录(Single Sign-On,简称SSO)成为一种常见的需求。为了实现这一目标,就需要解决不同服务器间Session共享的问题。Spring-Session正是为此而...
spring-session+spring依赖jar包,包含spring4.0.2.RELEASE相关jar包和commons-pool2-2.4.2.jar,jedis-2.7.3.jar,spring-data-redis-1.6.2.RELEASE.jar,spring-session-1.1.1.RELEASE.jar
自己实现spring-session,实现单点登陆的功能 使用filter拦截用户的请求,在filter中包装request,在request的包装类requestWrapper中,重写getSession(), 和getSession(boolean create)。自己实现httpSession,...
Spring Session是一个由Spring社区维护的项目,它允许我们通过多种后端存储(如Redis)来实现跨服务器的session共享。本项目“使用spring-session加redis来实现session共享”是一个基于Spring Boot框架的实现案例,...
在本文中,我们将详细介绍 Spring-Redis-Session 的自定义 key 和过期时间的实现原理和配置方法。 自定义 Key 在 Spring-Redis-Session 中,默认的会话 key 是以 "spring:session:sessions:" 开头的,如果我们想...
本压缩包包含的“spring-session+spring+redis”组合,是将Spring Session与Redis集成,利用Redis作为会话存储介质,以实现高可用性和可扩展性。 首先,我们要了解Spring Session的核心概念。它通过替换默认的...
spring-data-redis-2.0.2.RELEASE.jarspring-data-redis-2.0.2.RELEASE.jar
在SpringMVC中集成Spring Data Redis,可以利用Redis的高效特性来提升应用程序的数据处理能力,例如作为session共享的存储、缓存数据或者实现发布/订阅(Pub/Sub)功能。发布/订阅是一种通信模式,允许发送者(pub)将...
spring-session-data-redis-2.5.2.jar
spring-session-data-redis-2.2.0.RELEASE
最新最稳定的spring-data-reids配套支持,里面包含spring-data-redis的jar包,以及支持该jar包的相关jar包,适用于spring5.0版本,解决spring支持redis数据库缓存使用
Spring-Session可以通过将Session数据存储在Redis这样的分布式存储中,实现不同应用间Session的共享,从而达到SSO的效果。 **Spring-Session与Redis的结合** Spring-Session通过将Session数据持久化到Redis,确保...
SpringSession通过将Session数据持久化到外部存储,如Redis,实现了跨服务器的Session共享。 Redis 是一个高性能的键值数据库,常被用作缓存和消息代理。在SpringSession中,Redis被用作Session的存储后端,因为其...
8. **消息支持**:Spring Data Redis提供了对Redis Pub/Sub(发布/订阅)的支持,允许开发构建基于消息的应用程序,实现异步通信和解耦。 9. **Key的过期策略**:可以通过`expire()`, `expireAt()`等方法设置Redis...