Session共享
为什么会有session共享
- 目前互联网公司的项目大多是在微服务华和分布式的环境中进行搭建的,这就会导致一个项目很有可能分布部署在几个甚至很多的服务器集群下,此时就会出现一个问题
- 当用户进行一个session会话时,比如一个用户去登录项目,一般的大公司的项目都是有Nginx进行反向代理的,在此简单举例一下Nginx常用的几种反向代理的策略: 1.轮询策略 , 2.权重比例策略 , 3.ip_hash策略 , 4. 还可以自定义的策略 , 在Nginx的反向代理下,一般会把用户的请求分发到不同的服务器上,但是如果用户的请求存放在该服务器A上,那么该用户的sessionID 就存储在该服务器上JVM的一个ConcurrentHashmap中,以sessionID为key.
- 但如果此时用户请求的一个服务模块可能需要调用到服务器B,当用户发起请求的时候,此时的服务器B上并没有存储该用户的sessionID, 所有就会再次让用户进行一个登录操作,还有可能会导致用户本来就想单纯的完成一个下单操作,但却登录了好几次的清空
- 所有session共享方案在分布式环境中和微服务系统下,显得额外重要
解决方案一 基于Nginx的Ip_hash负载均衡
- 就是把请求过来的ip地址对你的多台可用的服务器进行取模,然后把你的请求通过Nginx的反向代理给分发到对应的服务器上.
这里会把可用的服务器放到一个数组当中,如果取模得到的结果是几,就会把请求分到服务器数组的下标中为几的服务器上
具体实现
- 需要在Nginx.conf文件中进行对应的修改, 添加自己可用的服务器
upstream zx.com {
ip_hash;
server localhost:8081 weight=1;
server localhost:8082 weight=2;
server localhost:8083 weight=3;
server localhost:8084 weight=4;
server localhost:8085 backup;
server localhost:8086 backup;
}
server {
listen 8088;
server_name localhost;
}
优点:
- 配置简单,对应用无侵入性,不需要修改代码
- 只要hash属性均匀的,多台web-server的负载是均衡的
- 便于服务器水平扩展
- 安全性较高
缺点
- 服务器重启会造成部分session丢失
- 水平扩展中也会造成部分session丢失
- 存在单点负载高的风险
解决方案二: 基于Tomcat的session复制
- 该解决方案就是当用户请求的时候,把产生的sessionID复制到系统所有的服务器中,这样就能保证当前用户请求的时候从服务器A可能调用到服务器B的时候,也能保证服务器B也能有该用户的sessionID ,这样也就解决了
具体实现
- 修改server.xml中的Cluster节点;
- 修改应用web.xml,增加节点: <distribtable/>
优点:
- 配置简单,对应用无侵入性,不需要修改代码
- 能适应各种负载均衡策略
- 服务器重启或宕机不会造成session丢失
- 安全性较高
缺点
- session同步会有一定的延迟
- 占用内网宽带资源
- 受限于内存资源,水平扩展能力差
- 服务器数量就多
- 序列化反序列化消耗CPU性能
解决方案三: 使用Redis做缓存session的统一缓存
- 该方法就是把每次用户的请求的时生成的sessionID存放到Redis的服务器上.在基于Redis的特性进行设置一个失效时间的机制,这样就能保证用户在我们设置的Redis中的session失效时间内,都不需要进行再次登录
具体实现
一. SpringBoot下基于Cookie实现的SpringSession
- 导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>No10_Session</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- SessionRedis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
</dependencies>
</project>
- 书写配置文件
server:
port: 8081
servlet:
context-path: /
spring:
redis:
host: 192.168.230.190
port: 6379
password: 123456
- 为项目启动类添加注解启用Spring-session
@SpringBootApplication
@EnableRedisHttpSession
public class GlobalSessionApplication {
public static void main(String[] args) {
SpringApplication.run(GlobalSessionApplication.class, args);
}
}
- 为项目添加控制类
@RestController
public class SessionController {
@Value("${server.port}")
private Integer pore;
@GetMapping("/set")
public String set (HttpSession session){
session.setAttribute("user","张三");
return String.valueOf(pore);
}
@GetMapping("/get")
public String get (HttpSession session){
return "user: "+session.getAttribute("user")+" \n 端口号: "+pore;
}
}
-
启动项目并查看Redis中的值
启动项目时配置端口号
存储数据的查看
-
在上述操作中已经完成了session共享但是会发现Redis中的数据出现了乱码的情况 这是因为默认采用的的是JDK序列化方式存储的,可读性比较差,隐藏我们可以补充添加Spring序列化方法
public class SessionConfig {
@Bean
public RedisSerializer springSessionDefaultRedisSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
defaultCookieSerializer.setCookiePath("/"); //解决contextPath不同的问题
// 设置cookie 的一级域名,不设置时默认与url中的域名一致
// cookieSerializer.setSameSite(null); //解决axios跨域携带cookie无效问题
return defaultCookieSerializer;
}
}
这样整个问题就解决了!!!
Redis中的存储说明:
- spring:session 是默认的Redis HttpSession 前缀(redis中,我们常用’ : ’ 作为分割符).
- 每一个session都会创建3组数据
-
hash结构, spring-session存储的主要内容
spring:session:sessions:709261a8-8c40-4097-8df2-92f88447063f
hash结构有key和field,如上面的例子:hash的key为"spring:session:sessions"前缀加709261a8-8c40-4097-8df2-92f88447063f,该key下的field有:-
key=sessionAttr:user, value=“张三” // session存储数据内容
-
key=creationTime, value=1644302828502 //创建时间(采用毫秒数保存)
-
key=maxInactiveInterval, value=1800 //最大非活动间隔(默认1800秒,即30分钟)
-
key=lastAccessedTime, value=1644302833137 //最后访问时间(采用毫秒数保存)
当我们重新访问localhost:8082/get时,我们可以看到lastAccessedTime会发生改变。
-
-
String结构,用于ttl过期时间记录
spring:session:sessions:expires:70924e71-c540-4097-8df2-92f88447063f key为“spring:session:sessions:expires:”前缀+70924e71-c540-4097-8df2-92f88447063fvalue为空 -
set结构,过期时间记录
spring:session:expirations:16443046000000
set的key固定为“spring:session:expirations:16443046000000”set的集合values为:- expires:c7fc28d7-5ae2-4077-bff2-5b2df6de11d8 //(一个会话一条)
简单提一下:redis清除过期key的行为是一个异步行为且是一个低优先级的行为,用文档中的原话来说便是,可能会导致session不被清除。于是引入了专门的expiresKey,来专门负责session的清除,包括我们自己在使用redis时也需要关注这一点。在开发层面,我们仅仅需要关注第一组hash结构数据就行了。
二. SpringBoot下基于Token实现的SpringSession
- 修改配置类
@Bean
public HeaderHttpSessionIdResolver headerHttpSessionIdResolver(){
return new HeaderHttpSessionIdResolver("token");
}
- 书写配置文件
spring:
session:
store-type: redis
- 在跨域条件下配置同源策略
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*")
.exposedHeaders("token");
}
}
解决方案四: Session存放到Cookie
把session放到cookie中去,因为每次用户请求的时候,都会把自己的cookie放到请求中,所以这样就能保证每次用户请求的时候都能保证用户在分布式环境下,也不会在进行二次登陆。