上一篇 《分布式 Session 常见的几种解决方案》中提到了 session 集中管理方案,这里将记录具体的方案实现。
方案中使用单独的 session 服务器来集中管理 session,而 session 服务器有多种选择,如 redis、mango、MySQL 等 DB,这里我们选择 redis 来管理 session,然后使用 spring 官方提供的 spring-session 来处理 session 的共享问题。
这也是目前企业开发用的比较多的一种分布式 session
解决方案:spring-session + redis
。
1. Spring-Session 介绍
Spring 提供了处理分布式session的解决方案——Spring Session。Spring Session 提供了用于管理用户会话的 API 和 实现。
Spring Session 提供了对 redis,mongodb,mysql 等常用的存储库的支持,Spring Session 提供与 HttpSession 的透明整合,这意味着开发人员可以使用 Spring Session 支持的实现切换 HttpSession 实现。
Spring Session 添加了一个 SessionRepositoryFilter 的过滤器,用来修改包装请求和响应,包装后的请求为 SessionRepositoryRequestWrapper,调用 getSession() 方法的时候实际上就是调用 Spring Session 实现了的 session。
2. 实现分布式 Session 共享
Spring Session 使用非常简单,添加了相关依赖后,直接操作 HttpSession
就可以实现效果。
:::info 实现流程:
- 启动运行 Redis 服务;
- 引入 Spring-Session 组件
- 演示统一程序启动 8081 端口和 8082 端口模拟 2 个服务分布式;
- 调用 8081 登录接口 /user/login;
- 调用 8081 获取用户信息接口 /user/info;
- 调用 8082 获取用户信息接口 /user/info; :::
(1) 运行 redis 容器
$ docker run -d -p 6379:6379 redis:5
(2) 添加 Spring Session 和 Redis 的相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
(3) 配置 redis 相关信息
spring:
redis:
# redis库
database: 0
# redis 服务器地址
host: localhost
# redis 端口号
port: 6379
# redis 密码
password:
# session 使用redis存储
session:
store-type: redis
timeout: 3600
(4) 示例代码
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/login")
public String login(@RequestParam String username,
@RequestParam String password,
HttpSession session){
//账号密码正确,登录成功,保存状态
session.setAttribute("login_user", username);
return "登录成功";
}
@GetMapping("/info")
public String info(HttpSession session) {
return "当前登录的是:" + session.getAttribute("login_user");
}
(5) 运行后 Redis 中存储内容
Redis 中每个 Session 存储了三条信息:
- login_user:session 在 redis db 下所属的 namespace,可以在 application.yaml 中配置;
- login_user:sessions:d65c9c19-f681-4393-9e83-a8cc41d6b2c4:Hash 结构,存储 spring-session 的主要内容:
- hash 结构有 key 和 field,key 为 “
Namespace:sessions:SessionID
“,该 key 下的 filed 有:- sessionAttr:login_user,存入 session 中的键值对
- maxInactiveInterval
- createTime
- lastAccessedTime
- 这个 key 的过期时间为 Session 的最大过期时间 +5 分钟,如果默认最大过期时间为 30 分钟,则这个 key 的过期时间为 35 分钟;
- hash 结构有 key 和 field,key 为 “
d65c9c19-f681-4393-9e83-a8cc41d6b2c4
为这个 Session 的 ID,而存储在 Cookie 中的 SessionID 是经过 base64 编码过后的值:ZDY1YzljMTktZjY4MS00MzkzLTllODMtYThjYzQxZDZiMmM0
- login_user:sessions:expires:d65c9c19-f681-4393-9e83-a8cc41d6b2c4:String 结构,不存储任何有用的值,只是表示 Session 过期而设置;
- key 为 “
Namespace:sessions:expires:SessionID
“,value 为空; - 这个 key 在 Redis 中的过期时间即为 Session 的过期时间间隔;
- key 为 “
- login_user:sessions:expirations:1622590980000:Set 结构,用来存储 Session ID,一条 session 有一个 key;
- 这个 key 中的最后 1622590980000 值是一个时间戳,根据这个 Session 过期时刻滚动至下一分钟而计算得出;
(6) Spring-Session 对 Session 过期事件的处理
Spring-Session 借助于 Redis 实现了 Session 共享,但是 Session 的过期事件仍然需要处理。
Redis 可以监听某个 key 的变化,当 key 发生变化时,可以快速做出相应的处理。
但是 Redis 监听 key 过期,有两种方式:
- 当访问时发现其过期
- Redis 后台逐步查找过期 key
虽然当访问 key 时会发现其过期,并产生过期事件,但是无法保证 key 的过期事件抵达后立即生成过期事件。
所以 Spring-Session 为了能够及时的产生 Session 的过期事件,除了保存 session 内容外,还增加了:
login_user:sessions:**expires**:d65c9c19-f681-4393-9e83-a8cc41d6b2c4
login_user:sessions:**expirations**:1622590980000
Spring-Session 中有个定时任务,每个整分钟都会查询相应的login_user:sessions:**expirations**:整分钟时间戳
中的过期 Session ID,然后再访问一次这个 Session ID,即 login_user:sessions:**expires**:SessionID
,以便能够让 Redis 及时的产生 key 过期事件,即 Session 过期事件。