分布式中session共享的解决方案:spring-session
Session是客户端与服务器通讯会话跟踪技术,是服务器与客户端保持整个通讯的会话基本信息。客户端在第一次访问服务器的时候,服务端会响应一个sessionId并且将它存入到本地的Cookie中,在之后的访问会将Cookie中的sessionId放入到请求头中去访问服务器,如果通过这个sessionId没有找到对应的数据,那么服务器就会创建一个新的sessioinId并且响应给客户端。分布式Session的一致性说白了就是服务器集群Session共享的问题。
分布式中Session存在的共享问题
假设客户端第一次访问服务A,服务A响应返回了一个sessionId并且存入了本地Cookie中。第二次不访问服务A了,转去访问服务B。因为客户端中的Cookie中已经存有了sessionId,所以访问服务B的时候,会将sessionId加入到请求头中,而服务B因为通过sessionId没有找到相对应的数据,因此它就会创建一个新的sessionId并且响应返回给客户端。这样就造成了不能共享Session的问题。
分布式中Session共享问题的解决方案
1.根据Cookie来完成(不安全)。
2.使用Nginx的IP绑定策略,同一个IP只能在指定的同一个机器访问(不支持负载均衡)。
3.利用数据库同步Session(效率不高)。
4.使用Tomcat内置的Session同步机制(同步可能会产生延迟)。
5.使用Token代替Session。
6.使用Spring-Session以及集成好的解决方案,存放在Redis中。
项目实例场景还原
启动两个Spring Boot项目,端口号分别是8081,8182。
在两个项目中分别创建SessionSharedController类。
@RestController public class SessionSharedController { @Value("${server.port}") private Integer projectPort; // 项目端口 @RequestMapping("/createSession") public String createSession(HttpSession session, String name) { session.setAttribute("name", name); return "【当前项目端口:" + projectPort + "】 【当前sessionId :" + session.getId() + "】"; } @RequestMapping("/getSession") public String getSession(HttpSession session) { return "【当前项目端口:" + projectPort + "】 【当前sessionId :" + session.getId() + "】 【获取的姓名:" + session.getAttribute("name") + "】"; } }
使用Nginx集群,通过修改nginx.conf配置文件使之支持轮询策略(默认)的负载均衡。
# 开启轮询策略(默认)的负载均衡 upstream balanceserver{ server 127.0.0.1:8081; server 127.0.0.1:8082; }
# 将请求转发到负载均衡配置的服务器上 location / { proxy_pass http://balanceserver; index index.html index.htm; }
我们直接通过轮询机制来访问首先向Session中存入一个姓名。
访问:http://localhost/createSession?name=yanggb
得到:【当前项目端口:8081】 【当前sessionId :D5312CBE049C0F486315CF550BFB255C】
因为我们使用的是默认的轮询策略,因为这次访问的是8081端口,那么下次访问的肯定是8082端口,我们可以直接获取到刚才存入Session的值。
访问:http://localhost/getSession
得到:【当前项目端口:8082】 【当前sessionId :D85157E33965BE6D7BB1E1CC0E43208F】 【获取的姓名:null】
这个时候我们会发现,8082端口中并没有我们存入的值,并且sessionId也是与8081端口不同。先想一想,这个时候我们是8082端口的服务器,但是之前我们是在8081端口中存入了一个姓名,那么我们现在来看看访问8081端口是否能获取到之前存入的姓名yanggb。
访问:http://localhost/getSession
得到:【当前项目端口:8081】 【当前sessionId :C5E2061BB03CE8FFE3E9FBDA00CFA28C】 【获取的姓名:null】
显然,8081端口中也获取不到之前存入的姓名yanggb。如果仔细地观察的话,会发现连sessionId都不一样了。原因是因为,在第二次去访问负载均衡服务器的时候,访问的是8082端口的服务器,这个时候客户端在cookie中获取到的是第一次访问8081端口的服务器时响应返回的sessionId,拿这个sessionId去8082端口的服务器上找是找不到的,因此8082端口就重新创建了一个sessionId并将这个sessionI响应返回给客户端,客户端拿这个sessionId替换掉了之前的8081端口服务器响应返回的sessionId。这样,当第三次访问的是8081端口的服务器的时候,就拿了一个在8081端口的服务器上找不到的sessionId去请求,导致又创建一个新的sessionId。这样就陷入了反复循环的境地,两个服务器永远拿到的是对方生成的sessionId,拿不到自己生成的sessionId。
解决这两个服务之间Session共享问题的方案:Spring-Session
Spring提供了一个解决方案:Spring-Session用来解决两个服务之间Session共享的问题。
要使用Spring-Session,需要在pom.xml中添加相关依赖。
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring session 与redis应用基本环境配置,需要开启redis后才可以使用,不然启动Spring boot会报错 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
同时,需要修改application.properties配置文件(本地要开启redis服务)。
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=8
spring.redis.timeout=10000
然后再在代码中添加Session配置类。
/** * 这个类用配置redis服务器的连接 * maxInactiveIntervalInSeconds为SpringSession的过期时间(单位:秒) */ @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) public class SessionConfig { // 冒号后的值为没有配置文件时,制动装载的默认值 @Value("${redis.hostname:localhost}") private String hostName; @Value("${redis.port:6379}") private int port; // @Value("${redis.password}") // private String password; @Bean public JedisConnectionFactory jedisConnectionFactory() { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); redisStandaloneConfiguration.setHostName(hostName); redisStandaloneConfiguration.setPort(port); // redisStandaloneConfiguration.setDatabase(0); // redisStandaloneConfiguration.setPassword(RedisPassword.of("123456")); return new JedisConnectionFactory(redisStandaloneConfiguration); } @Bean public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { return new StringRedisTemplate(redisConnectionFactory); } }
初始化Session配置
/** * 初始化Session配置 */ public class RedisSessionInitializer extends AbstractHttpSessionApplicationInitializer { public RedisSessionInitializer() { super(RedisSessionConfig.class); } }
这个时候再重新跑一次上面的测试,会发现能够拿到相同的Session信息,也就是实现了Session的共享。
Spring-Sesion实现的原理
当Web服务器接收到请求后,请求会进入对应的Filter进行过滤,将原本需要由Web服务器创建会话的过程转交给Spring-Session进行创建。Spring-Session会将原本应该保存在Web服务器内存的Session存放到Redis中。然后Web服务器之间通过连接Redis来共享数据,达到Sesson共享的目的。
"你离开以后,我遇见过很多女孩,像你的眉,像你的眼,但都不是你。"