

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
前面我们学习了黑马点评的第一个业务功能,也就是使用手机短信验证码进行登陆的功能,这里我们是把session存到tomcat服务器中的,但是在高并发的环境下,一台tomcat服务器往往不够用,这是就需要集群tomcat,但就是这一操作产生了相应的问题,从而引出了Redis的使用,下面我们具体讲解。
摘要:本文探讨了高并发环境下Tomcat集群的Session共享问题及解决方案。传统Session存储在单机Tomcat内存中,导致集群环境下用户需重复登录。通过引入Redis作为分布式Session存储,实现了多台Tomcat间的Session共享。文章详细对比了单机Session和分布式Session架构的区别,分析了Token架构的优势及适用场景,并提供了黑马点评项目从Session架构改造为Token架构的具体实现方案,包括登录校验接口改造、拦截器调整和前端配合改动等核心步骤。改造后的Token架构具备无状态、支持分布式和移动端等优势,更适合现代高并发应用场景。
Session集群的问题:
首先,什么是集群,我们通过一个例子来简单理解一下:
集群 就是把多个Tomcat服务器组成一个整体,对外看起来像一台超级服务器。
为什么要多个Tomcat
扛住高并发 :1个Tomcat一般能支撑几百到上千并发,淘宝双11需要几十万并发,就得加机器
防止单点故障 :1台挂了,其他还能顶上,系统不中断
便于维护升级 :轮流重启,用户无感知
举个例子:
你部署了一个购物网站,用3台服务器,每台都跑着Tomcat。用户访问时,前面有个负载均衡器 (如Nginx)把请求分发到其中一台。用户第一次请求分到Tomcat1,登录成功;第二次请求可能分到Tomcat2,如果Tomcat2不知道你已经登录了,就会让你重新登录 ------ 这就是Session共享问题。
Session共享问题的本质:
Session默认存在各自Tomcat的内存里,不互通。
解决方案:
Nginx粘性会话(ip_hash) :同一IP始终分到同一台Tomcat,简单但有机器宕机会丢session
Session复制 :Tomcat之间广播同步session,性能差,不适合大规模集群
集中存储(最常用) :把session放到Redis等中间件,所有Tomcat都去Redis读写
提要:对于初学者,对于这些名词不是很能理解,不知道这些远原理究竟是什么,往往会导致我们学习的积极性下降(本人就是),基于此,我后面也会专门开设一个专栏,具体名字还没想好,不过有需要的可以去看看,就在最近几天。
使用Redis对Session存储的迁移:
首先先简单区分一下:
| 概念 | 类比 | 说明 |
|---|---|---|
| 服务器(Tomcat) | 银行大楼 | 物理存在的建筑,里面有柜台、保险柜 |
| Session | 你的账户档案 | 不是大楼本身,而是大楼里存着的一份文件 |
| SessionId | 你的银行卡号 | 你手里拿着卡号,每次去银行报卡号,柜员去档案室调你的档案 |
| Redis | 银行的中央档案库 | 如果银行有多个网点(多台Tomcat),所有网点都去同一个中央档案库调你的档案 |
最重要的是我们要知道我们使用的是什么架构,我们没修改之前的,单纯用session的是单机架构,而老师课程里讲解的是Token架构,也是现在的主流架构,但是还有一种Session+Redis的分布式Session架构。
单机 Session 架构的完整流程
1. 用户登录 ↓ 2. Tomcat 在本地内存创建 Session 对象 Session 对象里直接存着 {userId: 123} ↓ 3. Tomcat 通过 Set-Cookie 返回 sessionId ↓ 4. 浏览器存 Cookie ↓ 5. 后续请求,浏览器带 Cookie ↓ 6. Tomcat 根据 sessionId 从本地内存找到 Session 对象 ↓ 7. 直接内存读取,不经过 Redis
两种架构的核心区别
| 对比 | 单机 Session(不用Redis) | 分布式 Session(用Redis) |
|---|---|---|
| Session 存在哪 | Tomcat 本地内存 | Redis |
| Session 对象里有数据吗 | ✅ 有,直接存 | ❌ 没有,只有 sessionId |
| Session 对象大小 | 大(包含所有用户数据) | 小(只有一个ID) |
| 能多台 Tomcat 共享吗 | ❌ 不能 | ✅ 能 |
| Tomcat 重启后 | Session 全部丢失 | Session 还在 Redis 里 |
| 适用场景 | 开发测试、单机部署、低并发 | 生产环境、集群部署、高并发 |
【不用 Redis - 单机 Session】
Tomcat 内存 ┌─────────────────┐ │ Session 对象 │ │ sessionId: ABC │ │ userId: 123 │ ← 数据直接存在这里 │ cart: [...] │ └─────────────────┘
【用 Redis - 分布式 Session】
Tomcat 内存 Redis ┌─────────────────┐ ┌─────────────────┐ │ Session 对象 │ │ Key: session:ABC│ │ sessionId: ABC │ ──────→ │ userId: 123 │ ← 数据存在这里 └─────────────────┘ │ cart: [...] │ └─────────────────┘
单机 Session】 一台Tomcat,Session存在本地内存 ↓ 需求:多台Tomcat集群 两条路可以走: ┌─────────────────────┐ ┌─────────────────────┐ │ 路径A:Session架构 │ │ 路径B:Token架构 │ │ │ │ │ │ 加Redis共享Session │ │ 放弃Session │ │ 继续用session API │ │ 自己生成Token │ │ │ │ │ │ 分布式Session │ │ 无状态Token │ └─────────────────────┘ └─────────────────────┘
具体对比
| 维度 | Session架构(用Redis) | Token架构 |
|---|---|---|
| 本质 | Session还在,只是存Redis | Session彻底消失 |
| 凭证 | SessionId(容器生成) | Token(自己生成) |
| API | session.setAttribute() |
redisTemplate.set() |
| 状态 | 有状态(Redis里存着用户数据) | 无状态(Token自包含或Redis查) |
| 适合 | 传统Web、服务端渲染 | 移动端App、前后端分离、微服务 |
Token架构的优点(为什么很多人觉得它升级了)
| 优点 | 说明 |
|---|---|
| 跨平台 | 不依赖Cookie,App/小程序/Web都能用 |
| 无状态 | 服务器不存Session,随便扩缩容 |
| 性能好 | 不用每次去Redis查(如果是JWT) |
| 解耦 | 认证服务和业务服务可以分开 |
但这些优点不是"升级",而是"不同场景下的取舍"。
Session架构优点
| 优点 | 说明 |
|---|---|
| 简单 | 不用自己生成Token、管理过期 |
| 安全 | SessionId是随机字符串,可随时失效 |
| 可控 | 可以在Redis里直接删除Session实现踢人 |
| 浏览器原生支持 | Cookie自动带,前端不用写代码 |
统计视角
| 维度 | Session架构 | Token架构 |
|---|---|---|
| 新项目(2020年后启动) | 约30% | 约70% |
| 存量老项目 | 约80% | 约20% |
| 总体比例(所有项目) | 约60% | 约40% |
结论 :Token架构在新项目 中占主导,Session架构在老项目中占主导。
什么场景还在用Session架构
| 场景 | 原因 |
|---|---|
| 老项目维护 | 代码已经跑了好几年,没必要重构 |
| 管理后台 | 用户少,用Session简单够用 |
| 政府/银行内部系统 | 技术保守,Cookie方案稳定 |
| 传统服务端渲染(JSP/Thymeleaf) | 天然和Session配合 |
| 小团队快速开发 | Session零配置,上手快 |
什么场景用Token架构
| 场景 | 原因 |
|---|---|
| 前后端分离项目 | Vue/React + Java,Token是标配 |
| 移动端App | 没有Cookie机制,只能用Token |
| 小程序 | 同App,只能用Token |
| 微服务架构 | 无状态,方便横向扩展 |
| 单点登录(SSO) | Token可以在多个系统间传递 |
| 第三方API | 给外部调用的接口,用Token鉴权 |
为什么Token在新项目中更流行
| 原因 | 说明 |
|---|---|
| 前后端分离是主流 | 现在大部分项目都是Vue/React + Java |
| 移动端需求普遍 | 几乎所有新项目都有App或小程序 |
| 云原生/微服务 | 需要无状态设计,方便弹性伸缩 |
| 开发体验 | 前后端可以完全独立开发 |
| 面试要求 | 现在面试问Session的少了,问JWT/Token的多了 |
我们一直在提架构:
─────────────────────────────────────────────┐ │ 整体架构(宏观层面) │ │ 单体架构 / 微服务架构 / 无服务架构 │ └─────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────┐ │ 应用架构(你这一层) │ │ 认证架构:Session / Token / OAuth2 │ │ 分层架构:Controller → Service → DAO │ └─────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────┐ │ 技术架构(具体选型) │ │ 框架:Spring Boot / Spring MVC │ │ 存储:MySQL / Redis │ └─────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────┐ │ 代码层面(实现细节) │ │ 你写的 if/else、for循环、session.setAttribute│ └─────────────────────────────────────────────┘
黑马点评项目的Token架构改进:
整体思路对比
| 环节 | 原 Session 架构 | 改造后的 Token 架构 |
|---|---|---|
| 生成凭证 | 由 Tomcat 自动生成 SessionId 写入 Cookie | 手动生成随机 Token(如 UUID),手动返回给前端 |
| 存储用户信息 | session.setAttribute("user", user) |
redisTemplate.opsForHash().putAll(...) 存入 Redis,key 为 token |
| 返回给前端 | 自动响应头 Set-Cookie |
响应体 JSON 中返回 {token: "xxx"} |
| 前端存储 | 浏览器自动存 Cookie | 前端手动存 localStorage |
| 后续请求携带 | 浏览器自动带 Cookie | 前端手动在请求头加 Authorization |
| 后端获取用户 | session.getAttribute("user") |
从请求头取 token,去 Redis 查 Hash |
具体操作:
1. 发送验证码(不变)
发送验证码部分逻辑不需要改,依然是把验证码存 Redis,key 为手机号。
java
java
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
2. 登录校验接口改造(核心)
原代码(Session 方式):
java
java
// 保存用户信息到 Session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
改造后(Token 方式):
java
java
// 1. 生成随机 token(使用 UUID)
String token = UUID.randomUUID().toString(true);
// 2. 将 UserDTO 转为 Hash 结构存储到 Redis
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 3. 存储到 Redis,key 为 "login:token:" + token
String tokenKey = LOGIN_TOKEN_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 4. 设置 token 有效期(比如 30 分钟)
stringRedisTemplate.expire(tokenKey, LOGIN_TOKEN_TTL, TimeUnit.MINUTES);
// 5. 返回 token 给前端(不再是返回空或 Cookie)
return Result.ok(token);
3. 登录拦截器改造(核心)
原登录拦截器(Session 方式):
java
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从 Session 获取用户
UserDTO user = (UserDTO) session.getAttribute("user");
if (user == null) {
throw new RuntimeException("未登录");
}
// 存入 ThreadLocal
UserHolder.saveUser(user);
return true;
}
改造后(Token 方式):
java
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从请求头获取 token(前端手动放进去的)
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
throw new RuntimeException("未登录");
}
// 2. 从 Redis 获取用户信息
String tokenKey = LOGIN_TOKEN_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
if (userMap.isEmpty()) {
throw new RuntimeException("未登录");
}
// 3. 转为 UserDTO 对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 4. 存入 ThreadLocal
UserHolder.saveUser(userDTO);
// 5. 刷新 token 有效期(活跃用户续期)
stringRedisTemplate.expire(tokenKey, LOGIN_TOKEN_TTL, TimeUnit.MINUTES);
return true;
}
4. 刷新 Token 中间件(可选但建议)
在 Session 架构中,只要用户操作,Session 会自动续期。在 Token 架构中需要手动刷新 Redis 过期时间。
可以在拦截器里每次请求都刷新 token 有效期(如上代码第 5 步),也可以单独做一个拦截器只负责刷新。
前端配合改动(概念说明)
黑马点评前端也需要改,不需要动前端代码,但需要知道它要做什么:
| 步骤 | 前端操作 |
|---|---|
| 登录后 | 从响应体拿到 token,存入 localStorage.setItem("token", token) |
| 后续请求 | 从 localStorage 取出 token,放到请求头 Authorization: token值 |
改造后的 Key 设计
| 用途 | Key 格式 | 示例 | TTL |
|---|---|---|---|
| 验证码 | login:code:手机号 |
login:code:13800138000 |
2 分钟 |
| 用户 Token | login:token:UUID |
login:token:abc-123-def |
30 分钟 |
改造前后对比
| 对比项 | Session 架构 | Token 架构 |
|---|---|---|
| 凭证 | SessionId(自动 Cookie) | Token(手动返回 JSON) |
| 存储 | session.setAttribute |
redisTemplate.opsForHash().putAll |
| 获取用户 | session.getAttribute |
请求头取 token → 查 Redis |
| 分布式支持 | 需要配置 Session 共享 | 天然支持 |
| 移动端/小程序 | 不方便(依赖 Cookie) | 天然支持 |
一句话总结
Token 架构改造黑马点评登录,就是把"Tomcat 自动管的 Session"换成"自己生成的 UUID 作为 Token",把用户信息从 Session 内存搬到 Redis 的 Hash 结构中,前端从存 Cookie 改为存 localStorage,每次请求手动带 Token。
改造完成后,项目就具备了无状态、支持分布式、支持移动端的能力。
结语:如果对你有帮助,请**,点赞,关注,收藏**,你的支持就是我最大的鼓励!