前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Spring Session工作原理

Spring Session工作原理

原创
作者头像
2020labs小助手
修改于 2019-07-30 03:09:54
修改于 2019-07-30 03:09:54
62600
代码可运行
举报
文章被收录于专栏:vivo互联网技术vivo互联网技术
运行总次数:0
代码可运行

本文首发于 vivo互联网技术 微信公众号 https://mp.weixin.qq.com/s/KCOFv0nRuymkX79-RZi9eg 作者:张正林

HTTP协议本身是无状态的,为了保存会话信息,浏览器Cookie通过SessionID标识会话请求,服务器以SessionID为key来存储会话信息。在单实例应用中,可以考虑应用进程自身存储,随着应用体量的增长,需要横向扩容,多实例session共享问题随之而来。

Spring Session就是为了解决多进程session共享的问题,本文将介绍怎么使用Spring Session,以及Spring Session工作原理。

1、引入背景

应用部署在Tomcat时,session是由Tomcat内存维护,如果应用部署多个实例,session就不能共享。Spring Session就是解决为了解决分布式场景中的session共享问题。

2、使用方法

Spring Session支持存储在Hazelcast 、RedisMongoDB关系型数据库,本文主要讨论session存储在Redis。

web.xml配置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!-- spring session -->
  <filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

Spring 主要配置:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!--创建了一个RedisConnectionFactory,它将Spring会话连接到Redis服务器-->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!--配置Redis连接池 ,可以不配置,使用默认就行!-->
        p:poolConfig-ref="jedisPoolConfig"
    </bean>
 
    <!--创建一个Spring Bean的名称springSessionRepositoryFilter实现过滤器-->
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
        <!--默认session时效30分钟-->
        <property name="maxInactiveIntervalInSeconds" value="60" />
    </bean>

3、工作流程

Tomcat web.xml解析步骤:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
contextInitialized(ServletContextEvent arg0); // Listener
init(FilterConfig filterConfig); // Filter
init(ServletConfig config); // Servlet

初始化顺序:Listener > Filter > Servlet。

1) 通过 Tomcat 的 listener 把SessionRepositoryFilter加载到Spring容器中。

上一小节Spring配置文件里面声明了RedisHttpSessionConfiguration,正是在其父类SpringHttpSessionConfiguration中生成了SessionRepositoryFilter:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Bean
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
        SessionRepository<S> sessionRepository) {
    ......
    return sessionRepositoryFilter;
}

RedisHttpSessionConfiguration类继承关系

2) filter初始化

web.xml里面配置的filter是DelegatingFilterProxy。

DelegatingFilterProxy类继承关系

DelegatingFilterProxy初始化入口在其父类GenericFilterBean中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final void init(FilterConfig filterConfig) throws ServletException {
        ......
        // Let subclasses do whatever initialization they like.
        initFilterBean();
        ......
    }

DelegatingFilterProxy去Spring容器取第1步初始化好的springSessionRepositoryFilter:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // If no target bean name specified, use filter name.
                if (this.targetBeanName == null) {
                    //targetBeanName 为springSessionRepositoryFilter
                    this.targetBeanName = getFilterName();
                }
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }

至此 sessionRepositoryFilter 初始化完成,DelegatingFilterProxy 实际代理了SessionRepositoryFilter。

SessionRepositoryFilter 工作核心流程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected void doFilterInternal(HttpServletRequest request,
           HttpServletResponse response, FilterChain filterChain)
           throws ServletException, IOException {
       request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
       //包装了HttpServletRequest,覆写了HttpServletRequest中 getSession(boolean create)方法
       SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
               request, response, this.servletContext);
             ......
       try {
           filterChain.doFilter(strategyRequest, strategyResponse);
       }
       finally {
           //保证session持久化
           wrappedRequest.commitSession();
       }
   }

4、缓存机制

每一个session,Redis实际缓存的数据如下:

spring:session:sessions:1b8b2340-da25-4ca6-864c-4af28f033327 spring:session:sessions:expires:1b8b2340-da25-4ca6-864c-4af28f033327 spring:session:expirations:1557389100000

spring:session:sessions为hash结构,存储Spring Session的主要内容:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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"

spring:session:sessions:expires 为 string 结构,存储一个空值。

spring:session:expirations为set结构,存储1557389100000 时间点过期的spring:session:sessions:expires键值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
smembers spring:session:expirations:1557389100000
1) "\xac\xed\x00\x05t\x00,expires:1b8b2340-da25-4ca6-864c-4af28f033327"

RedisSessionExpirationPolicy,三个键值生成流程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void onExpirationUpdated(Long originalExpirationTimeInMilli,
            ExpiringSession session) {
        String keyToExpire = "expires:" + session.getId();
        long toExpire = roundUpToNextMinute(expiresInMillis(session));
                ......
        //把spring:session:sessions:expires加入到spring:session:expirations开头的key里面
        String expireKey = getExpirationKey(toExpire);
        BoundSetOperations<Object, Object> expireOperations = this.redis
                .boundSetOps(expireKey);
        expireOperations.add(keyToExpire);
 
        long fiveMinutesAfterExpires = sessionExpireInSeconds
                + TimeUnit.MINUTES.toSeconds(5);
        //spring:session:expirations开头的key过期时间为xml配置的时间后五分钟
        expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
        if (sessionExpireInSeconds == 0) {
            this.redis.delete(sessionKey);
        }
        else {
            //spring:session:sessions:expires开头的key过期时间为xml配置的时间
            this.redis.boundValueOps(sessionKey).append("");
            this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds,
                    TimeUnit.SECONDS);
        }
        //spring:session:sessions开头的key过期时间为xml配置的时间后五分钟
        this.redis.boundHashOps(getSessionKey(session.getId()))
                .expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
    }

Redis过期键有三种删除策略,分别是定时删除,惰性删除,定期删除。

  1. 定时删除:通过维护一个定时器,过期马上删除,是最有效的,但是也是最浪费cpu时间的。
  2. 惰性删除:程序在取出键时才判断它是否过期,过期才删除,这个方法对cpu时间友好,对内存不友好。
  3. 定期删除:每隔一定时间执行一次删除过期键的操作,并限制每次删除操作的执行时长和频率,是一种折中。

Redis采用了惰性删除和定期删除的策略。由此可见依赖 Redis 的过期策略实时删除过期key是不可靠的。

另外一方面,业务可能会在Spring Session过期后做业务逻辑处理,同时需要session里面的信息,如果只有一个 spring:session:sessions键值,那么Redis删除就删除了,业务没法获取session信息。

spring:session:expirations键中存储了spring:session:sessions:expires键,而spring:session:sessions:expires键过期五分钟早于spring:session:expirations键和spring:session:sessions键(实际Spring Session对于过期事件处理订阅的spring:session:sessions:expires键,下一节会具体讲),这样在订阅到过期事件时还能获取spring:session:sessions键值。

如果通过Redis本身清理机制未及时清除spring:session:sessions:expires,可以通过Spring Session提供的定时任务兜底,保证spring:session:sessions:expires清除。

RedisSessionExpirationPolicy,session清理定时任务

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void cleanExpiredSessions() {
        long now = System.currentTimeMillis();
        long prevMin = roundDownMinute(now);
        ......
        //获取到spring:session:expirations键
        String expirationKey = getExpirationKey(prevMin);
        // 取出当前这一分钟应当过期的 session
        Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
        // 注意:这里删除的是spring:session:expirations键,不是删除 session 本身!
        this.redis.delete(expirationKey);
        for (Object session : sessionsToExpire) {
            String sessionKey = getSessionKey((String) session);
            //遍历一下spring:session:sessions:expires键
            touch(sessionKey);
        }
    }
 
    /**
     * 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) {
        //并不是直接删除 key,而只是访问 key,通过惰性删除确保spring:session:sessions:expires键实时删除,
        // 同时也保证多线程并发续签的场景下,key移动到不同spring:session:expirations键里面时,
        //以spring:session:sessions:expires键实际ttl时间为准
        this.redis.hasKey(key);
    }

5、事件订阅

默认至少订阅键空间通知gxE事件(http://redisdoc.com/topic/notification.html)。

ConfigureNotifyKeyspaceEventsAction,开启键空间通知:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void configure(RedisConnection connection) {
       String notifyOptions = getNotifyOptions(connection);
       String customizedNotifyOptions = notifyOptions;
       if (!customizedNotifyOptions.contains("E")) {
           customizedNotifyOptions += "E";
       }
       boolean A = customizedNotifyOptions.contains("A");
       if (!(A || customizedNotifyOptions.contains("g"))) {
           customizedNotifyOptions += "g";
       }
       if (!(A || customizedNotifyOptions.contains("x"))) {
           customizedNotifyOptions += "x";
       }
       if (!notifyOptions.equals(customizedNotifyOptions)) {
           connection.setConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS, customizedNotifyOptions);
       }
   }

RedisHttpSessionConfiguration,注册监听事件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Bean
 public RedisMessageListenerContainer redisMessageListenerContainer(
         RedisConnectionFactory connectionFactory,
         RedisOperationsSessionRepository messageListener) {
             ......
     //psubscribe del和expired事件
     container.addMessageListener(messageListener,
             Arrays.asList(new PatternTopic("__keyevent@*:del"),
                     new PatternTopic("__keyevent@*:expired")));
     //psubscribe created事件
     container.addMessageListener(messageListener, Arrays.asList(new PatternTopic(
             messageListener.getSessionCreatedChannelPrefix() + "*")));
     return container;
 }

RedisOperationsSessionRepository,事件处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void onMessage(Message message, byte[] pattern) {
      ......
      if (channel.startsWith(getSessionCreatedChannelPrefix())) {
          ...
          //处理spring:session created事件
          handleCreated(loaded, channel);
          return;
      }
 
      //非spring:session:sessions:expires事件不做处理
      String body = new String(messageBody);
      if (!body.startsWith(getExpiredKeyPrefix())) {
          return;
      }
 
      boolean isDeleted = channel.endsWith(":del");
      if (isDeleted || channel.endsWith(":expired")) {
          ......
          if (isDeleted) {
              //处理spring:session:sessions:expires del事件
              handleDeleted(sessionId, session);
          }
          else {
              //处理spring:session:sessions:expires expired事件
              handleExpired(sessionId, session);
          }
          ......
          return;
      }
  }

事件订阅样例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
public class SessionExpiredListener implements ApplicationListener<SessionExpiredEvent> {
    @Override
    public void onApplicationEvent(SessionExpiredEvent event) {
        ......
    }
}

6、总结

Spring Session给我们提供了很好的分布式环境下资源共享问题解决思路,其基于Servlet 规范实现,业务使用时只需要简单配置就可以实现session共享,做到与业务低耦合,这都是以后我们项目开发中可以借签的设计理念。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
补习系列(15)-springboot 分布式会话原理
在 补习系列(3)-springboot 几种scope 一文中,笔者介绍过 Session的部分,如下:
美码师
2019/01/23
6340
Spring Session解决分布式Session问题的实现原理
使用Spring Session和Redis解决分布式Session跨域共享问题
Java后端技术
2018/08/09
8310
Spring Session解决分布式Session问题的实现原理
分布式共享Session之SpringSession源码细节
1. 概要 本文介绍SpringSession的主要功能的实现原理。在看源码的同时参照SpringSession开了一个“简化”版的Session框架--SimpleSession,简单好用,功能刚好够用,由于删除了很多SpringSession种用不到的功能,源码上可读性更好和自定义开发更容易。 2. 替代本地原生Session的秘密 几乎所有的方案都类似,使用 Filter 把请求拦截掉然后包装 Request 和 Response 使得 Request.getSession 返回的 Session 也
alexqdjay
2018/05/11
1.3K0
Spring Session - 使用Spring Session从零到一构建分布式session
https://spring.io/projects/spring-session-data-redis#samples
小小工匠
2021/08/17
3K0
spring-session配置 原
同大多数session共享工具一样,通常拿redis作为session存放地,以下为maven依赖
尚浩宇
2018/08/17
3490
面试题:你都知道哪些分布式Session实现方案?
面试官问了你一堆 dubbo 是怎么玩儿的,你会玩儿 dubbo 就可以把单块系统弄成分布式系统,然后分布式之后接踵而来的就是一堆问题,最大的问题就是分布式事务、接口幂等性、分布式锁,还有最后一个就是分布式 session。
JAVA葵花宝典
2019/07/23
6730
Spring-data-redis 的实现原理
强烈建议阅读Spring-session原理的"2.spring-session重写servlet request 及 redis实现存储相关问题"
平凡的学生族
2019/10/12
1.7K0
Spring-data-redis 的实现原理
帅气的 Spring Session 功能,基于 Redis 实现分布式会话,还可以整合 Spring Security!
摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Distributed-Session/ 「芋道源码」欢迎转载,保留摘要,谢谢!
芋道源码
2020/06/01
6.9K0
【SSO单点登录】分布式Session存在问题&& spring-session的设计之妙
当我们用nginx做负载均衡时,用户在A服务器登录了,A服务器存储了session,客户端也存储了cookie,其中有JSESSIONID。
用户6256742
2024/07/09
1330
【SSO单点登录】分布式Session存在问题&& spring-session的设计之妙
使用springSession完成分布式session
分布式session可以使用spring session来进行实现,由于HttpRequest的Wrapper功能,许多HttpRequest中的方法都可以进行替换来进行自定义的重写,可以在Wrapper中将getSession方法重写掉,然后进行自定义的session存储和处理,当然了,你能想到的,一般都是早就有人已经实现了,就不重复造轮子了
科技新语
2024/09/20
840
使用springSession完成分布式session
从Spring Session源码看Session机制的实现细节
去年我曾经写过几篇和 Spring Session 相关的文章,从一个未接触过 Spring Session 的初学者视角介绍了 Spring Session 如何上手,如果你未接触过 Spring Session,推荐先阅读下「从零开始学习Spring Session」系列(https://www.cnkirito.moe/categories/Spring-Session/) Spring Session 主要解决了分布式场景下 Session 的共享问题,本文将从 Spring Session 的源码
kirito-moe
2018/04/27
2.4K0
Spring Session 实现分布式会话管理
1、分布式会话管理是什么? 在Web项目开发中,会话管理是一个很重要的部分,用于存储与用户相关的数据。通常是由符合session规范的容器来负责存储管理,也就是一旦容器关闭,重启会导致会话失效。因此打造一个高可用性的系统,必须将session管理从容器中独立出来。 2、分布式会话管理的解决方案选用 实现方案有很多种,下面简单介绍下:   第一种是使用容器扩展来实现,大家比较容易接受的是通过容器插件来实现,比如基于Tomcat的tomcat-redis-session-manager,基于Jetty的jett
斯武丶风晴
2018/03/01
1.7K0
Spring Session 实现分布式会话管理
Spring Session关键类源码分析
要想使用spring session,还需要创建名为springSessionRepositoryFilter的SessionRepositoryFilter类。该类实现了Sevlet Filter接口,当请求穿越sevlet filter链时应该首先经过springSessionRepositoryFilter,这样在后面获取session的时候,得到的将是spring session。为了springSessonRepositoryFilter作为filter链中的第一个,spring session提
JavaEdge
2018/04/28
1.2K0
Spring Session关键类源码分析
理解分布式Session处理来看看spring怎么做的
​ Session 是我们在做java web项目 或者是其他的web项目时 一定会接触的,在学习中,常常被我们用来存储用户的一些关键信息,如:登录状态等
冷环渊
2022/11/07
4430
理解分布式Session处理来看看spring怎么做的
Spring Session 的原理
今天在写一个对外接口, 这个接口大致原理是在过滤器中通过 token 获取用户信息然后创建 session, 后续的流程就是 Controller -> Service -> Dao 了.
tuhooo
2022/07/20
3240
Externalizing Session State for a Spring Boot Application Using Spring-Session
Spring-session is a very cool new project that aims to provide a simpler way of managing sessions in Java based web applications. One of the features that I explored with spring-session recently was the way it supports externalizing session state without needing to fiddle with the internals of specific web containers like Tomcat or Jetty.
九州暮云
2019/08/21
4190
Externalizing Session State for a Spring Boot Application Using Spring-Session
使用Spring Session和Redis解决分布式Session跨域共享问题
对于分布式使用Nginx+Tomcat实现负载均衡,最常用的均衡算法有IP_Hash、轮训、根据权重、随机等。不管对于哪一种负载均衡算法,由于Nginx对不同的请求分发到某一个Tomcat,Tomcat在运行的时候分别是不同的容器里,因此会出现session不同步或者丢失的问题。
Java后端技术
2018/08/09
1.9K0
使用Spring Session和Redis解决分布式Session跨域共享问题
分布式session的几种解决方案,你中意哪种?
我发现了一个商城,我还没有登录,就可以往购物车中添加商品,加了好几件后,我准备付款,需要我先去登录,登录完之后付款。
Java旅途
2021/05/10
3390
分布式session的几种解决方案,你中意哪种?
redis管理spring-session
做负载均衡的时候,往往会遇到需要session同步的问题,方案很多,但是都有各种的缺点,最近发现spring-session项目,原理是将session保存到Redis,来实现session同步,好处是不管增加多少个tomcat,都没有问题,做负载的时候,也不用考虑session问题,而且配置灰常简单,因为我们的项目就是用redis来缓存数据的,只需要添加两行配置即可 1、添加spring-session <dependency> <groupId>org.springframework.sessi
用户1220053
2018/02/09
6890
从零开始的Spring Session(二)
上一篇文章 从零开始的Spring Session(一) 中介绍了一些Session和Cookie的基础知识,这篇文章开始正式介绍Spring Session是如何对传统的Session进行改造的。官网这么介绍Spring Session: Spring Session provides an API and implementations for managing a user’s session information. It also provides transparent integration
程序猿DD
2018/02/01
9310
从零开始的Spring Session(二)
推荐阅读
相关推荐
补习系列(15)-springboot 分布式会话原理
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文