资讯动态

Tomcat Session共享实战:基于Redis的会话一致性方案

发布时间:2026/9/2 18:07:08 来源:尧图企业网站定制
简介面向需要在分布式系统中实现跨节点会话共享的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统一管理底层就是一个ConcurrentHashMapkey是Session IDvalue是Session对象本身。用户首次访问时Tomcat生成一个唯一的Session ID通过Cookie下发给浏览器后续每次请求浏览器带上CookieTomcat根据ID找到对应的Session从而识别出“这是同一个用户”。这种方式在单机部署时没有任何问题但一旦上了多台Tomcat麻烦就来了。Nginx做负载均衡时同一个用户的请求会被轮流分发到不同节点如果用户上一次请求落在A机器下一次落到B机器B机器根本没有这个Session ID对应的数据于是用户被当成新访客处理。表现就是用户明明登录了却频繁被踢回登录页购物车内容突然清空表单填了一半丢了状态。这类问题在线上故障里属于“高发但隐蔽”的类型因为单机测试完全复现不出来。你可能会说那让Nginx做ip_hash把同一个IP固定到同一台机器不就行了这个方案确实能解决大部分场景但有两个硬伤明显。一是移动网络下用户的出口IP可能频繁变化从WiFi切到4G/5GIP一变就被分到其他机器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如何被“骗”去读写Redis2.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或removeAttributeRedisSession会把自身标记为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 classNamecom.orangefunction.tomcat.redissessions.RedisSessionHandlerValve/ Manager classNamecom.orangefunction.tomcat.redissessions.RedisSessionManager host127.0.0.1 port6379 database0 passwordyourpassword maxInactiveInterval60 maxRedirections3 serializercom.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:开头的keyvalue就是序列化后的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依然是一个值得认真考察的备选方案。实际落地时记得多花点时间在版本选型和序列化器的选择上这两个环节决定着你后续会不会被线上问题折磨。会话共享这件事没有银弹但读懂了别人的方案理解了背后的原理你就能在任何业务场景中快速找到适合自己那一款。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价