【黑马点评日记02】Redis解决Tomcat集群Session共享问题

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

前面我们学习了黑马点评的第一个业务功能,也就是使用手机短信验证码进行登陆的功能,这里我们是把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的内存里,不互通。

解决方案:

  1. Nginx粘性会话(ip_hash) :同一IP始终分到同一台Tomcat,简单但有机器宕机会丢session

  2. Session复制 :Tomcat之间广播同步session,性能差,不适合大规模集群

  3. 集中存储(最常用) :把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。

改造完成后,项目就具备了无状态、支持分布式、支持移动端的能力。

结语:如果对你有帮助,请**,点赞,关注,收藏**,你的支持就是我最大的鼓励!

相关推荐
cheems95272 小时前
[JavaEE]深度解构 Spring 核心:从控制反转 (IoC) 到依赖注入 (DI) 的架构演进
java·spring·架构·java-ee
MRDONG12 小时前
从 Prompt 到智能体:深入理解 APE、Active-Prompt、DSP、PAL、ReAct 与 Reflexion
前端·react.js·prompt
立莹Sir2 小时前
【架构图解+实战配置】SaaS多租户资源隔离的云原生完整方案
云原生·架构
毅炼2 小时前
MySQL常见问题总结(2)
java·数据库·mysql
翻斗包菜2 小时前
实战:使用 HAProxy 搭建高可用 Web 负载均衡集群
运维·前端·负载均衡
庞轩px2 小时前
第二篇:String、StringBuilder、StringBuffer深度剖析
java·字符串·stringbuilder·string·stringbuffer·字符串常量池
色空大师2 小时前
【阿里云部署服务问题指南】
java·mysql·阿里云·docker
Rsun045512 小时前
9、Java 外观模式从入门到实战
java·开发语言·外观模式
清心歌2 小时前
TreeSet 深度解析
java·开发语言