文章目录

  • 1. Cookie Session
  • 2. Spring Session + Redis
  • 3. Redis 中的 Spring Session


1. Cookie Session

由于Http协议是无状态的协议,为了能够记住请求的状态,于是引入了Session和Cookie的机制。

Session是存在于服务器端的,在单体式应用中,它是由Tomcat管理的,存在于Tomcat的内存中。

当我们为了解决分布式场景中的Session共享问题时,引入了Redis,其共享内存,以及支持key自动过期的特性,非常契合Session的特性,我们在企业开发中最常用的也就是这种模式。但是只要你愿意,也可以选择存储在JDBC,Mongo中,这些,Spring都提供了默认的实现,在大多数情况下,我们只需要引入配置即可。

Cookie则是存在于客户端,更方便理解的说法,可以说存在于浏览器。Http协议允许从服务器返回Response时携带一些Cookie,并且同一个域下对Cookie的数量有所限制,之前说过Session的持久化依赖于服务端的策略,而Cookie的持久化则是依赖于本地文件。虽然说Cookie并不常用,但是有一类特殊的Cookie却是我们需要额外关注的,那便是与Session相关的sessionId,他是真正维系客户端和服务端的桥梁。

当服务端往Session中保存一些数据时,Response中自动添加了一个Cookie:JSESSIONID:xxxx,再后续的请求中,浏览器也是自动的带上了这个Cookie,服务端根据Cookie中的JSESSIONID取到了对应的Session。

客户端服务端是通过JSESSIONID进行交互的,并且,添加和携带key为JSESSIONID的Cookie都是Tomcat和浏览器自动帮助我们完成的。

不同浏览器,访问是隔离的,甚至重新打开同一个浏览器,JSESSIONID也是不同的。但是存放在客户端的Cookie还是存在安全问题的,可修改Cookie“骗”过了服务器。

Spring Sessoin使用第三方仓储来实现集群Session管理,也就是常说的分布式Session容器,替换应用容器(如Tomcat的Session容器)。仓储的实现,Spring Session提供了三个实现(redis,mongodb,jdbc),其中Redis使我们最常用的。

2. Spring Session + Redis

添加依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--spring2.0集成redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.4.2</version>
</dependency>

添加Redis配置

spring:
  redis:
    # Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
    database: 0
    host: localhost
    port: 6379
    # 连接密码(默认为空)
    password:
    # 连接超时时间(毫秒)
    timeout: 10000ms
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-wait: -1
        # 连接池中的最大空闲连接 默认 8
        max-idle: 8
        # 连接池中的最小空闲连接 默认 0
        min-idle: 0

添加注解@EnableRedisHttpSession,可自定义Cookie的名称,默认为SESSION

@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setCookiePath("/");
        defaultCookieSerializer.setCookieName("SESSION_TEST");
        return defaultCookieSerializer;
    }

}

Spring Session的核心流程是在SessionRepositoryFilter的doFilterInternal方法中。

SessionRepositoryFilter的核心操作是用来修改包装请求和响应,负责包装切换HttpSession至Spring Session的请求和响应。每个HttpRequest进入,都会被该Filter包装成切换Session的请求很响应对象

3. Redis 中的 Spring Session

springboot Session 存储到redis springsession redis存储结构_spring


spring:session是默认的Redis HttpSession前缀。

类型


数据结构

TTL

A

spring:session:sessions:bcfc0d30-290b-40c5-bdb9-8303c9449c7d

Hash

35分钟

B

spring:session:expirations:1586512320000

Set

30分钟

C

spring:session:sessions:expires:bcfc0d30-290b-40c5-bdb9-8303c9449c7d

String

30分钟

A类型键
将内存中的Session信息序列化到了Redis中。

创建时间creationTime;最后访问时间lastAccessedTime;最大间隔maxInactiveInterval;若有数据存在Session中,也是存在该Hash中,

B类型键
可当作一个桶,存放着这一分钟应当过期的 Session 的 key,具体值是C类型键
时间计算:lastAccessedTime向上取整 + maxInactiveInterval * 1000

后台有一个定时任务去“删除”过期的 key,来补偿 Redis 到期未删除的 key。
具体操作就是取得当前时间的时间戳作为 key,去 redis 中定位到 spring:session:expirations:{当前时间戳} ,这个 set 里面存放的便是所有过期的 key 了。

每次 Session 的续签,需要将旧桶中的数据移除,放到新桶中。前后相隔一分钟访问,会发现桶发生了变化。
当众多用户活跃时,桶的增删和以及 Set 中数据的增删都是很频繁的,并且对应 key 的 ttl 时间也会被更新。

如果只有A和B两个键可能存在并发问题,前后两分钟都进行续签,可能导致后续签的Session放在了前一个续签桶里,导致Session提前失效。

并且在Spring Session 中 A 类型键的过期时间是 35 分钟,这意味着即便 Session 已经过期,我们还是可以在 Redis 中有 5 分钟间隔来操作过期的 Session。于此同时,Spring Session 引入了 C 类型键来作为 Session 的引用。

C类型键
具体作用便是在自身过期后触发 Redis 的 keyspace notifications,keyspace notifications 只会告诉我们哪个键过期了,不会告诉我们内容是什么。关键就在于如果 Session 过期后监听器可能想要访问 Session 的具体内容,然而自身都过期了,还怎么获取内容。

解耦 Session 的存储和过期,并且使得 server 获取到过期通知后可以访问到 Session 真实的值。
对于用户来说,C 类型键过期后,意味着登录失效,而对于服务端而言,真正的过期其实是 A 类型键过期,这中间会有 5 分钟的误差。

总结
Spring Session 使用这B和C类型键来维持Session过期,Redis清除过期key的行为是一个异步行为且是一个低优先级的行为,可能会导致session不被清除。于是引入了专门的expiresKey,来专门负责Session的清除.

Spring Session 是为了严谨而设计了这一套方案,但引入了定时器和很多辅助的键值对,无疑对内存消耗和 cpu 消耗都是一种浪费。如果在生产环境大量使用 Spring Session,最好权衡下相关问题。

参考:
从零开始的Spring Session(一)从零开始的Spring Session(二)从零开始的Spring Session(三)从Spring-Session源码看Session机制的实现细节Redis键空间通知spring-session(一)揭秘spring-session(一)揭秘续篇Spring Session 官方文档