☰
Tomcat Session共享实战:基于Redis的会话一致性方案
2026/10/14 22:31:04 网站建设 项目流程

简介:面向需要在分布式系统中实现跨节点会话共享的Java Web开发者,这套资源整合了Tomcat与Redis的Session管理方案,解决传统单机Session无法支撑负载均衡场景的问题。包内共40个文件,包括20个Java源文件、15个Jar依赖库和5个XML配置实例,压缩后约1.99MB;针对Tomcat 7/8/8.5/9及JDK 1.7/1.8准备了多套适配目录,依赖库涵盖Jedis与连接池组件,可减少环境兼容性排查成本。已有610人学习下载,说明它在会话持久化与缓存集成实践中具备一定参考价值。资源不仅包含可直接部署的Jar包和context.xml配置模板,还保留了核心源码,便于深入理解SessionStore接口与Redis后端的协作原理,从而支持会话过期策略配置、故障转移与集群场景下的二次开发调整。 做Java Web开发的朋友,大概率都遇过这样一个头疼问题:明明在A服务器上登录成功,刷新一下或者请求被负载均衡转发到B服务器,居然又让我重新登录。这个问题的根源就是Session数据只存在于各自Tomcat的内存里,彼此之间完全隔离。tomcat-redis-session-manager这个老牌开源项目,做的就是一件事:把Tomcat的Session从本地内存搬到Redis里,让负载均衡下的每一台Tomcat都读写同一份会话数据。今天这篇分享,我以这个项目为主线,从原理、版本兼容到实际部署,把整个落地过程的关键细节一次讲透。不管你是负责老系统运维的工程师,还是刚入行想搞懂Session机制的后端开发,都能从中找到可以直接抄作业的内容。

1. 这个项目解决什么问题:多机部署下的会话共享之痛

1.1 Session的“本地内存”困局

Session是Java Web服务端记录用户状态的核心机制。默认情况下,Tomcat把Session存在自己的JVM堆内存里,由StandardManager统一管理,底层就是一个ConcurrentHashMap,key是Session ID,value是Session对象本身。用户首次访问时,Tomcat生成一个唯一的Session ID,通过Cookie下发给浏览器;后续每次请求,浏览器带上Cookie,Tomcat根据ID找到对应的Session,从而识别出“这是同一个用户”。

这种方式在单机部署时没有任何问题,但一旦上了多台Tomcat,麻烦就来了。Nginx做负载均衡时,同一个用户的请求会被轮流分发到不同节点,如果用户上一次请求落在A机器,下一次落到B机器,B机器根本没有这个Session ID对应的数据,于是用户被当成新访客处理。表现就是用户明明登录了,却频繁被踢回登录页,购物车内容突然清空,表单填了一半丢了状态。这类问题在线上故障里属于“高发但隐蔽”的类型,因为单机测试完全复现不出来。

你可能会说,那让Nginx做ip_hash,把同一个IP固定到同一台机器不就行了?这个方案确实能解决大部分场景,但有两个硬伤明显。一是移动网络下用户的出口IP可能频繁变化,从WiFi切到4G/5G,IP一变就被分到其他机器,Session照样丢。二是ip_hash会让负载分摊不均匀,某个“重量级”IP一旦集中访问某台机器,其他节点闲着,这台机器扛不住。所以从架构上看,ip_hash只能算临时缓解手段,真正要根治,必须让所有节点共享同一份Session数据。

1.2 主流方案对比,这个项目站在哪一档

会话共享领域,业界有几种典型方案。最早一代的思路是Tomcat的DeltaManager,靠节点之间组播同步Session数据,配置起来复杂,而且节点一多,同步风暴能把内网打爆,现在基本没人用了。第二代就是本文要讲的tomcat-redis-session-manager,把Redis作为Session的统一存储层,优点是实现简单、对应用无侵入,缺点是过度依赖Redis的可用性,且引入了额外的序列化开销。

第三代是以Spring Session为代表的容器外方案。Spring Session不再依赖Tomcat的Manager机制,而是通过Filter拦截HttpServletRequest,把Session的存取逻辑替换成Redis操作,开发者为Spring Boot应用配置一个EnableRedisHttpSession注解即可。Spring Session的好处是生态好、跟随Spring版本迭代,天然支持WebFlux等新框架,但对老项目来说,需要改代码,引入Spring依赖,迁移成本反而高。

对比下来你会发现,tomcat-redis-session-manager的定位非常精准:它适合那些跑着Struts、Spring MVC早期版本甚至纯JSP/Servlet的老项目,这些项目不想动代码,只想通过改Tomcat配置把Session存储切到Redis。我做过一个金融老系统的改造,几万个类,贸然引入Spring Session重构风险太大,最后就是靠这个项目把会话共享问题平缓落地,只改了Tomcat的context.xml配置文件,应用代码一行没动。

2. 核心原理拆解:Tomcat如何被“骗”去读写Redis

2.1 Manager接口:接管Session生命周期

要理解这个项目的工作原理,先要明白Tomcat留给我们的扩展点在哪里。Tomcat的Session管理入口是org.apache.catalina.Manager接口,它定义了Session的创建、查找、删除、持久化等生命周期操作。默认的StandardManager把Session放在内存Map中,并通过后台线程周期性检查Session是否过期。

tomcat-redis-session-manager的核心类RedisSessionManager,就是实现了Manager接口的一个自定义Manager。它重写了createSession、findSession、removeSession等方法。createSession时,它会在Redis里写入一条新记录,并设置过期时间;findSession时,它从Redis读取Session数据,反序列化成RedisSession对象返回;removeSession则删除Redis里的对应key。对Tomcat来说,它看到的仍然是一个“正常的Manager”,只是底层存储从JVM内存换成了Redis,这个替换过程对应用层完全透明。

2.2 Valve机制:请求结束前完成持久化

光有Manager还不行,还有一个关键的时序问题要解决:Session的数据在请求处理过程中随时会被修改,比如执行session.setAttribute("user", userInfo),那这些修改什么时候同步到Redis?如果每次setAttribute都立即写Redis,性能会非常差。这个项目的做法很有意思,它额外提供了一个RedisSessionHandlerValve,在每次请求处理的最后阶段触发持久化逻辑。

整条链路的运行流程大概是这样的:请求进入Tomcat后,先经过Valve管道,到达Wrapper,调用Servlet处理业务逻辑。在这个过程里,如果调用了Session的setAttribute或removeAttribute,RedisSession会把自身标记为dirty(数据已变更)。等Servlet处理完,请求开始返回时,RedisSessionHandlerValve的postInvoke方法被触发,检查Session是否dirty,如果发现数据确实被修改过,就把整个Session对象序列化后全量写回Redis。

这里有一个性能点需要留意:项目为了追求实现的简洁性,采用的是每次请求后全量写回,而不是只写变更的字段。也就是说,即使某个Session对象里只有一个属性变了,它会连同其他所有属性一起序列化写入Redis。如果Session里塞了大对象,比如一个几MB的用户列表,那么每次请求都会产生几MB的Redis写入流量,这在高并发场景下是个不容忽视的瓶颈。我见过有人把大量业务查询结果丢进Session,结果Redis的带宽直接被打满,后来改成只存必要状态字段才好起来。

2.3 序列化:Session里对象的“搬家公司”

Session里的数据要存到Redis,就必须经过序列化。这个项目默认使用JDK原生的ObjectOutputStream进行序列化,实现方式很简单,但有两个明显缺点:一是序列化后的字节体积偏大,二是CPU开销高。项目中预留了Serializer接口,可以被替换成Kryo等高性能序列化方案。

在使用序列化器时,有几条经验值得分享。第一,无论用哪种序列化器,Session里保存的对象都必须实现java.io.Serializable接口,否则运行时会抛NotSerializableException。第二,如果你在多台Tomcat之间共享Session,所有节点的序列化器配置必须保持一致,否则A节点用Java序列化写入的数据,B节点用Kryo去反序列化,根本对不上格式,直接报错。第三,如果对已有代码做序列化器升级,旧Redis数据可能读不出来,建议在低峰期操作,并规划好数据迁移或允许用户重新登录。

3. 版本选型与兼容性:最容易踩坑的地方

3.1 几个常见代码分支的来龙去脉

tomcat-redis-session-manager这个项目名字听起来像个单一项目,实际上在GitHub上有多个分支和fork,各自适配不同的Tomcat版本,这是新手最容易踩坑的地方。我第一次用它就是因为下载了错误的版本,启动时直接报ClassNotFoundException,折腾了好几个小时。

最早的版本由jcoleman维护,基于Tomcat 7开发,底层用的是Jedis 2.x和commons-pool,功能比较基础。后来orangefunction对这个项目做了fork和较大改进,配置项更丰富,稳定性也更好,这个版本在Tomcat 7/8场景下使用最广,网上大部分教程默认指的都是它。再后来,有开发者觉得Jedis性能不够好,用Lettuce客户端重写了一个分支,比如Mag Arena的tomcat-cluster-redis-session-manager,特点是支持Tomcat 8.5/9,且Lettuce天然支持异步和集群模式。

3.2 Tomcat版本与分支搭配速查表

根据我实际用下来的经验,不同Tomcat版本建议选择的搭配大致如下,可以先按这个表去试,能少走很多弯路。

Tomcat版本推荐分支底层客户端备注
Tomcat 6 / 7jcoleman版Jedis + commons-pool老项目常用,功能最简单
Tomcat 7 / 8orangefunction版Jedis社区使用最广,教程多
Tomcat 8.5 / 9Mag Arena分支Lettuce支持新版本,需自己编译较多
Tomcat 10+都不建议直接用-javax.servlet已改为jakarta.servlet,旧jar包无法加载

特别提醒一点,Tomcat 10是一个分水岭。从Tomcat 10开始,Java EE的命名空间从javax.迁移到了jakarta.,而这个老项目编译时依赖的还是javax.servlet包,直接放到Tomcat 10/11下会启动失败。如果你必须用Tomcat 10以上的版本,要么自己改源码重新编译,要么干脆转向Spring Session方案,后者对新版本的支持更及时。

3.3 如果项目太老怎么继续用

这个项目历史上停更过一段时间,有些读者会担心它是否还“值得用”。我的观点是,老不代表不能用,关键在于你有没有能力控制风险。一个比较稳健的做法是做二次维护:把代码fork下来,替换掉过旧的依赖版本,重新编译打包。比如把Jedis升级到2.9.x甚至3.x(注意API变化),把commons-pool2的版本对齐,再把项目用Maven构建一遍,放到目标Tomcat上做冒烟测试。这样做一次之后,后续维护基本就是一劳永逸。

4. 从下载到联调:一次完整的落地实操

4.1 准备jar包与依赖

部署的第一步是准备jar包。以使用最广的orangefunction版为例,你需要把tomcat-redis-session-manager的jar包、Jedis的jar包以及commons-pool2的jar包一起放进Tomcat的lib目录。注意不是放到Web应用的WEB-INF/lib下面,而是放到Tomcat全局的lib目录,因为这个Manager是在Tomcat容器层面工作的,不是应用层面。

我在实际操作中遇到过一种情况:应用自身也引用了Jedis,而且版本和Tomcat lib下的不一致。Tomcat的类加载机制是父优先,极有可能加载Tomcat lib下的老版本Jedis,导致应用里的Redis操作出现NoSuchMethodError。遇到这种冲突,有两个处理思路:一是把两边版本统一,最好都用新版本;二是调整类加载策略,让应用优先加载自己的类,但这样可能会导致Manager和应用里的Jedis重复初始化,反而更乱。更推荐前者,统一在Tomcat层面维护一份依赖。

4.2 修改context.xml的核心配置

配置集中在Tomcat的conf/context.xml文件中,在Context节点下同时配置Valve和Manager两个组件。一个完整的配置文件长这样:

<Context> <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve"/> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" host="127.0.0.1" port="6379" database="0" password="yourpassword" maxInactiveInterval="60" maxRedirections="3" serializer="com.orangefunction.tomcat.redissessions.JavaSerializer"/> </Context>

这里几个关键参数逐个说。host和port不用解释,指向Redis实例。database用于指定会话数据落在Redis的哪个逻辑库,默认0,如果你的Redis里已经有很多业务数据,建议单独开一个库比如2,避免key冲突,也方便单独做过期清理。password是Redis认证密码,如果Redis没设密码可以留空。maxInactiveInterval是Session的过期时间,单位是秒,它直接控制Redis里key的TTL,需要和应用原本的Session超时策略保持一致。maxRedirections牵扯到Tomcat的Session穿越机制,简单说就是同一个请求在没有Cookie的情况下发生重定向时,允许跨容器重建Session的次数,保持默认3即可,不建议随意调大。

注意一个细节:如果只想让某一个Web应用启用Redis Session,而不是整个Tomcat,不要把配置写在conf/context.xml里,而是放到该应用的META-INF/context.xml文件中。这样不同应用可以有自己的Session策略,互不干扰。

4.3 验证会话共享的两种方法

配置完成后,重启Tomcat。启动日志中如果能看到类似“RedisSessionManager: Initializing...”的输出,说明Manager加载成功。接下来需要验证Session是否真的共享了。比较快的办法是看Redis里的key,执行命令:

redis-cli keys 'tomcat:session:*'

正常情况下,访问一个会生成Session的页面后,Redis里会出现一个tomcat:session:开头的key,value就是序列化后的Session数据。用SELECT 0切换到你配置的database,再查看ttl,可以看到它和配置的maxInactiveInterval对得上。

更严谨的做法是写一个测试Servlet,放在两台Tomcat上,然后用Nginx做轮询分发去访问。测试逻辑很简单:Session里放一个计数器,每次请求加一,把Session ID、计数器和当前节点名返回给客户端。

import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class SessionTestServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session = req.getSession(); Object value = session.getAttribute("counter"); int counter = (value == null) ? 0 : (Integer) value; counter++; session.setAttribute("counter", counter); resp.getWriter().println(String.format( "sessionId=%s, counter=%d, node=%s", session.getId(), counter, System.getProperty("catalina.base"))); } }

反复刷新页面,如果看到node字段在A、B之间切换,而counter一直连续递增,说明两台Tomcat确实在读写同一个Redis里的Session数据,会话共享配置成功。如果counter重新从1开始,说明Session没有共享成功,优先检查两边配置是否一致,尤其是Redis地址、database是否相同。

5. 常见问题与排查记录

5.1 启动阶段的高发问题

有一个现象我见过多次:配置全部正确,但Tomcat启动时直接在Manager初始化阶段报ClassNotFoundException。原因绝大多数是jar包没放全。RedisSessionManager底层依赖Jedis操作Redis,依赖commons-pool2维护连接池,缺了任何一个都会初始化失败。处理方法是把tomcat-redis-session-manager、jedis、commons-pool2三个jar包全部放进Tomcat lib目录,而且要注意版本兼容。Jedis 2.x搭配commons-pool2 2.4.x通常没问题,Jedis 3.x则需要commons-pool2 2.6.x以上。

还有一种情况是jar包明明放齐了,却报NoSuchMethodError,这多半是版本冲突。比如Tomcat自带了某个旧版本的commons-pool,而你引入的Jedis需要新版本的方法,两者冲突导致运行时找不到方法。解决办法是把Tomcat lib下冲突的旧版本jar替换成新版本,或者把新版本挤到应用自己的lib里,但要保证两边统一。

5.2 运行阶段的老大难问题

配置完成后,最常碰到的运行期问题是NotSerializableException。某次给客户做改造,对方上线后半夜出现大量报错,查日志发现是Session里存了一个自定义的权限对象,这个对象没实现Serializable接口。业务代码里的Session对象五花八门,排查起来很痛苦。这里给一个建议:改造前用静态扫描找一下项目里所有Session的setAttribute调用,逐个确认存入的对象是否可序列化,一次性处理完,省得上线后被日志轰炸。

还有一个容易被忽略的问题:过期Session在Redis里堆积。正常情况下Session过期后Redis会自动删除对应key,但如果maxInactiveInterval配置成-1(永久有效),或者高并发下Redis的过期扫描不及时,Redis里会残留大量无用的session key。我的排查经验是用SCAN命令配合匹配模式去统计:

redis-cli --scan --pattern 'tomcat:session:*' | wc -l

生产环境千万别用keys命令去全量扫描,尤其Redis里key多的时候,会阻塞Redis服务。如果发现堆积,有两种清理思路:一是调整maxInactiveInterval为合理值,让Redis的过期机制自动处理;二是写一个定时任务,定期SCAN并删除超过一定TTL的session key,但要注意和业务错开高峰。

5.3 排查工具与速查表

把常见问题整理成一张表,平时排查问题可以直接对照,效率会高很多。

现象可能原因处理方法
启动即报ClassNotFoundExceptionjar包缺失或版本不匹配确认Manager、Jedis、commons-pool2均放入lib目录
启动报NoSuchMethodError依赖版本冲突统一Jedis和commons-pool2的版本
运行时抛NotSerializableExceptionSession中对象未实现序列化接口给对象实现Serializable接口或更换序列化器
多节点之间Session数据不共享配置不一致或database不一致逐个核对host、port、database配置
Redis中session key堆积过期时间设置过长或未开启过期清理调短过期时间,用SCAN统计后清理
HTTP响应出现set-cookie覆盖旧session cookie与新id冲突清理浏览器Cookie,或检查maxRedirections设置

排查阶段我习惯配合Redis的MONITOR命令实时观察请求写入Redis的情况,先用cli连接Redis,执行MONITOR,然后访问业务页面,如果看到SET或EXPIRE命令,说明Tomcat确实在写Redis。这个办法比翻日志直观很多,适合快速确认链路是否走通。

5.4 一个容易被忽略的Cookie细节

浏览器端Session ID是通过Cookie传递的,默认Cookie名为JSESSIONID。如果项目里有两个Web应用上下文,或者Nginx配置了不同域名,浏览器可能同时存在多个JSESSIONID Cookie,导致Tomcat拿到的是错误的Session ID,表现为“Session总是保存不住”。我遇到过最诡异的一次故障,用户反馈时好时坏,查了所有代码和Redis配置都没问题,最后发现是Nginx上有人配了多个Server块,后端两个域名共用了同一个Cookie域。解决方法是给Manager配置自定义Cookie名,或者调整Cookie的Domain属性。这个细节虽然不常发生,但一旦发生,排查成本非常高,值得记在脑子里。

6. 写在最后的一点个人体会

我最初接触tomcat-redis-session-manager是在一个传统保险项目里,当时刚接手运维,对Tomcat内部机制的理解还停留在“能用就行”的层面。为了排查一个莫名其妙的用户掉线问题,我啃了两天源码,把Manager、Valve、Session序列化这几个概念啃通了,才发现原来Tomcat的扩展性比我想象中强大得多。这个项目尽管技术上不算炫酷,甚至有些代码风格带着上世纪的痕迹,但它确实是一个极好的教学范本。

如果你现在维护的是Spring Boot新项目,我建议你直接选Spring Session,没必要在这个老项目上折腾。但如果你面对的是改不起代码的老系统,又在为多机Session共享发愁,tomcat-redis-session-manager依然是一个值得认真考察的备选方案。实际落地时,记得多花点时间在版本选型和序列化器的选择上,这两个环节决定着你后续会不会被线上问题折磨。会话共享这件事没有银弹,但读懂了别人的方案,理解了背后的原理,你就能在任何业务场景中快速找到适合自己那一款。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询