大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
分布式会话:登录态为什么必须进 Redis
这一课你将能做到
- 画出「内存会话 + 多实例」的失败现场,说清为什么登录态必须集中存储
- 真实登录一次,用 redis-cli 找到 satoken: 前缀的会话 key(只读,不删),说出 Sa-Token 在 Redis 里存了什么、TTL 是多少
- 解释 timeout=86400、is-concurrent、is-share 三个配置的含义和产品效果,并演示开关关闭时会话落回内存
为什么要学
本站登录用的是 Sa-Token,默认把会话存在 JVM 内存里。开发时单实例没问题;但生产是多实例(或将来扩容、重启):
- 重启即全员下线:进程一死,内存里的登录态全没,所有用户重新登录
- 多实例登录精神分裂:Nginx 把请求轮询到 1 号实例登录,下一个请求落到 2 号实例——2 号说「你谁啊」,要求重新登录;每次请求都可能在实例间弹跳
解法只有一个:会话放到所有实例都能读的中心存储——Redis。本站生产 profile 里 codeforge.redis.enabled=true 时,sa-token-redis-jackson 自动接管会话存储。这一课让你亲眼看到它写了什么。
先建立图像
【内存会话 + 2 实例】
浏览器 ──token──▶ Nginx ──▶ 实例1(内存里有 token ✅)
│
└──▶ 实例2(内存里没有 ❌ → 401,请重新登录)
(轮询命中不同实例 → 登录态时有时无)
【Redis 会话 + 2 实例】
浏览器 ──token──▶ Nginx ──▶ 实例1 ─┐
│ ├─ 都查同一个 Redis → 结果一致 ✅
└──▶ 实例2 ───────┘
跟着做
步骤 1:读本站的 Sa-Token 配置
backend/src/main/resources/application.yml:
sa-token:
token-name: Authorization # 前端请求头名字
timeout: 86400 # 会话有效期:24 小时(秒)
is-concurrent: true # 同一账号允许多端同时登录
is-share: true # 多端登录共享同一个 token
token-style: uuid # token 样式:随机 UUID
这些配置的每个值都会变成 Redis 里的一条记录或一个 TTL——下一步亲眼验证。
步骤 2:打开开关并启动 backend
本课需要真实登录,把本机开关临时打开(Redis 已在跑):
codeforge:
redis:
enabled: true # 临时改,本课结束改回
启动 backend(IDE 或 mvnw spring-boot:run),日志确认 RedisConfig 的 Bean 生效。
步骤 3:真实登录一次
用测试账号登录(没有就先走注册接口造一个):
curl -s -X POST localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"你的账号","password":"你的密码"}'
# 返回体里有 tokenValue(token-style: uuid 的样子)
步骤 4:去 Redis 里找会话 key
docker exec -it redis-dev redis-cli
SCAN 0 MATCH satoken:* COUNT 100
你会看到类似:
satoken:login:token:<uuid> ← token → loginId 的映射
satoken:login:session:<loginId> ← 账号级会话对象
用 TYPE 和 TTL 看它们:
TYPE satoken:login:token:<uuid> # string(值是 loginId 相关 JSON)
TTL satoken:login:token:<uuid> # ≈86400,随活跃滚动续期
纪律:只看,不 DEL、不改名——删了会话等于把真实用户踢下线。本机踢的是你自己没损失,但习惯要现在养成。
步骤 5:验证 token 校验走的是 Redis
拿 token 调一个需要登录的接口:
curl -s localhost:8080/api/user/me -H "Authorization: <token>"
Sa-Token 校验 token 的过程:拿请求头里的 token 拼 key 查 Redis → 命中则取 loginId 放进请求上下文。想亲眼验证,可以在本机临时开 MONITOR 观察这次请求打出的 GET satoken:login:token:... 命令,看完立刻 Ctrl+C(MONITOR 生产禁用,下一课细讲为什么)。
步骤 6:把开关关回去,体会降级
codeforge:
redis:
enabled: false # 改回
重启后重新登录,再 SCAN satoken:*——Redis 里没有新 key 了:会话落回了 Sa-Token 的内存实现。同一套业务代码,存储位置由开关决定,这就是第 7 课说的「整组换轨」。
讲清楚要点
- 会话 key 的 TTL 就是登录有效期:timeout=86400 → 写入时 EXPIRE 24 小时;活跃访问滚动续期,久不用自然过期下线——Redis 的 TTL 机制和产品语义完美对应。
- is-concurrent + is-share 决定 token 与账号的映射形状:is-share=true 时多端共用同一个
satoken:login:token:条目;关掉则每端一个 token、各自 TTL。 - sa-token-redis-jackson 的原理 = 实现 SaTokenDao 接口:所有 get/set/delete 会话的调用被路由到 RedisTemplate;本站 RedisConfig 里的
SaTokenDaoForRedisTemplate+dao.init(factory)就是接线处。 - token 本身是无意义的随机串:安全性来自「token 只在服务端(现在是在 Redis)有映射」,泄露了可以单点踢下线(DEL 对应 key 或走管理接口)。
- 内存会话不是不能用,是只能单实例:本机开发 enabled=false 反而更快更简单——架构决策跟着部署形态走。
你可能会懵的地方
-
JWT 不也能无状态多实例吗,为什么要 Redis? JWT 校验不需要中心存储,但无法主动踢人下线(签发后服务端管不了)。会话进 Redis = 可随时 DEL 强制下线、可查在线人数。本站选 Sa-Token + Redis 就是看重可控性。
-
会话丢了用户会怎样? 下次请求校验失败 → 前端引导重新登录。体验损失可接受,数据无损——这正是第 1 课「Redis 数据丢失后果分级」的实例:缓存丢了重建,会话丢了重登,谁都不能丢 MySQL。
-
Redis 里的会话被 LRU 挤掉怎么办? 用户被登出。所以会话实例要容量规划、监控 maxmemory(第 11 课 INFO),或会话/缓存分实例部署。
-
为什么 token 请求头叫 Authorization?
sa-token.token-name配置的,本站与常见 HTTP 规范对齐;要改只动配置。 -
多实例下 Caffeine 那 30 秒会影响会话吗? 不会。Caffeine 只缓存业务只读数据(tracks 等),会话每次都查 Redis,保证踢人即时生效。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 多实例 + 内存会话 | 登录态随实例漂移、重启全员掉线 | Sa-Token + sa-token-redis-jackson |
测试时随手 DEL satoken:* | 把自己/他人踢下线 | 只读验证:TYPE/TTL/SCAN |
| timeout 设超大(如一年) | 会话堆积占内存 | 合理有效期 + 滚动续期 |
| 会话和缓存挤同一个小内存实例 | LRU 挤掉会话 → 用户被踢 | 分实例或容量规划 + 监控 |
| token 明文写进日志 | 泄露风险 | 脱敏;本站生产 cookie.secure=true |
卡点急救
- 登录成功但接口 401:请求头名字要对(Authorization),值就是 token 本体;本机确认开关 true 且 Redis PING 通。
- SCAN 不到 satoken key:开关没开(会话在内存里);或登录用的是改开关之前的旧 token。
- 重启 backend 后掉线:开关 false → 会话在内存。打开开关再登录。
- token TTL 不刷新:确认没被手工 PERSIST;Sa-Token 活跃续期依赖 auto-renew(默认开)。
- 想踢某人下线:走管理端接口或 Sa-Token 的 API;应急可
DEL satoken:login:token:<token>(生产别裸删,容易误伤)。
面试常问
-
多实例下登录态怎么共享? 中心化会话存储:Sa-Token 实现 SaTokenDao 接到 RedisTemplate,登录写
satoken:login:token:{uuid},校验时各实例查同一个 Redis,TTL=登录有效期、活跃滚动续期。 -
Session、Redis 会话、JWT 的取舍? 内存 Session:最快但限单机;Redis 会话:可控踢人、多实例共享,代价一次网络查询;JWT:无状态但难撤销。内容站选 Redis 会话,平衡可控与简单。
-
怎么实现「强制下线/踢人」? 删掉该用户在 Redis 里的 token 映射(或 Sa-Token API),下一次请求校验失败即下线——这是无状态 JWT 做不到的。
自测三问
- 内存会话在多实例下的两个具体故障是什么?
satoken:login:token:和satoken:login:session:分别存什么?各自的 TTL 由什么配置决定?- timeout=86400 在 Redis 层面靠什么机制实现?「滚动续期」靠什么触发?
小结一句话
会话进 Redis = 多实例共享、重启不丢、可踢可查;本站靠 sa-token-redis-jackson 一个 Bean 完成接线,开关一关还能整体落回内存。
延伸
- Sa-Token 集成 Redis
- 下一课:Docker 与持久化——生产怎么跑 Redis、RDB/AOF 怎么选、为什么没有卷的数据一删容器就蒸发。