首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【黑马点评日记02】Redis解决Tomcat集群Session共享问题

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

作者头像
北极的代码
发布2026-04-22 14:05:13
发布2026-04-22 14:05:13
820
举报

前言:

前面我们学习了黑马点评的第一个业务功能,也就是使用手机短信验证码进行登陆的功能,这里我们是把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 架构的完整流程

代码语言:javascript
复制
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】

代码语言:javascript
复制
Tomcat 内存
    ┌─────────────────┐
    │ Session 对象     │
    │   sessionId: ABC │
    │   userId: 123    │ ← 数据直接存在这里
    │   cart: [...]    │
    └─────────────────┘

【用 Redis - 分布式 Session】

代码语言:javascript
复制
Tomcat 内存                    Redis
┌─────────────────┐          ┌─────────────────┐
│ Session 对象     │          │ Key: session:ABC│
│   sessionId: ABC │ ──────→ │   userId: 123   │ ← 数据存在这里
└─────────────────┘          │   cart: [...]   │
                             └─────────────────┘

代码语言:javascript
复制
单机 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的多了

我们一直在提架构:

代码语言:javascript
复制
─────────────────────────────────────────────┐
│           整体架构(宏观层面)                │
│   单体架构 / 微服务架构 / 无服务架构          │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│           应用架构(你这一层)                │
│   认证架构: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 为手机号。

代码语言:javascript
复制
java

stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

2. 登录校验接口改造(核心)

原代码(Session 方式):

代码语言:javascript
复制
java

// 保存用户信息到 Session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

改造后(Token 方式):

代码语言:javascript
复制
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 方式):

代码语言:javascript
复制
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 方式):

代码语言:javascript
复制
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。

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-04-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Session集群的问题:
  • Session共享问题的本质:
  • 使用Redis对Session存储的迁移:
  • 单机 Session 架构的完整流程
  • 两种架构的核心区别
  • 具体对比
  • Token架构的优点(为什么很多人觉得它升级了)
  • Session架构优点
  • 统计视角
  • 什么场景还在用Session架构
  • 什么场景用Token架构
  • 为什么Token在新项目中更流行
  • 我们一直在提架构:
  • 黑马点评项目的Token架构改进:
    • 整体思路对比
    • 具体操作:
    • 1. 发送验证码(不变)
    • 2. 登录校验接口改造(核心)
    • 3. 登录拦截器改造(核心)
    • 4. 刷新 Token 中间件(可选但建议)
    • 前端配合改动(概念说明)
    • 改造后的 Key 设计
    • 改造前后对比
    • 一句话总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档