Skip to content

20250225-实现Session共享多种解决方案

为何需要共享session?

目前,互联网公司项目大多是微服务化的分布式架构,不同微服务分布在多台不同的机器上。

在Nginx的反向代理下,会把用户的请求分发到不同的服务器上,但是如果用户的请求存放在该服务器A上,那么该用户的 sessionID 就存储在该服务器上 JVM 的一个以sessionID 为 key 的ConcurrentHashMap中。如果此时用户请求的一个服务模块可能需要调用到服务器B,当用户发起请求的时候,此时的服务器B上并没有存储该用户的sessionID,所以就会再次让用户进行一个登录操作。

所以,session共享方案在分布式环境中和微服务系统下,显得额外重要。

Nginx常用的几种反向代理的策略:

1.轮询策略。

2.权重比例策略。

3.ip_hash策略。

  1. 还可以自定义的策略。

方案1 基于Nginx的ip_hash负载均衡

就是把请求过来的IP地址对你的多台可用的服务器进行取模,然后把你的请求通过Nginx的反向代理给分发到对应的服务器上。这里会把可用的服务器放到一个数组当中,如果取模得到的结果是几,就会把请求分到服务器数组的下标中为几的服务器上。

ip_hash指令用于在Nginx中实现基于客户端IP地址的会话粘滞,确保每个客户端的请求都被转发到同一台服务器上。这对于保持会话状态非常有用,比如用户登录状态。

步骤

编辑Nginx配置文件,通常位于/etc/nginx/nginx.conf或者/etc/nginx/sites-available/目录下的特定站点配置文件。

httpserver块中配置ip_hash

nginx
 http {
    upstream myapp {
        ip_hash;  # 启用ip_hash
        server backend1.example.com weight=3;
        server backend2.example.com;
    }
 
    server {
        listen 80;
        server_name example.com;
 
        location / {
            proxy_pass http://myapp;  # 使用定义的upstream块
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

权重(weight=3)表示backend1.example.com将接收3倍于其他服务器的请求量。

优点:

  • 配置简单,对应用无侵入性,不需要修改代码
  • 只要hash属性均匀的,多台web-server的负载是均衡的
  • 便于服务器水平扩展
  • 安全性较高

缺点:

  • 服务器重启会造成部分session丢失
  • 水平扩展中也会造成部分session丢失
  • 存在单点负载高的风险

方案2 基于Tomcat的session复制

该解决方案就是处理用户请求的时候,把产生的session数据复制到系统所有的服务器中。

具体实现

  • 修改 server.xml 中的Cluster节点;
  • 修改应用 web.xml ,增加节点: <distribtable/>

优点

  • 配置简单,对应用无侵入性,不需要修改代码
  • 能适应各种负载均衡策略
  • 服务器重启或宕机不会造成session丢失
  • 安全性较高

缺点

  • session同步会有一定的延迟
  • 占用内网宽带资源
  • 受限于内存资源,水平扩展能力差
  • 服务器数量就多
  • 序列化反序列化消耗CPU性能

方案3 基于SpringSession+Redis

该方法就是把用户的请求时生成的session数据存放到Redis的服务器上,并设置一个失效时间,这样就能保证用户session有效期内,无论访问哪个节点,不需要进行再次登录。

基于cookies

导入依赖

xml
<?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>

书写配置文件

yaml
server:
  port: 8081
  servlet:
    context-path: /
spring:
  redis:
    host: 192.168.230.190
    port: 6379
    password: 123456

为项目启动类添加注解启用Spring-Session

java
@SpringBootApplication
@EnableRedisHttpSession
public class GlobalSessionApplication {
	public static void main(String[] args) {
		SpringApplication.run(GlobalSessionApplication.class, args);
	}
}

为项目添加控制类

java
@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中的值。

Redis中存储的数据格式

在 Redis 中,我们常用 : 作为分割符,spring:session:sessions 是默认的Redis HttpSession前缀。

每一个session都是hash结构,key 是 spring:session:sessions:709261a8-8c40-4097-8df2-92f88447063f。field有:

FieldValue说明
creationTime{ "fields": [ { "value": 1740574368566 } ], "annotations": [], "className": "java.lang.Long", "serialVersionUid": 4290774380558885855 }创建时间,采用毫秒数保存
maxInactiveInterval{ "fields": [ { "value": 1800 } ], "annotations": [], "className": "java.lang.Integer", "serialVersionUid": 1360826667806852920 }最大非活动间隔,默认1800秒,即30分钟
lastAccessedTime{ "fields": [ { "value": 1740574534138 } ], "annotations": [], "className": "java.lang.Long", "serialVersionUid": 4290774380558885855 }最后访问时间,采用毫秒数保存
sessionAttr:user"青山之巅"session存储数据内容

当我们重新访问localhost:8082/get时,我们可以看到lastAccessedTime会发生改变。

基于Token

配置文件

yaml
spring:
  session:
    store-type: redis

在跨域条件下配置同源策略

java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
		.allowedHeaders("*")
		.allowedMethods("*")
		.allowedOrigins("*")
		.exposedHeaders("token");
	}
    @Bean
    public HeaderHttpSessionIdResolver headerHttpSessionIdResolver(){
        return new HeaderHttpSessionIdResolver("token");
    }    
}

方案4 Session存放到Cookie

还不太理解

把session放到cookie中去,因为每次用户请求的时候,都会把自己的cookie放到请求中,所以这样就能保证每次用户请求的时候都能保证用户在分布式环境下,也不会在进行二次登陆。