简介:Tomcat 8 应用在分布式部署时,会话共享常成为瓶颈。此压缩包集成了 tomcat-redis-session-manager 所需全部运行依赖,将 Redis 作为 Session 存储后端,使多节点间用户状态保持一致,提升高并发场景下的扩展性。适合具备 Java Web 基础、希望优化会话管理的中级开发者,接入时无需改动业务代码,已有 940 人学习使用。压缩包共 6 个文件,以 4 个 jar 依赖为主(含主库、Jedis 驱动、连接池及日志组件),同时提供 1 份 context.xml 配置模板与 1 份说明文档,整体仅 445KB。配置模板具体演示了 Manager 节点与 Redis 连接参数,说明文档则讲解序列化及过期策略,便于快速部署至 Tomcat 的 lib/conf 目录。通过这份资源可免去逐一检索 jar 包及匹配版本的繁琐,直接获得一套可验证的 Session 共享方案,在本地环境演示 Session 在 Redis 中的存取过程,为后续性能调优和集群扩展提供可靠基础。
1. tomcat-redis-session-manager是什么:先把你从“两台Tomcat反复掉线”里救出来
两台Tomcat挂在负载均衡后面,用户在节点A登录成功,下一个请求被转发到节点B,登录态直接没了,被踢回登录页,这是集群会话没共享的典型症状。tomcat-redis-session-manager就是解决这个问题的组件:它把HttpSession从Tomcat内存挪到Redis,所有节点读写同一个Redis里的Session,节点挂了流量切走,用户登录态还在,重启也不再丢会话。这个包集合在Tomcat 8环境里很常用,部署成本低、见效快,是中小集群做会话共享最常见的一条路。
这篇笔记面向两类读者:一类是刚把应用从单机改成多节点、天天被踢登录的开发者;另一类是运维和架构师,想在不改业务代码的前提下把会话外置,又怕引入不稳定方案。下面我会把这个组件的运行原理、Tomcat 8下的jar包集合构成、配置参数和踩坑点一路讲清楚,最后给出一套可以照着复现的验证步骤。
2. 运行原理与选型:为什么用Redis接管Session而不是粘性会话
2.1 一次请求的生命周期:反序列化、回写与TTL
这个管理器的核心套路是Session外置。从代码层面看,HttpSession的存储位置从Tomcat的StandardManager换成了RedisSessionManager。它的类名在不同分叉里不一样,但职责相同:实现Tomcat的Manager接口,接管Session的创建、读取、保存和销毁。
一次请求的完整生命周期是这样的:请求打到Tomcat节点,容器拿到JSESSIONID后,SessionManager去Redis执行GET,取出一个序列化后的Session对象,反序列化成内存对象挂到当前请求线程;业务代码正常读写session属性;请求结束时,Manager判断Session是否有变化,有则序列化后SET回Redis,并刷新过期时间。整个过程对业务代码透明,你该用getSession、setAttribute还照样用,只是背后存储换了地方。
// 伪代码,展示管理器做了什么 public class RedisSessionManager extends ManagerBase { public Session findSession(String id) { // 1. 从Redis读取序列化数据 byte[] data = redis.get(buildKey(id)); // 2. 反序列化成Session对象 return deserialize(data); } public void save(Session session) { // 3. 序列化并写回Redis,设置TTL redis.setex(buildKey(session.getId()), ttl, serialize(session)); } }逻辑和参数说明:这里的buildKey通常会加前缀避免业务key冲突,常见的是“Tomcat:Session:”加sessionId;ttl来自Manager配置的maxInactiveInterval,单位是秒。这两点直接决定Session能不能在节点间互通、多久过期。如果你在某次改动里把maxInactiveInterval调短了,会发现用户没过几分钟就被踢,原因是Redis的TTL跟着这个值走,不是Tomcat在管超时。
从这里面能看出选型的第一理由:无状态节点。业务节点不再持有用户状态,横向扩容、滚动重启都不影响在线用户。代价是每次请求多一次Redis访问,网络延时会成为新瓶颈,Session数据太大的时候尤其明显。
2.2 社区版与商业版两个分支怎么选
这套组件在开源世界里的谱系不简单。最初由社区开发者发布,代码托管在公共仓库;后来商业公司接手改造,衍生出了自己的版本。很多刚接触的人把两个版本混着用,jar包和配置类名对不上,启动直接报ClassNotFound。
我的建议是:中小项目优先社区版,因为它够用、资料多、坑别人都踩过;对稳定性要求极高、愿意付费的项目才考虑商业版。两者的差异集中在序列化器、连接池处理和哨兵支持的完整度上,社区版对Redis Sentinel的支持比较弱,商业版做得更完整。
从评估角度列一个对比:
| 对比维度 | 社区版 | 商业版 |
|---|---|---|
| 授权与成本 | 开源免费 | 需购买授权 |
| 配置类名 | 常见带 orangefunction 包路径 | 独立包路径,各版本不一致 |
| 序列化器 | 默认JDK,有JSON扩展可选 | 内置高效序列化 |
| Sentinel 支持 | 弱,部分构建不支持 | 完整 |
| 维护活跃度 | 取决于社区 | 跟随商业版本迭代 |
如果你预算有限又不想被序列化性能卡脖子,先别急着付费,把第四、五章看完,社区版加JSON序列化器在大部分场景下已经够用了。
2.3 底层依赖:Jedis、连接池与高可用
管理器本身不直接连Redis,它依赖Jedis客户端。整个包集合里,管理器jar、Jedis jar、连接池jar是绑定的,版本之间互相有要求。特别是Jedis,社区版管理器大多是在Jedis 2.x时代写的,你换成4.x的高版本,常见翻车是连接池API对不上或者Jedis返回结构变化,运行时才报错,而且报错信息很隐晦。
高可用方面,如果你只有一台Redis,管理器连的是单点Redis,Redis一挂整个集群的会话就全丢了,虽然不会崩溃,但所有用户要重新登录。生产环境我会强烈建议Redis至少做主从,配置管理器支持的主从或哨兵地址。如果社区版Sentinel配置不行,就在前面加一层高可用代理,把连接入口透明化。
连接池参数也别照抄默认。默认的maxTotal不大,高并发下Jedis连接耗尽,表现是接口响应变慢、日志里出现连接获取超时。一个保守起步配置是maxTotal=100、maxIdle=30、minIdle=10,再根据你的QPS和Session访问量压测调整。
3. Tomcat 8环境准备与Redis前置检查
3.1 先认清Tomcat 8的小版本差异
Tomcat 8.0和8.5不是同一个时代的东西。8.5引入了大量9.0的特性,特别在Digester机制上加了安全限制,导致一些老版本管理器无法在8.5上正常解析配置,启动时直接抛异常。所以先确认你用哪个版本:
# Linux 下查看 Tomcat 版本 $CATALINA_HOME/bin/version.sh # Windows 下 %CATALINA_HOME%\bin\version.bat # 或者直接查看 lib 目录 ls $CATALINA_HOME/lib | grep catalina逻辑说明:你需要在动手下载jar前就搞清楚版本,因为8.0和8.5对应不同的管理器构建。如果坚持用Tomcat 8.5,务必找明确标注支持8.5的构建版本;如果考虑直接升9,我更推荐,因为新版本的兼容性修复更多。但这个包集合标题既然锚定Tomcat 8,下面按Tomcat 8系列展开。
注意一点:Tomcat 10和8之间Servlet API的包名变化很大,本文这套配置不适用于10以上,别用这个包集合硬套。
3.2 Redis端确认三件事:密码、库号、内存上限
Session数据写进Redis之前,先确认Redis本身参数,不然到时候出了事你分不清是管理器问题还是Redis配置问题。我一般按三个顺序查:
redis-cli -a <密码> ping redis-cli -a <密码> info memory | grep -E "maxmemory|used_memory" redis-cli -a <密码> config get databases逻辑说明:第一行确认连接和密码正确;第二行看内存上限,如果maxmemory为0说明没限制,但如果服务器内存有限,Session无限膨胀会把Redis耗死;第三行确认库号,默认0,别把Session写到其他业务共用的库里。我的习惯是给Session单独开一个Redis实例或者单独库,这样清数据、调参数都不影响其他业务。
Session的value大小也别忽略。如果你把用户信息、购物车、权限列表全塞进Session,一次请求就要把这一坨数据在Redis和Tomcat之间来回传一次。会话里应该只存能恢复身份的小对象,例如userId、租户Id,其余数据按需查询。这样既省带宽也降低序列化开销。
3.3 确认Java版本与Servlet API兼容性
管理器本质上是一组Servlet容器扩展,对Java版本和Servlet API有要求。Tomcat 8默认Java 7以上,8.5要求Java 7+,Java 9、11、17都见过跑得正常的。但有一个隐藏点:你用高版本JDK编译的类序列化后,低版本JDK的节点反序列化可能报Class版本错误,多节点环境里各节点JDK版本必须一致。
另一个容易忽略的是各节点的容器版本要一致。混用Tomcat 8.0和8.5时,Session数据格式和序列化行为不一定兼容,同一个Redis里的Session在A节点能读,在B节点可能反序列化失败。节点软件环境的一致性是这套方案不出幺蛾子的前提。
4. 下载与部署:把tomcat8的jar包集合落到Tomcat的lib目录
4.1 jar包集合的构成:至少这三个
标题里的“包集合”指的不只是一个jar,而是管理器本体加依赖。我见过很多人在这一步只丢一个jar进去,启动后报缺类,其实缺的就是依赖。
常见的包集合构成:
# Tomcat 8 下需要的 jar 文件 tomcat8-redis-session-manager-*.jar # 管理器本体 jedis-*.jar # Redis 客户端 commons-pool2-*.jar # Jedis 连接池依赖逻辑说明:管理器本体提供RedisSessionManager实现;Jedis负责和Redis通信;commons-pool2在Jedis使用连接池模式时被引用。有的发布包把commons-pool也绑定了,有的需要你自己补,具体看下载说明。缺哪个jar,启动日志里的NoClassDefFoundError会直接告诉你是哪个类,顺着类名去推断jar就行。
配置时另一个常见做法是把这些jar放到每个Tomcat的lib目录,而不是直接扔进Web应用的WEB-INF/lib。放Tomcat的lib目录意味着所有部署在这个节点上的应用都能用这套Session管理器;只放某个应用的WEB-INF/lib的话,只有那个应用生效,而且可能和容器自带的Session机制打架。集群环境下所有节点的Tomcat lib目录都要放同一份。
4.2 两个下载路径:依赖坐标与源码自构建
jar包的获取路径要分情况。如果项目的构建工具是Maven,最省事的方式是用依赖坐标引入,版本以仓库里能搜到的最新稳定版为准;如果你的环境里没有合适的坐标,或者你不想冒版本风险,那么下载源码包自己构建是更稳妥的路。构建通用步骤是:
# 下载管理器源码包并解压 unzip tomcat8-redis-session-manager-src.zip cd tomcat8-redis-session-manager # 用 Maven 构建,跳过测试加快速度 mvn -DskipTests package逻辑说明:构建产物在target目录下,把产出的管理器jar连同依赖拷贝到Tomcat的lib目录即可。自构建的好处是你能拿到和当前Tomcat版本匹配的构建,坏处是你要自己准备本地Maven环境和依赖下载,第一次搭可能要花半小时。如果图省事,先直接找现成的包集合,但注意确认它声明的Tomcat版本范围,别拿个Tomcat 7的包硬塞进Tomcat 8。
我一般建议先走Maven坐标。两个理由:依赖关系由Maven自动拉齐,不会漏jar;后续升级版本只需改坐标。离线环境或者内网环境再考虑源码构建。
4.3 修改context.xml:一组可以直接抄的配置
jar就位后,修改Tomcat的conf/context.xml,在Context标签内加入Manager配置。以社区版为例,一个典型的配置长这样:
<Context> <WatchedResource>WEB-INF/web.xml</WatchedResource> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" host="127.0.0.1" port="6379" database="0" password="your-redis-password" maxInactiveInterval="1800" sessionPersistPolicy="SAVE_ON_CHANGE" maxTotal="100" maxIdle="30" minIdle="10" /> </Context>参数说明:host和port填Redis地址,注意不要填localhost,多节点时一定要填能被所有Tomcat节点访问到的内网IP;password留空表示Redis无密码,但生产环境必须有密码;database指定库号;maxInactiveInterval单位是秒,1800即30分钟,这个值同时决定Redis的TTL;sessionPersistPolicy控制保存时机,SAVE_ON_CHANGE表示Session属性变化时才写回Redis;maxTotal、maxIdle、minIdle是Jedis连接池参数。
如果你用的是商业版,类名和参数名会不一样,尤其有的版本序列化器要单独指定键,否则走默认。配置文件一共有这一处要改,但注意context.xml是对整个Tomcat生效,想只对某个应用启用Session外置,就放到那个应用的META-INF/context.xml,两者优先级以应用内配置为准。
4.4 启动检查:从日志和Redis确认生效
配置改完,干净地重启Tomcat,然后分三步确认它真的生效了。第一步看启动日志:
# 重启 Tomcat 后,查看 catalina.out 中的关键日志 tail -f $CATALINA_HOME/logs/catalina.out # 启动日志里应当出现 SessionManager 初始化的信息 grep -i "RedisSessionManager" $CATALINA_HOME/logs/catalina.out逻辑说明:不同构建输出格式不同,但只要看到和RedisSessionManager相关的信息,或者启动没有报ClassNotFound、连接拒绝,就说明管理器被加载了。第二步是访问你的应用,curl一个会创建Session的页面,让管理器和Redis完成首次交互。第三步在Redis客户端里确认key真的存在:
redis-cli -a <密码> # 列出所有以 Session 前缀开头的 key keys *Session* # 查看某个 session 的 TTL ttl "Tomcat:Session:<sessionId>"逻辑说明:看到key存在、TTL和maxInactiveInterval接近,说明Session已经落到了Redis。到了这一步,单节点的Session外置就通了,但集群共享能不能成立,还要按最后一章的验证方法在双节点上测一遍。
5. 避坑:序列化、TTL与Tomcat 8.5的兼容性
5.1 类没实现Serializable:JDK序列化的隐藏炸弹
现象:应用刚刚接入管理器,首次登录一切正常,点两下页面突然抛java.io.NotSerializableException,堆栈指向某个实体类,而且有时只在特定接口触发。
原因:默认序列化器是JDK序列化,Session里存的每个属性对象都必须实现java.io.Serializable。你以为存了个普通JavaBean进去,但它内部嵌套了不序列化的成员,照样炸。这个错误在单机Tomcat从不出现,因为内存Session根本不需要序列化,所以很多老代码从来没做过序列化约束。
解决:给对象实现Serializable并生成serialVersionUID;如果对象来自第三方库且无法改,就不要把整个对象塞进Session,改为存它的Id或必要字段,请求时再查出来。做这个改造时,注意嵌套对象的每个层级都要检查,特别是继承来的父类字段。这是社区版默认配置隐藏最深的坑,也是很多人第一次用它就劝退的原因。
5.2 Tomcat 8.5启动直接失败:Digester安全限制撞上老构建
现象:Tomcat 8.0上跑得好好的,换到8.5或9,Tomcat启动立即失败,catalina.out里出现Digester相关的安全报错,指向context.xml中的Manager元素。
原因:Tomcat 8.5开始收紧了Digester反射权限,不再允许XML配置里通过任意类名实例化非白名单对象。老版本管理器构建没适配这个限制,自然被拒。
解决:三条路任选。第一,换成明确支持Tomcat 8.5的更新构建,这是最推荐的路;第二,如果必须用老构建,在catalina.properties里把相关包路径加入Digester的扩展白名单属性,不同构建要求不同,看了报错再对症配;第三,干脆把Tomcat停在8.0.x。这个问题网上问的人很多,但根因就一条:容器安全机制变了,管理器构建跟不上。
5.3 maxInactiveInterval与Redis TTL脱节:用户20分钟掉一次
现象:配置里明明写maxInactiveInterval为1800,但Redis里Session的TTL只有几百秒,用户隔一会儿就被踢下线;或者反过来Redis的TTL很长,用户早已不活跃却仍被认定为在线。
原因:这个组件的过期机制是Redis的TTL在管,而TTL来自配置的maxInactiveInterval。如果两个节点配置的这个值不一致,或者Redis那把key的TTL在运行中被其他清理任务重置过,就会出现脱节。还有一种常见情况:你把sessionPersistPolicy设成了每次请求都写回,但Redis的TTL是在写回时刷新的,如果某一次写回时maxInactiveInterval取到了错误的值,TTL就错了。
解决:所有节点统一maxInactiveInterval;在Redis里用ttl命令抽查Session key,长时间观察TTL是否随着访问在刷新;如果有定时任务在清理Redis,确认清理逻辑没有把Session前缀的key当成临时数据删掉。这个坑最大的迷惑性在于:它不报错,只是让你觉得系统偶发掉线,很多人先怀疑网络再怀疑代码,最后才发现是TTL策略的问题。
5.4 Jedis版本随便升:连接池罢工的高版本暗坑
现象:用管理器默认依赖跑得好好的,出于“升级依赖更安全”的想法把Jedis升到大版本,应用启动正常,但高并发时频繁出现连接获取超时或者JedisConnectionException,降低并发反而稳定。
原因:管理器在旧版Jedis的API之上封装,高版本Jedis的连接池配置项、异常类型甚至命令返回值都有变化,旧管理器没有适配。类不报错不代表行为一致,这是典型的运行时兼容问题。升级依赖之前先查管理器官方说明里支持的Jedis版本范围。
解决:以管理器发布时绑定的Jedis版本为准,不要单独升。真有必要升级,先看序列化器和连接池代码里有没有用到被废弃的API,再做全链路压测。很多项目栽在这里,升级的动机是好的,但忽略了组件之间的版本契约。我自己的习惯是把坐标里Jedis的版本显式钉住,不给传递依赖浮动更新的机会。
6. 进阶验证:双节点会话共享与JSON序列化器改造
6.1 两分钟验证双节点会话共享
部署完成后最要紧的是验证“节点A写入、节点B读出”。常见做法是起两个Tomcat节点,端口错开,比如18080和18081,部署同一个应用,应用里写一个会读取并打印Session属性的接口。
操作步骤:在浏览器或curl脚本里,把请求固定到18080,Set进去一个属性,拿到JSESSIONID;再把同一个JSESSIONID用Cookie头发到18081,看接口能否读到同一个属性。如果读到了,集群会话共享成立。注意两个请求之间Redis必须有写回完成,建议在A节点的写接口后停一拍再请求B节点。
# 节点 A:写入属性 test=hello curl -i -c cookies.txt "http://127.0.0.1:18080/app/set?key=test&value=hello" # 节点 B:用同一个会话 ID 读取属性 curl -b cookies.txt "http://127.0.0.1:18081/app/get?key=test"逻辑说明:curl的-c是保存Cookie,-b是带上Cookie,只要两次用的是同一个JSESSIONID,B节点就会去Redis里加载Session。如果你在B节点读不到,先回Redis查key是否还在,再确认两个应用是否部署在同一个Context路径下。Cookie路径不一致是这里最容易犯的错:两个节点应用分别部署在/和/app下,JSESSIONID的path就不同,浏览器根本不会把Cookie发给路径不对的节点。
6.2 自定义JSON序列化器
默认JDK序列化的性能和跨语言短板我已经在前面说过,如果你需要更快的序列化,最常见的破解思路是换JSON序列化器。部分封装好的构建自带JsonSerializer类,在配置里把序列化器指定成它即可;如果构建没带,就需要自己实现一个Serializer接口,代码量不大。
// 一段最简 JSON 序列化器骨架 public class JsonSerializer implements Serializer { @Override public byte[] serialize(Session session) throws IOException { Map<String, Object> data = session.getAttributes(); return jsonMapper.writeValueAsBytes(data); } @Override public Session deserialize(byte[] bytes, SessionManager manager) { // 反序列化并重建 Session 对象 return sessionBuilder.fromJson(bytes, manager); } }逻辑和参数说明:serialize负责把Session里的属性集合转成字节,deserialize负责把字节转回Session。换序列化器的坑在于:如果当前Redis里已经有JDK序列化的旧Session,新序列化器反序列化会直接失败,所以要清空Redis里的Session前缀数据再切换,让用户重新登录一次。另外,JSON序列化对复杂嵌套类型还原不友好,泛型集合、内部类、枚举都容易翻车,需要在类型信息上做处理。换JSON之后,建议用6.1的验证方法重新走一遍双节点读写,确认对象还原没有丢失字段。
6.3 一个小习惯
我在生产环境用这套东西超过三年,习惯是每次改动后把Redis里的Session前缀key清一次,避免新旧数据格式混在一起产生幽灵问题。这个包集合的复杂度其实不高,大部分翻车都出在版本匹配和序列化器上。按本文的顺序先确认Tomcat小版本、备齐jar集合、统一配置参数,再动手验证,半天就能跑通。希望帮到你。
本文还有配套的精品资源,点击获取