session存在的问题
spring-session
token取代session,实现服务端到客户端的跨变
没有什么是加一层解决不了的hhh
当我们用nginx做负载均衡时,用户在A服务器登录了,A服务器存储了session,客户端也存储了cookie,其中有JSESSIONID。
此时负载均衡,访问B服务器的话,B服务器是没有这个session的,客户端的cookie里边JSESSIONID也就找不到对应的session,相当于没有登录,此时如何解决呢?
用nginx的ip_hash可以使得某个ip的用户,只固定访问某个特定的服务器,这样就不会跑到其他服务器,也就不需要考虑session共享的问题了
但与此同时,这又违背了Nginx负载均衡的初衷,请求都固定打到某一台服务器,宕机就不好办了,于是我们有了spring-session
当请求进来的时候,SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的getSession方法。
这个方法是被重写过的,逻辑是先从 request 的属性中查找,如果找不到;再查找一个key值是"SESSION"的 Cookie,通过这个 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接创建一个RedisSession 对象,同步到 Redis 中。
说的简单点就是:拦截请求,将之前在服务器内存中进行 Session 创建销毁的动作,改成在 Redis 中创建。
-
- /**
- * HttpServletRequest getSession()实现
- */
- @Override
- public HttpSessionWrapper getSession() {
- return getSession(true);
- }
-
- @Override
- public HttpSessionWrapper getSession(boolean create) {
- HttpSessionWrapper currentSession = getCurrentSession();
- if (currentSession != null) {
- return currentSession;
- }
- //从当前请求获取sessionId
- String requestedSessionId = getRequestedSessionId();
- if (requestedSessionId != null
- && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
- S session = getSession(requestedSessionId);
- if (session != null) {
- this.requestedSessionIdValid = true;
- currentSession = new HttpSessionWrapper(session, getServletContext());
- currentSession.setNew(false);
- setCurrentSession(currentSession);
- return currentSession;
- }
- else {
- // This is an invalid session id. No need to ask again if
- // request.getSession is invoked for the duration of this request
- if (SESSION_LOGGER.isDebugEnabled()) {
- SESSION_LOGGER.debug(
- "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
- }
- setAttribute(INVALID_SESSION_ID_ATTR, "true");
- }
- }
- if (!create) {
- return null;
- }
- if (SESSION_LOGGER.isDebugEnabled()) {
- SESSION_LOGGER.debug(
- "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
- + SESSION_LOGGER_NAME,
- new RuntimeException(
- "For debugging purposes only (not an error)"));
- }
- //为当前请求创建session
- S session = SessionRepositoryFilter.this.sessionRepository.createSession();
- //更新时间
- session.setLastAccessedTime(System.currentTimeMillis());
- //对Spring session 进行包装(包装成HttpSession)
- currentSession = new HttpSessionWrapper(session, getServletContext());
- setCurrentSession(currentSession);
- return currentSession;
- }
-
- /**
- * 根据sessionId获取session
- */
- private S getSession(String sessionId) {
- S session = SessionRepositoryFilter.this.sessionRepository
- .getSession(sessionId);
- if (session == null) {
- return null;
- }
- session.setLastAccessedTime(System.currentTimeMillis());
- return session;
- }
-
- /**
- * 从当前请求获取sessionId
- */
- @Override
- public String getRequestedSessionId() {
- return SessionRepositoryFilter.this.httpSessionStrategy
- .getRequestedSessionId(this);
- }
-
- private void setCurrentSession(HttpSessionWrapper currentSession) {
- if (currentSession == null) {
- removeAttribute(CURRENT_SESSION_ATTR);
- }
- else {
- setAttribute(CURRENT_SESSION_ATTR, currentSession);
- }
- }
- /**
- * 获取当前请求session
- */
- @SuppressWarnings("unchecked")
- private HttpSessionWrapper getCurrentSession() {
- return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
- }
-
- 复制代码
查询我们搞懂了,很简单,其实就是透明的包装,我们拿还是直接用session.getAttributes(),那相应的也带来了问题
redis中每个session存储了三条信息。

spring:session:expirations 为set结构, 存储1620393360000 时间点过期的 spring:session:sessions:expires 键值
第二个用来存储Session的详细信息,这个key的过期时间为Session的最大过期时间 + 5分钟。如果默认的最大过期时间为30分钟,则这个key的过期时间为35分钟。
spring:session:sessions为hash结构,主要内容:包括Session的过期时间间隔、最近的访问时间、attributes
- hgetall spring:session:sessions:1b8b2340-da25-4ca6-864c-4af28f033327
- 1) "creationTime"
- 2) "\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\x0ejava.lang.Long;\\\\x8b\\\\xe4\\\\x90\\\\xcc\\\\x8f#\\\\xdf\\\\x02\\\\x00\\\\x01J\\\\x00\\\\x05valuexr\\\\x00\\\\x10java.lang.Number\\\\x86\\\\xac\\\\x95\\\\x1d\\\\x0b\\\\x94\\\\xe0\\\\x8b\\\\x02\\\\x00\\\\x00xp\\\\x00\\\\x00\\\\x01j\\\\x9b\\\\x83\\\\x9d\\\\xfd"
- 3) "maxInactiveInterval"
- 4) "\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\x11java.lang.Integer\\\\x12\\\\xe2\\\\xa0\\\\xa4\\\\xf7\\\\x81\\\\x878\\\\x02\\\\x00\\\\x01I\\\\x00\\\\x05valuexr\\\\x00\\\\x10java.lang.Number\\\\x86\\\\xac\\\\x95\\\\x1d\\\\x0b\\\\x94\\\\xe0\\\\x8b\\\\x02\\\\x00\\\\x00xp\\\\x00\\\\x00\\\\a\\\\b"
- 5) "lastAccessedTime"
- 6) "\\\\xac\\\\xed\\\\x00\\\\x05sr\\\\x00\\\\x0ejava.lang.Long;\\\\x8b\\\\xe4\\\\x90\\\\xcc\\\\x8f#\\\\xdf\\\\x02\\\\x00\\\\x01J\\\\x00\\\\x05valuexr\\\\x00\\\\x10java.lang.Number\\\\x86\\\\xac\\\\x95\\\\x1d\\\\x0b\\\\x94\\\\xe0\\\\x8b\\\\x02\\\\x00\\\\x00xp\\\\x00\\\\x00\\\\x01j\\\\x9b\\\\x83\\\\x9d\\\\xfd"
-
- 复制代码
处理一个session为什么要存储三条数据,而不是一条呢!对于session的实现,需要监听它的创建、过期等事件,redis可以监听某个key的变化,当key发生变化时,可以快速做出相应的处理。
Redis中过期key的策略有两种:
也就是:无法保证key的过期时间抵达后立即生成过期事件【把session给销毁】。 这也侧面说明了,前端访问的时候,是先拿服务器的Tocamt本地缓存,而不是拿redis,也就导致了,redis的键一直没有被访问,即使expire到了,也还是没被及时访问,没法触发过期事件
redis 是一个存储键值数据库系统,那它源码中是如何存储所有键值对的呢?
Redis 本身是一个典型的 key-value 内存存储数据库,因此所有的 key、value 都保存在之前学习过的 Dict 结构中。不过在其 database 结构体中,有两个 Dict:一个用来记录 key-value;另一个用来记录 key-TTL。

内部结构

这里有两个问题需要我们思考:
惰性删除:顾明思议并不是在 TTL 到期后就立刻删除,而是在访问一个 key 的时候,检查该 key 的存活时间,如果已经过期才执行删除。
周期删除:通过一个定时任务,周期性的抽样部分过期的 key,然后执行删除。执行周期有两种:
SLOW 模式规则:
FAST 模式规则(过期 key 比例小于 10% 不执行 ):
spring-session为了能够及时的产生Session过期时的过期事件,所以增加了:
- spring:session:sessions:expires:726de8fc-c045-481a-986d-f7c4c5851a67
- spring:session:expirations:1620393360000
- 复制代码
spring-session中有个定时任务,每个整分钟都会查询相应的spring:session:expirations:【整分钟的时间戳 中的过期SessionId】
🎈然后再访问一次这个SessionId,即spring:session:sessions:expires:SessionId ,【相当于主动访问这个key ,此时会触发redis的过期发生】——即本地缓存的Session过期事件。
可能有同学会问?这不跟redis的第二个过期策略一样吗,都是去扫一遍,有必要这里再扫吗?
定时任务代码
- @Scheduled(cron = "0 * * * * *")
- public void cleanupExpiredSessions() {
- this.expirationPolicy.cleanExpiredSessions();
- }
- 复制代码
定时任务每整分运行,执行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy实例,是RedisSession过期策略。
- 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<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
- // 删除过期Session键集合
- this.redis.delete(expirationKey);
- // touch访问所有已经过期的session,触发Redis键空间通知消息
- for (Object session : sessionsToExpire) {
- String sessionKey = getSessionKey((String) session);
- touch(sessionKey);
- }
- }
- 复制代码
将时间戳滚动至整分
- static long roundDownMinute(long timeInMs) {
- Calendar date = Calendar.getInstance();
- date.setTimeInMillis(timeInMs);
- // 清理时间错的秒位和毫秒位
- date.clear(Calendar.SECOND);
- date.clear(Calendar.MILLISECOND);
- return date.getTimeInMillis();
- }
- 复制代码
获取过期Session的集合
- String getExpirationKey(long expires) {
- return this.redisSession.getExpirationsKey(expires);
- }
-
- // 如:spring:session:expirations:1439245080000
- String getExpirationsKey(long expiration) {
- return this.keyPrefix + "expirations:" + expiration;
- }
- 复制代码
调用Redis的Exists命令,访问过期Session键,触发Redis键空间消息
- /**
- * 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);
- }
- 复制代码
这个留到下篇,我们再来详讲嘞,简单说就是:
客户端跟服务端,是1对多的关系,客户端只需要存储一份tokne即可,无需考虑共享问题 而若是服务端存【也就是session】,就需要考虑共享问题