

前言:
前面我们学习了黑马点评的第一个业务功能,也就是使用手机短信验证码进行登陆的功能,这里我们是把session存到tomcat服务器中的,但是在高并发的环境下,一台tomcat服务器往往不够用,这是就需要集群tomcat,但就是这一操作产生了相应的问题,从而引出了Redis的使用,下面我们具体讲解。
摘要: 本文探讨了高并发环境下Tomcat集群的Session共享问题及解决方案。传统Session存储在单机Tomcat内存中,导致集群环境下用户需重复登录。通过引入Redis作为分布式Session存储,实现了多台Tomcat间的Session共享。文章详细对比了单机Session和分布式Session架构的区别,分析了Token架构的优势及适用场景,并提供了黑马点评项目从Session架构改造为Token架构的具体实现方案,包括登录校验接口改造、拦截器调整和前端配合改动等核心步骤。改造后的Token架构具备无状态、支持分布式和移动端等优势,更适合现代高并发应用场景。
首先,什么是集群,我们通过一个例子来简单理解一下: 集群就是把多个Tomcat服务器组成一个整体,对外看起来像一台超级服务器。 为什么要多个Tomcat
举个例子: 你部署了一个购物网站,用3台服务器,每台都跑着Tomcat。用户访问时,前面有个负载均衡器(如Nginx)把请求分发到其中一台。用户第一次请求分到Tomcat1,登录成功;第二次请求可能分到Tomcat2,如果Tomcat2不知道你已经登录了,就会让你重新登录 —— 这就是Session共享问题。
Session默认存在各自Tomcat的内存里,不互通。 解决方案:
提要:对于初学者,对于这些名词不是很能理解,不知道这些远原理究竟是什么,往往会导致我们学习的积极性下降(本人就是),基于此,我后面也会专门开设一个专栏,具体名字还没想好,不过有需要的可以去看看,就在最近几天。
首先先简单区分一下:
概念 | 类比 | 说明 |
|---|---|---|
服务器(Tomcat) | 银行大楼 | 物理存在的建筑,里面有柜台、保险柜 |
Session | 你的账户档案 | 不是大楼本身,而是大楼里存着的一份文件 |
SessionId | 你的银行卡号 | 你手里拿着卡号,每次去银行报卡号,柜员去档案室调你的档案 |
Redis | 银行的中央档案库 | 如果银行有多个网点(多台Tomcat),所有网点都去同一个中央档案库调你的档案 |
最重要的是我们要知道我们使用的是什么架构,我们没修改之前的,单纯用session的是单机架构,而老师课程里讲解的是Token架构,也是现在的主流架构,但是还有一种Session+Redis的分布式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、前后端分离、微服务 |
优点 | 说明 |
|---|---|
跨平台 | 不依赖Cookie,App/小程序/Web都能用 |
无状态 | 服务器不存Session,随便扩缩容 |
性能好 | 不用每次去Redis查(如果是JWT) |
解耦 | 认证服务和业务服务可以分开 |
但这些优点不是“升级”,而是“不同场景下的取舍”。
优点 | 说明 |
|---|---|
简单 | 不用自己生成Token、管理过期 |
安全 | SessionId是随机字符串,可随时失效 |
可控 | 可以在Redis里直接删除Session实现踢人 |
浏览器原生支持 | Cookie自动带,前端不用写代码 |
维度 | Session架构 | Token架构 |
|---|---|---|
新项目(2020年后启动) | 约30% | 约70% |
存量老项目 | 约80% | 约20% |
总体比例(所有项目) | 约60% | 约40% |
结论:Token架构在新项目中占主导,Session架构在老项目中占主导。
场景 | 原因 |
|---|---|
老项目维护 | 代码已经跑了好几年,没必要重构 |
管理后台 | 用户少,用Session简单够用 |
政府/银行内部系统 | 技术保守,Cookie方案稳定 |
传统服务端渲染(JSP/Thymeleaf) | 天然和Session配合 |
小团队快速开发 | Session零配置,上手快 |
场景 | 原因 |
|---|---|
前后端分离项目 | Vue/React + Java,Token是标配 |
移动端App | 没有Cookie机制,只能用Token |
小程序 | 同App,只能用Token |
微服务架构 | 无状态,方便横向扩展 |
单点登录(SSO) | Token可以在多个系统间传递 |
第三方API | 给外部调用的接口,用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│
└─────────────────────────────────────────────┘环节 | 原 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 |
发送验证码部分逻辑不需要改,依然是把验证码存 Redis,key 为手机号。
java
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);原代码(Session 方式):
java
// 保存用户信息到 Session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));改造后(Token 方式):
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);原登录拦截器(Session 方式):
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
@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;
}在 Session 架构中,只要用户操作,Session 会自动续期。在 Token 架构中需要手动刷新 Redis 过期时间。
可以在拦截器里每次请求都刷新 token 有效期(如上代码第 5 步),也可以单独做一个拦截器只负责刷新。
黑马点评前端也需要改,不需要动前端代码,但需要知道它要做什么:
步骤 | 前端操作 |
|---|---|
登录后 | 从响应体拿到 token,存入 localStorage.setItem("token", token) |
后续请求 | 从 localStorage 取出 token,放到请求头 Authorization: token值 |
用途 | 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。
改造完成后,项目就具备了无状态、支持分布式、支持移动端的能力。
结语:如果对你有帮助,请,点赞,关注,收藏,你的支持就是我最大的鼓励!