做 Java Web 开发久了,session 这个问题迟早会撞到你脸上。我刚入行那会儿,项目还是个单体应用,所有 session 都老老实实躺在 Tomcat 内存里,一个用户登录了就有一份会话数据,简单直接,根本不用操心别的。直到后来服务拆成多台、用户量上来,每次重启都得让所有人重新登录,线上偶发“诡异掉线”,我才被迫把 session 从内存迁到 Redis,也才真正把“session 放内存”和“session 放 Redis”这两条路的底细摸清楚。
这篇文章不绕弯子,直接聊这个话题。我会先讲 session 到底存了什么、为什么有存储位置的问题,再分别拆内存方案和 Redis 方案的工作原理、优劣和适用场景,最后给出从内存迁到 Redis 的完整实操配置和我在生产环境里踩过的一些坑。适合正在做 Web 开发、被 session 共享和持久化问题困扰的开发者,也适合准备面试、想系统理解 session 存储原理的同学。
1. session 的本质,以及为什么会有“放哪里”的问题
1.1 session 到底存了什么?
HTTP 协议本身是无状态的。用户登录成功后,服务端怎么知道后面的请求还是同一个用户?最常见的做法就是:服务端在内存里创建一份会话数据,生成一个唯一的 sessionId,通过响应头里的Set-Cookie: JSESSIONID=xxx交给浏览器;浏览器后续请求都会自动带上这个 Cookie,服务端再根据 sessionId 找到对应的数据。
这份“对应的数据”就是 session。它本质上是一个键值对容器,里面可以放 userId、用户角色、购物车内容、临时校验标记等任何你需要跨请求保留的信息。你可以把 session 理解成服务员手里的“客人档案卡”:客人报出桌号(JSESSIONID),服务员就能找到对应的菜单偏好和忌口记录。
1.2 为什么 session 不能只放在客户端?
有同学可能会问:既然 Cookie 能存放数据,为什么还要在服务端开一块空间存 session?原因很简单:Cookie 在客户端,数据可以被用户随意查看和篡改,而且大小有限制(通常 4KB 左右)。如果登录凭证、权限角色这些都放 Cookie,安全防线基本等于没有。服务端 session 的好处是数据留在自己的可控范围内,客户端只持有一个不可猜测的 ID,服务器拿到 ID 后再去查真正的数据,避免把核心信息暴露给用户。
既然要在服务端存,就得选地方。最自然的选择是内存——毕竟对象本来就在 JVM 里,直接一个 Map 存下来,读写速度极快。但随着系统变大,“服务端”可能不止一台机器,session 放内存的短板就逐渐暴露了。这就是这篇文章里内存方案与 Redis 方案所有分歧的根源。
1.3 会话数据在内存和 Redis 中,本质上的差异是什么?
一句话概括:内存方案把 session 当成“进程内临时对象”来管理,Redis 方案把 session 当成“独立存储中的键值数据”来管理。前者生命周期绑定在 JVM 进程上,进程死了 session 就没了;后者生命周期绑定在 Redis 中,通过 key 和 TTL 控制,和具体哪一个应用节点无关。一句话的差异,背后的设计取舍和运维成本差出十万八千里。
2. session 放内存:默认方案的真实面貌
2.1 Tomcat 默认是怎么管理内存 session 的
绝大多数 Java Web 开发者第一次接触的就是这个方案。以 Tomcat 为例,默认的StandardManager内部维护了一个ConcurrentHashMap,key 是 sessionId,value 是StandardSession对象。每次请求进来,Tomcat 从 Cookie 里解析出 JSESSIONID,到这个 Map 里查一下,存在就返回同一个 session 对象,不存在就新建一个。
这个 Map 是有淘汰机制的。Tomcat 后台有个定时任务,周期性地扫描所有 session,检查lastAccessedTime和当前时间的差值,超过最大未访问时间就移除。默认超时时间是 30 分钟,也就是web.xml里<session-config><session-timeout>30</session-timeout></session-config>的来历。用户持续频繁操作,lastAccessedTime 会被不断刷新,session 就不会“过期”。这套机制对单机应用完全够用,实现也简单。
2.2 为什么内存 session 会让人头疼?
我在生产里见过三种典型的坑,都和“把 session 放内存”直接相关。
第一,重启丢会话。不管你用kill -9还是优雅停机,JVM 进程一旦退出,堆内存里所有 session 对象立刻灰飞烟灭。你发布一次代码,用户就集体掉线一次。有些项目为了缓解这个问题,用 Tomcat 的PersistentManager把 session 落盘或存到数据库,先不说性能,配置复杂度和恢复延迟就够喝一壶了。
第二,多实例无法共享。用户第一次请求打到 A 机器,session 存在 A 的内存里;下一次请求被负载均衡转发到 B 机器,B 的内存里当然没有这个 session。常见临时方案是负载均衡开启“粘性会话”(sticky session),让同一个用户固定打到同一台机器。但代价是:某台机器故障时,还是会让该节点上的用户全部掉线,弹性扩容和缩容也极其痛苦。
第三,内存压力不可控。session 数量取决于在线用户数和每个 session 的大小编码,但它占用的堆内存是动态增长的。如果业务方在一个 session 里塞了大量对象(比如把某个很重的查询结果直接丢进去),堆内存会迅速攀升,GC 变频繁,甚至触发 Full GC 长停顿。我见过一个老系统,内存 session 里存了用户权限表,几万人一登录,堆直接涨了 2GB,整站卡成幻灯片。
2.3 内存 session 就没有优点吗?
不能一棍子打死。内存方案最大的优点就是快——读取 session 不需要走网络、不需要序列化,直接取对象引用,性能下限极高。而且它零额外组件依赖,Tomcat 开箱即用,连运维都不用操心。对单机部署、用户量小(比如内部管理系统、低并发后台)、可以接受重启丢会话的场景,用内存 session 反而是最优解。加一个 Redis 进去不是不行,但属于无谓的复杂度。
3. session 放 Redis:解决共享和重启问题的正经方案
3.1 Redis 里的 session 长什么样
把 session 放 Redis,本质上就是用 Redis 替代 JVM 内部那个 Map。sessionId 作为 key,session 数据作为 value,TTL 设置为会话超时时间。用户请求进来时,应用从 Cookie 取到 sessionId,去 Redis 查一下,数据还在就恢复会话;数据已过期就新建。
用 Spring Session 的例子来说,一个 session 在 Redis 里往往不是单条记录,而是有几类 key:
spring:session:sessions:<sessionId>:主键,hash 类型,存放当前 session 的属性、创建时间、最后访问时间、过期时间。spring:session:expirations:<时间戳>:辅助键,集合类型,用来定时清理过期的 session,避免依赖 Redis 自身的惰性删除导致残留。spring:session:index:<索引名>:如果开启了按 userName 等字段索引查询,还会额外生成索引键。
实际拿redis-cli看,你会发现 value 可能是一串 JSON,也可能是二进制乱码,这取决于你配置的序列化方案。后面实操部分我会详细说怎么让它变得可读、可排查。
3.2 为什么 Redis 能解决“多实例不共享”
理解这一点是全文的核心。内存 session 的数据归属在某个 JVM 进程内部,别的进程看不见也摸不着。Redis 则是一个独立的存储服务,所有应用节点通过 TCP 连接访问同一个 Redis 实例或集群。session 数据不再属于任何特定应用节点,而是属于统一的存储层。
于是负载均衡不再需要粘性会话了。用户第一次请求登录打到 A,session 存入 Redis;第二次请求打到 B,B 同样从 Redis 里按 sessionId 查到了这份数据。应用节点可以任意横向扩容和缩容,哪台机器挂了都不影响其他节点的用户。这也是微服务架构、网关层无状态化常用的基础能力之一。
3.3 Spring Session 是怎么做到“无缝替换”的
你可能会问:换成 Redis 存 session,业务代码是不是要大改?Spring Session 做得比较巧妙,它通过 Servlet 规范里的HttpSession接口,把“会话实现”整体替换掉了。应用层拿到的不再是 Tomcat 的StandardSession,而是一个基于 Redis 数据构造的RedisSession对象。你照样request.getSession().setAttribute(...),底层写操作却已经透传到 Redis。
原理上,Spring Session 在 Servlet 容器里注册了一个SessionRepositoryFilter,这个过滤器会把原生的HttpServletRequest包装成SessionRepositoryRequestWrapper,然后重写getSession()方法。真正干活的是RedisSessionRepository,它负责把 session 对象序列化写入 Redis、按 ID 反序列化读回、设置 TTL。这个过程对绝大多数业务代码是透明的,你甚至不需要改动 Service 层和 Controller 层。
3.4 Redis session 的“序列化”是一个大坑
把 session 放进 Redis,意味着数据要跨越 JVM 进程边界,必须变成字节流再存进去,读取时再还原成对象。序列化方案选错了,表面看不出问题,但排查起来能让人怀疑人生。
常见序列化方式有这三种:
- JDK 原生序列化:直接把
ObjectOutputStream写出来的二进制存进 Redis。缺点很明显:内容体积大、不可读、有反序列化安全风险(构造恶意数据可能触发漏洞)。很多“重启 Redis 之后用户全部掉线”的诡异问题,追根究底就是新旧代码用的序列化方式不一致,导致旧数据还原失败。 - JSON 序列化(Jackson):推荐方案,可读性强,方便用
redis-cli直接观察数据,跨语言友好。但要注意:对象的全限定类名会被写进 JSON 里,重命包名或类名后旧数据也无法读取。 - 自定义压缩序列化:对超大 session 数据(比如塞了对象列表)能显著节省 Redis 内存,但调试更麻烦。
我现在默认用GenericJackson2JsonRedisSerializer,原因只有一个:线上出问题时我能直接看到 session 里到底存了什么东西,不需要拿着二进制串去解析。
4. 内存方案与 Redis 方案的硬核对比
4.1 关键指标对比表
下面这张表是我自己整理的项目选型速查表,后面每一项我都会展开解释。
| 维度 | 内存 session | Redis session |
|---|---|---|
| 读写性能 | 最快,走 JVM 堆内引用 | 需要网络 RTT + 序列化,通常多 1-5ms |
| 容量上限 | 受 JVM 堆限制,过大会加重 GC | 受 Redis 内存限制,可随集群扩展 |
| 进程重启 | 直接丢失 | Redis 有 RDB/AOF,数据基本不丢 |
| 多实例共享 | 不支持,需要粘性会话 | 天然支持,所有节点读写同一存储 |
| 数据可观测性 | 需借助 dump 堆栈 | 可直接redis-cli查看 key 和 value |
| 运维复杂度 | 无额外组件 | 需要维护 Redis 集群(或保证高可用) |
| 会话过期方式 | 容器定时扫描 lastAccessedTime | TTL + 定期清理,依赖 Redis 删除机制 |
| 对小流量系统 | 最合适 | 偏重,引入依赖反而增加故障点 |
4.2 性能差距并没有想象中那么大
很多同学一听 Redis 方案,第一反应是“每次读 session 都走网络,肯定慢”。真实情况要冷静看待。如果你和 Redis 都部署在同一内网,一次GET的往返延迟通常在 0.2-1ms 左右,加上 JSON 反序列化时间,撑死几个毫秒。而内存 session 虽然对象引用拿到就能用,但当堆内存被大量 session 塞满时,频繁 GC 造成的毛刺也许比 Redis 一次网络 IO 还夸张。
举个例子:我压过某个电商后台,内存 session 方案在并发 2000 时 GC 暂停时间平均 80ms,最坏接近 300ms;切到 Redis session 之后,每次 session 操作增加 2ms,但堆内存稳住了,GC 暂停掉到 20ms 以内。整体用户体验反而更平滑。当然,如果你的每个请求里有十几次 session 访问,且 Redis 网络波动较大,这个结论要重测。关键是用数据说话,不要凭感觉。
4.3 过期机制的本质区别
这里必须细讲,因为很多线上掉线问题都和它有关。
内存方案里,Tomcat 的StandardManager是周期扫描判断“最后访问时间距离当前时间是否超过超时阈值”。用户连续操作,session 会被不断续期;用户离开,session 撑到超时阈值后由后台任务清除。这种机制是“应用层主动控制”的。
Redis 方案里,session 过期依赖 Redis 的 key 过期机制,包括惰性删除和定期删除。Spring Session 还会额外维护一个expirations集合,配合定时任务确保过期 session 会被明确清理,而不只是挂个 TTL 等 Redis 自己想起来了再删。但需要注意:Redis 的定期删除默认每秒 10 次,扫描一定比例的 key,如果某个 key 的 TTL 刚一到,也不会立刻在物理上消失,而是被标记为逻辑过期。客户端访问时惰性删除才会真正触发。所以线上偶尔能看到 Redis 里还残留着“已过期”的 session key,这是正常的,不意味着 session 还能被正常使用。
4.4 安全层面的差异
内存 session 的数据在 JVM 内部,外部无法直接读取;Redis session 是独立存储,如果 Redis 端口直接暴露或未设置认证,局域网内任何能连上 Redis 的人都可能直接顺着 sessionId 读出用户数据。所以用 Redis session 之后,Redis 本身的访问控制反而成了安全重点:必须设requirepass、绑定非公网地址、最好启用 TLS。另外,敏感字段不要直接塞进 session,即使 Redis 里也要考虑加密存储。我在项目里习惯只往 session 放“非敏感的上下文信息”,像真实的令牌这类高危数据另外放专门的存储。
4.5 什么时候你必须用 Redis session
我不会摆“万金油”结论,只告诉你哪些硬指标一出现,内存 session 就该淘汰了:
- 应用部署了 2 个以上实例,且不方便开启粘性会话。
- 发布频率高,接受不了每次发布用户集体掉线。
- 有弹性扩容诉求,希望新节点启动后能立刻接管用户流量。
- 多个应用需要共享同一份登录态(比如网关 + 多个后端服务)。
- 用户量虽不大,但单机堆内存非常紧张,不希望被 session 占用太多。
只要命中其中两条,直接上 Redis session 是正确选择。反之,就继续用内存,别给自己找事。
5. 实操:把 Spring Boot 的 session 从内存迁到 Redis
5.1 环境准备与依赖
我下面以 Spring Boot 2.7 / 3.x 为例,Redis 用 6.x 或 7.x 都行。先确认你已经有可用的 Redis 实例,本地可以用 Docker 快速起一个:
docker run -d --name redis-session -p 6379:6379 redis:7-alpine项目里需要引入两个关键依赖:spring-boot-starter-data-redis提供 Redis 操作能力,spring-session-data-redis提供 HttpSession 与 Redis 的桥接。如果你是 Maven 项目,在pom.xml里加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>引入之后,Spring Boot 会尝试自动配置 Redis 连接和 Session 仓库。理论上你只需要加依赖和配置就能跑,但实际生产中,序列化和连接池参数必须手动调。
5.2 配置 Redis 连接与会话超时
在application.yml中做如下配置:
spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 2000ms lettuce: pool: max-active: 50 max-idle: 10 min-idle: 5 max-wait: 3000ms session: store-type: redis timeout: 30m这里有几个容易忽略的点。spring.session.store-type: redis是显式声明 session 存储类型,不加也能通过依赖自动推断,但显式写出来更可控。spring.session.timeout是 session 的过期时间,默认 30 分钟,生产环境要根据业务自己定,比如“记住我”类需求可能需要 7 天,纯登录态管理可能 2 小时就够了。连池参数要结合请求峰值估算,太小会导致 Redis 连接等待超时,太大又会占用内存和连接数。
5.3 配置 JSON 序列化器,避免乱码
这是我最想让你注意的一步。默认情况下 Spring Session 会把 session 属性用 JDK 序列化存储,Redis 里全是\xAC\xED\x00\x05开头的二进制。调试时你根本看不出来里面是什么,反序列化出错时也难排查。我推荐直接换成GenericJackson2JsonRedisSerializer:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; @Configuration public class SessionRedisConfig { @Bean(name = "springSessionDefaultRedisSerializer") public RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } }注意 bean 名字必须叫springSessionDefaultRedisSerializer,Spring Session 会优先使用这个名字注入它默认的 RedisSerializer。如果你自己另起一个名字,配置可能不生效,session 照样走 Java 原生序列化。这是 Spring Session 设计上比较隐蔽的一点,网上很多教程都没提,实际踩坑的人不少。
配置好之后,session.setAttribute("loginUser", user)里的User对象必须保证能被 Jackson 序列化:类需要有默认构造器,字段要有 getter/setter,或者类上用@JsonIgnoreProperties(ignoreUnknown = true)兜底。否则写入时会直接抛序列化异常。
5.4 写一个最简单的登录接口验证效果
为了验证迁移是否成功,我通常会在 Controller 里加一个非常简单的接口:
@RestController public class SessionController { @PostMapping("/login") public String login(HttpServletRequest request, String username) { request.getSession().setAttribute("username", username); return "login success, sessionId=" + request.getSession().getId(); } @GetMapping("/whoami") public String whoami(HttpServletRequest request) { Object username = request.getSession().getAttribute("username"); return username == null ? "anonymous" : (String) username; } @PostMapping("/logout") public String logout(HttpServletRequest request) { request.getSession().invalidate(); return "logout success"; } }先调用/login登录,再用redis-cli查 Redis:
redis-cli > keys spring:session:*你会看到类似这样的结果:
"spring:session:sessions:5f2c1a..." "spring:session:expirations:1710000000000"再执行:
> type spring:session:sessions:5f2c1a... hash > hgetall spring:session:sessions:5f2c1a... 1) "creationTime" 2) "1710000000000" 3) "lastAccessedTime" 4) "1710000001000" 5) "maxInactiveInterval" 6) "1800" 7) "sessionAttr:username" 8) "\"zhangsan\""能看到sessionAttr:username的值是"zhangsan",说明 session 已经干净地写入 Redis,并且是 JSON 格式,没有乱码。
5.5 关键参数与淘汰策略的联动
Redis session 的 TTL 虽然由 Spring Session 写入,但你别忘了 Redis 本身还有内存淘汰策略在下面看着。如果你的 Redismaxmemory很小,且maxmemory-policy是allkeys-lru,那么在内存吃紧的时候,Redis 会优先淘汰最近最少使用的 session key。这会导致明明用户还在活跃,但因为其他大量 key 占内存把 session key 挤掉了,用户莫名其妙掉线。
所以用了 Redis session 之后,要么把maxmemory-policy设置成volatile-ttl(只淘汰设置了 TTL 的 key,且优先淘汰剩余寿命短的),要么给 Redis 规划足够的内存,并在监控里盯住evicted_keys指标。如果是集群方案,还要考虑不同分片之间的内存均衡。
6. 常见问题与排查实录
6.1 重启后用户全部掉线,但已经用的是 Redis
这类问题通常不是“没用 Redis”,而是“Redis 根本没接上”,或“Spring Session 没有接管容器 session”。排查路径先看启动日志,有没有出现SessionRepositoryFilter相关的初始化信息;再看启动时是否报 Redis 连接失败;最后看 Redis 里到底有没有 session key。如果 Redis 里根本没 key,说明 session 还是落在了 Tomcat 内存里,多半是依赖没引全或者配置里spring.session.store-type没生效。
还有一种情况是:你以前用内存 session 时,负载均衡开了 sticky session;换 Redis session 后 sticky 也解除了,但有些老浏览器的 Cookie 里同时存了多个 JSESSIONID,请求时带上旧 ID 找不到 session,也会表现为掉线。遇到这种,清一下 Cookie 往往就好了。
6.2 Redis 里的 session key 全是二进制乱码
如果你在redis-cli里看到 key 或 value 是\xAC\xED\x00\x05开头的内容,大概率是 JDK 原生序列化在起作用。排查思路是把SessionRedisConfig里那个 bean 名确认一遍,确认名字是springSessionDefaultRedisSerializer,而不是redisSerializer。改完配置后重启应用,再登录一次,新的 session 就会是 JSON 格式。旧数据不会自动转换,线上切换时可以先清除旧 session key,或等 TTL 自然过期。
6.3 Redis 连接闪断导致接口报错、session 疑似丢失
Redis session 方案引出一个新问题:Redis 成了关键依赖,一旦连接异常,所有需要 session 的接口都可能 5xx。我遇到过 Redis 实例因为 AOF 重写导致短暂阻塞,结果线上大量请求在等待连接,超时后抛 RedisConnectionFailureException。
应对措施分三层:第一层是给 Lettuce 连接池足够上限,避免瞬时流量把连接打满;第二层是用 Redis Sentinel 或 Cluster 保证单节点故障时可切换;第三层是在代码里对 session 读取异常做降级,比如允许部分只读请求在 session 缺失时以匿名身份继续,而不是直接抛错。注意降级逻辑不要误放行需要登录态的写操作,安全优先级永远高于可用性。
6.4 内存和 Redis 双份 session 串场
有些团队迁移到一半,好奇“两者能不能共存”,结果发现 Cookie 里的 JSESSIONID 在 Tomcat 内存和 Redis 里都能查到,有的人访问到旧的,有的人访问到新的。这是因为你同时保留了容器默认 session 管理和 Spring Session 过滤器,两者各自创建会话,极易串场。
我的建议是不要做这种混搭。要迁就一次性迁干净:移除容器默认 session 持久化配置,只用 Spring Session 管 Redis。如果你的老代码里手动依赖session.getServletContext()这类容器 API,迁到 Spring Session 后虽然兼容,但很多容器特有方法已经变成空实现,业务层千万别再去依赖 Tomcat 私有 API。这是迁移中的隐藏雷区。
6.5 排查思路速查表
下面是我平时排查 session 问题的一整套顺序,先控制变量再动手。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 登录后又变匿名 | session 过期太快 | 检查spring.session.timeout与 CookieMax-Age |
| 重启后全量掉线 | session 还在内存里 | 登录后查 Redis 有无 session key |
| 多实例间时而登录时而没登录 | 粘性会话未关闭 | 看负载均衡策略 |
| Redis 里 key 是乱码 | 默认 JDK 序列化 | 按上文配置 JSON 序列化器 |
| Redis 内存不断上涨 | 过期 key 清理延迟 | 看expired_keys和evicted_keys |
| 某节点 Redis 连接池占满 | 峰值流量超过 pool | 调大 max-active,看 Redis 端慢日志 |
此外,注意不要把数据库 session 的概念和 HTTP session 混为一谈。像某些数据库日志里的session unused timeout, terminating connection,那是数据库连接或 SQL 会话层面的超时,跟 Web 登录态没有任何关系。排查时先搞清楚是哪个“session”,别看见关键词就往上套,容易白忙活半天。
6.6 Cookie 与 sessionId 的联动问题
另一个高频坑是 Cookie 的Path和Domain设置不对。如果应用部署在/app这个 context path 下,但 Cookie 的 Path 被设成/,那么其他应用也可能会把这个 JSESSIONID 发过来,造成 Redis 里查不到对应 session,白增加一次查询。反过来,如果多个子域名需要共享登录态,得把 Cookie Domain 设置成顶级域名,并把 Path 设置成/。Spring Session 里可以通过server.servlet.session.cookie.path和domain来调,动手前先理清域名拓扑。
7. 我的选型经验,以及一点扩展建议
7.1 三个典型场景我分别怎么选
我先给一套我自己在项目里反复用的判断标准。内部管理系统、工单平台这类 200 人以内的小应用,单机部署,我直接用内存 session,连 Redis 都不碰。理由很简单:部署简单,出了岔子也好排查,重启丢登录这件事对低频率用户几乎没有感知。
电商业务或面向 C 端的门户,只要是两个以上实例,我会第一优先级上 Redis session。因为这种业务最怕的就是发布时用户集体掉线,而且多实例之间共享登录态是硬需求。用户量再大一点、微服务拆分再细一点,我会考虑把 session 从业务应用里彻底剥离,统一放到一个独立的会话服务中,由网关统一校验,业务服务不再直接依赖HttpSession。
如果你已经全面用 JWT 或 OAuth2 Token,那 session 的使用场景会被压缩不少。但注意,Token 方案只能证明“用户身份有效”,不等于可以替代所有会话数据。临时购物车、分布式事务里的中间状态、多步表单,这些还是需要一个像 Redis 一样的临时存储。这也是为什么即使很多新项目已经“无状态化”,Redis 作为会话存储依然有很强存在感的原因。
7.2 从内存 session 迁到 Redis 时,最容易被低估的成本
很多人以为加个依赖、改几行配置就完了。实际上最大的成本在数据兼容和数据迁移。老系统里 session 可能存了大量复杂对象,你的自定义类型里可能藏着不支持 JSON 序列化的ThreadLocal、类加载器等脏东西,迁移过去必然报错。所以迁移前要做一次 session 内容的盘点:只保留必须跨请求传递的字段,把重量级对象放数据库或缓存里的单独 key,session 里只留轻量引用。
另一个成本是监控。内存 session 时代你基本不用看 session 相关指标;上了 Redis session 之后,Redis 的内存、命中率、连接数、慢查询都变成了你要养的指标。别等到线上出问题才临时去配监控,上线之前就要把 Redis 运维面板和大盘拉起来。
7.3 最后分享一个我自己的“土办法”
如果你刚接手一个老项目,不确定它当前用的到底是内存 session 还是 Redis session,先别急着看代码,直接登录一次,然后去 Redis 里执行keys *session*。有结果就是 Redis 方案,没有结果基本是内存方案。这比翻配置文件快得多。然后再看代码里HttpSession被 setAttribute 过哪些 key,决定后续优化方案。这个土办法我用过很多次,每次都能快速定位问题。
如果实在不想维护 Redis,又没有多实例需求,其实也可以换一种思路:用本地内存缓存 + 分布式失效广播,或者直接让业务层做成无状态 Token。但这些方案都有自己的复杂度,不会比一个健壮的 Redis session 方案简单太多。选型这件事,最重要的不是选最火的,而是选你团队能长期养得起的。