文章目录
- 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
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 官方文档