highlight: xcode
theme: vuepress
问题引入:什么是分布式Session?
分布式 Session 是指在多台服务器之间共享和管理用户的会话数据,使得用户的会话状态能够在不同的服务器上保持一致。这样,无论用户的请求被路由到哪台服务器,都能够访问到相同的会话信息,从而保证用户体验的一致性。
回顾一下单机服务的 HttpSession 的存储:
在传统的 JavaWeb 的 Tomcat + Servlet 的项目中,HttpSession 通常存储在 JVM 内存中。浏览器第一次访问服务之后,会得到一个名为 JSESSIONID
的 Cookie。在后续的请求中,浏览器都会携带此 Cookie。用一个图来简单说明一下:
在 HttpSession 中存储用户数据最原始的做法是怎么做的呢?初学者一般都是这么写(因为教科书上也是这么写的🐶):
java public void doGet(HttpServletRequest req, HttpServletResponse resp) { HttpSession session = req.getSession(); // 获取用户名 String username = req.getParameter("username"); session.setAttribute("username", username); // 后续操作... }
这种写法只适合单机部署的场景,在分布式场景下是不可行的,因为请求会到不同的机器上,每台机器上的数据都不一样。而且服务端对同一个客户端的请求不能共享 Session。
常见解决方案
对于分布式 Session,通常有以下几种解决方案:
- Session 粘滞:通过负载均衡器(如 Nginx、F5 等)将同一用户的请求始终路由到同一台服务器上。
- 数据库共享 Session:将 Session 存储在数据库中(如 MySQL、PostgreSQL 等),各个服务器访问同一个数据库。
- Token 方式:将 Session 数据编码为 Token(如 JWT),并由客户端保存(通常在 Cookie 或 HTTP 头中传输)。
- 分布式文件系统:将 Session 存储在分布式文件系统(如 NFS、GlusterFS 等)中,各个服务器访问共享的文件系统。
- 缓存共享 Session:使用分布式缓存系统(如 Redis、Memcached 等)存储 Session 数据,各个服务器共享同一个缓存。
Session 粘滞
以 nginx 的 IP Hash 策略为例,通过 IP Hash 策略可以将客户端的 IP 地址哈希到特定的后端服务器上,从而确保同一个客户端的请求总是被路由到同一台服务器上。
示例配置:
```conf http { upstream backend { ip_hash; server backend1.example.com; server backend2.example.com; server backend3.example.com; }
server {
location / {
proxy_pass http://backend;
}
}
} ```
寥寥几行,通过简单的配置即可实现。同一客户端的请求总是路由到同一台服务器,因此可以保证会话的一致性,避免 Session 丢失。
但是缺点也很明显,由于 IP 地址的分布不均匀,可能导致某些服务器负载过重,而其他服务器负载较轻。如果某台服务器宕机,与其绑定的所有客户端会话信息都会丢失,无法自动迁移到其他服务器。添加或移除服务器会改变 IP 到服务器的映射,导致会话丢失。对于跨地域的用户,IP Hash 可能导致某些地域的用户集中到特定的服务器,进一步加剧负载不均衡。
明显缺点大于优点,所以基本不推荐。
数据库共享Session
使用数据库存储 Session,虽然实现起来很简单,但是会导致每次请求都要查询数据库让数据库的负载变大。 在一些性能敏感的系统中,性能瓶颈明显,扩展性很差。所以也不推荐。
Token方式
将 Session 数据编码为 Token,并由客户端保存可以让服务端无状态化,扩展性好,不依赖集中存储。但是 Token 很可能被截取和伪造,安全性低,需要加密和签名。也不是很推荐。
分布式文件系统
使用分布式文件系统才存储 Session,实现起来相对简单,可以利用现有的文件系统。但是性能比较差,文件系统的同步和一致性问题需要考虑,所以也不是很推荐。
缓存共享Session
上面说了几种方案都不推荐,这个总得推荐了吧?哈哈哈,那肯定啊,不然这篇文章都没有写的必要了。
使用缓存系统存储 Session,读写速度快,而且支持高并发。需要注意的是缓存失效策略。以 Redis 为例存储分布式 Session。
创建一个 Spring Boot 工程,导入 Spring Session 的依赖:
```xml org.springframework.boot spring-boot-starter-web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<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>
```
配置一下 application.yml 文件:
```yaml server: port: 8081
spring: redis: port: 6379 host: localhost ```
写一个简单的 Controller:
```java @RestController public class TestController {
@GetMapping("/")
public String hello(HttpServletRequest req) {
HttpSession session = req.getSession(true);
System.out.println(session);
Cookie[] cookies = req.getCookies();
if (cookies == null) {
return "Hello World";
}
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
}
return "Hello World";
}
} ```
访问一下浏览器:
可以看到名称为 JSESSIONID 的 Cookie。
再看一下 Redis 中的数据:
可以看到 Session 数据已经自动存到 Redis 中了。你可能疑问为什么会自动存进去了,我并没有干什么操作啊?
因为 spring-session-data-redis 这个包在 Spring 接收到了请求的时候自动帮我们做了这个操作。有兴趣的话可以分析一下源码。
我们可以再开启一个实例观察一下 Cookie 是否发生了变化:
访问一下 http://localhost:8082 然后打开 F12 查看 Cookie:
可以看到 8081 和 8082 的 JSESSIONID 的 Cookie 是一样的,最终实现了 Session 的共享。
怎么样,你学会了吗?