分布式 Session 一致性?
说白了就是服务器集群 Session 共享的问题
Session 的作用?
Session 是客户端与服务器通讯会话跟踪技术, 服务器与客户端保持整个通讯的会话基本信息.
客户端在第一次访问服务端的时候, 服务端会响应一个 sessionId 并且将它存入到本地 cookie 中, 在之后的访问会将 cookie 中的 sessionId 放入到请求头中去访问服务器, 如果通过这个 sessionid 没有找到对应的数据那么服务器会创建一个新的 sessionid 并且响应给客户端.
分布式 Session 存在的问题?
假设第一次访问服务 A 生成一个 sessionid 并且存入 cookie 中, 第二次却访问服务 B 客户端会在 cookie 中读取 sessionid 加入到请求头中, 如果在服务 B 通过 sessionid 没有找到对应的数据那么它创建一个新的并且将 sessionid 返回给客户端, 这样并不能共享我们的 Session 无法达到我们想要的目的.
解决方案:
使用 cookie 来完成 (很明显这种不安全的操作并不可靠)
使用 Nginx 中的 ip 绑定策略, 同一个 ip 只能在指定的同一个机器访问 (不支持负载均衡)
利用数据库同步 session(效率不高)
使用 tomcat 内置的 session 同步 (同步可能会产生延迟)
使用 token 代替 session
我们使用 spring-session 以及集成好的解决方案, 存放在 Redis 中
目前项目中存在的问题
启动两个项目端口号分别为 8080,8081.
依赖:
- <!--springboot 父项目 -->
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.1.1.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <dependencies>
- <!--web 依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-Web</artifactId>
- </dependency>
- </dependencies>
创建测试类:
- @RestController
- public class TestSessionController {
- @Value("${server.port}")
- private Integer projectPort;// 项目端口
- @RequestMapping("/createSession")
- public String createSession(HttpSession session, String name) {
- session.setAttribute("name", name);
- return "当前项目端口:" + projectPort + "当前 sessionId :" + session.getId() + "在 Session 中存入成功!";
- }
- @RequestMapping("/getSession")
- public String getSession(HttpSession session) {
- return "当前项目端口:" + projectPort + "当前 sessionId :" + session.getId() + "获取的姓名:" + session.getAttribute("name");
- }
- }
YAML 配置:
- server:
- port: 8080
修改映射文件
- # 将本机 ip 映射到 www.hello.com 上
- 127.0.0.1 www.hello.com
在这里我们开启 nginx 集群, 修改配置:
- # 加入
- # 默认使用轮询,
- upstream backserver{
- server 127.0.0.1:8080;
- server 127.0.0.1:8081;
- }
- # 修改 server 中的 local
- location / {
- proxy_pass http://backserver;
- index index.html index.htm;
- }
我们直接通过轮询机制来访问首先向 Session 中存入一个姓名, http://www.hello.com/createSession?name=123
当前项目端口: 8081 当前 sessionId :0F20F73170AE6780B1EC06D9B06210DB 在 Session 中存入成功!
因为我们使用的是默认的轮询机制那么下次肯定访问的是 8080 端口, 我们直接获取以下刚才存入的值 http://www.hello.com/getSession
当前项目端口: 8080 当前 sessionId :C6663EA93572FB8DAE27736A553EAB89 获取的姓名: null
这个时候发现 8080 端口中并没有我们存入的值, 并且 sessionId 也是与 8081 端口中的不同.
别急继续访问, 因为轮询机制这个时候我们是 8081 端口的服务器, 那么之前我们是在 8081 中存入了一个姓名. 那么我们现在来访问以下看看是否能够获取到我们存入的姓名: SimpleWu, 继续访问: http://www.hello.com/getSession
当前项目端口: 8081 当前 sessionId :005EE6198C30D7CD32FBD8B073531347 获取的姓名: null
为什么 8080 端口我们没有存入连 8081 端口存入的都没有了呢?
我们仔细观察一下第三次访问 8081 的端口 sessionid 都不一样了, 是因为我们在第二次去访问的时候访问的是 8080 端口这个时候客户端在 cookie 中获取 8081 的端口去 8080 服务器上去找, 没有找到后重新创建了一个 session 并且将 sessionid 响应给客户端, 客户端又保持到 cookid 中替换了之前 8081 的 sessionid, 那么第三次访问的时候拿着第二次访问的 sessionid 去找又找不到然后又创建. 一直反复循环.
如何解决这两个服务之间的共享问题呢?
spring 已经给我们想好了问题并且已经提供出解决方案: spring-session 不了解的可以去百度了解下.
我们首先打开 Redis 并且在 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>
修改 YAML 配置文件:
- server:
- port: 8081
- spring:
- Redis:
- database: 0
- host: localhost
- port: 6379
- jedis:
- pool:
- max-active: 8
- max-wait: -1
- max-idle: 8
- min-idle: 0
- timeout: 10000
- Redis:
- hostname: localhost
- port: 6379
- #password: 123456
添加 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 connectionFactory() {
- JedisConnectionFactory connection = new JedisConnectionFactory();
- connection.setPort(port);
- connection.setHostName(hostName);
- //connection.setPassword(password);
- // connection.setDatabase(0);
- return connection;
- }
- }
初始化 Session 配置
- // 初始化 Session 配置
- public class SessionInitializer extends AbstractHttpSessionApplicationInitializer {
- public SessionInitializer() {
- super(SessionConfig.class);
- }
- }
然后我们继续启动 8080,8081 来进行测试:
首先存入一个姓名 http://www.hello.com/createSession?name=123:
当前项目端口: 8080 当前 sessionId :cf5c029a-2f90-4b7e-8345-bf61e0279254 在 Session 中存入成功!
应该轮询机制那么下次一定是 8081, 竟然已经解决 session 共享问题了那么肯定能够获取到了, 竟然这样那么我们直接来获取下姓名 http://www.hello.com/getSession:
当前项目端口: 8081 当前 sessionId :cf5c029a-2f90-4b7e-8345-bf61e0279254 获取的姓名: 123
这个时候我们发现不仅能够获取到值而且连 sessionid 都一致了.
实现原理:
就是当 Web 服务器接收到 http 请求后, 当请求进入对应的 Filter 进行过滤, 将原本需要由 Web 服务器创建会话的过程转交给 Spring-Session 进行创建, 本来创建的会话保存在 Web 服务器内存中, 通过 Spring-Session 创建的会话信息可以保存第三方的服务中, 如: Redis,MySQL 等. Web 服务器之间通过连接第三方服务来共享数据, 实现 Session 共享!
来源: http://www.jianshu.com/p/d96930cf3455