ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Tomcat 8 集群 Session 共享:Redis Session Manager 配置与避坑指南

Tomcat 8 集群 Session 共享:Redis Session Manager 配置与避坑指南

简介:针对Tomcat8与JDK1.8环境下的Java Web应用集群部署场景,下载包整合了tomcat-redis-session-manager所需的核心依赖与配置模板。默认Tomcat的Session保存在本地内存,难以在多个实例间共享;引入Redis存储后,用户会话可集中存放,实现多节点无缝切换,有效提升系统并发吞吐与故障恢复能力。压缩包共6个文件,包含4个功能不同的jar包(其中管理器负责与Redis交互,Jedis客户端执行缓存命令,连接池管理资源)、1份context.xml配置样例和1份使用说明txt,整体大小仅445KB,便于直接放入Tomcat的lib目录快速试用。目前已有940人学习下载,适合正在研究分布式会话同步问题或准备升级现有架构的初中级开发者。资源对配置参数、序列化策略及常见报错给出了梳理,可帮助理解Manager接口的工作机制,缩短环境搭建与调试验证的时间,让刚接触Redis会话管理的读者也能顺利跑通。

1. Tomcat 8 的 Session 丢失困局:为什么要折腾 Redis Session Manager

接手某实验室的模拟项目X时,应用在单台 Tomcat 上一切正常,一部署到两台服务器做负载均衡,用户就频繁被踢下线,登录状态反复消失。排查到最后原因很朴素:Tomcat 默认把 Session 存在本地内存里,节点 A 存的会话节点 B 看不到,重启 Tomcat 更是直接清空。给 tomcat-redis 做补偿的这套包集合,就是把“本地内存”换成 Redis,通过 Tomcat 自带的 Valve 和 Manager 机制,在每次请求结束时把 Session 序列化写进 Redis,其他节点再从 Redis 读回同名会话。

这套方案不是给你一个现成的分布式会话框架,它的定位很窄:你的应用需要多节点共享 Session,又不打算引入重型组件,那它是最短落地路径。适合两类人,一类是在做集群改造、被 Session 共享卡住的运维或后端,另一类是刚把应用拆成多节点、发现登录态到处丢的初中级开发者。新手按第 2 章的清单放好 jar,照第 3 章的配置抄一遍就能跑通;熟手可以跳过前两章,直接看第 4 章的踩坑记录和第 5 章的验证方法。

2. 包集合里都有什么:依赖清单与版本匹配逻辑

2.1 解压之后的三件 jar:管理类与两个必配依赖

拿到这个下载包先别急着扔进 Tomcat,我习惯先解压看清单。里面不是单个文件,而是三件套:核心的管理 jar,加上 Jedis 客户端和 commons-pool2 连接池。这个组合不是随便凑的,三者有明确的调用链:核心 jar 通过 Valve 介入 Tomcat 的 Session 生命周期,用 Jedis 去连 Redis,而 Jedis 内部的连接池又依赖 commons-pool2 来创建和管理连接。缺一个,启动时要么找不到类,要么连接池直接初始化失败。

三件套默认放进 Tomcat 的 lib 目录,注意不是应用自己的 WEB-INF/lib,很多第一次做的人在这里翻过车,后面排查章节会专门讲。文件清单和各自职责先列出来:

文件类型作用放置位置
tomcat-redis-session-manager(Tomcat 8 版)jar核心实现,内含 RedisSessionHandlerValve 与 RedisSessionManagertomcat/lib
jedis-2.x.x.jarjarRedis Java 客户端,负责连接、读写命令tomcat/lib
commons-pool2-2.x.x.jarjar通用连接池,Jedis 的连接复用依赖它tomcat/lib

核心 jar 是整个方案的入口。它里面两个关键类分别对应配置里的两行内容:RedisSessionHandlerValve 是一个 Valve,Tomcat 处理每个请求时都会经过 Valve 链,它负责在请求结束时把当前 Session 同步到 Redis;RedisSessionManager 则负责 Session 的创建、查找、销毁,并和 Redis 里的数据做关联。也就是说,一个管“什么时候存”,一个管“存哪里”。Jedis 和 commons-pool2 是底层支撑,Jedis 负责把对象变成 Redis 命令,pool2 负责把连接复用起来,避免每个请求都重新建 TCP 连接。

2.2 版本匹配红线:为什么 Tomcat 8 需要专用包而不是通用包

tomcat-redis-session-manager 并不是处于活跃维护状态的项目,最早的实现只针对 Tomcat 6/7 的内部 API。Tomcat 8 对 Session 管理器的接口做过调整,老版本的 jar 在 Tomcat 8 上启动时 class 能加载,但运行到具体方法时会因为内部类找不到或签名变化直接抛异常。这个下载包标了 tomcat8,指的是已经针对 Tomcat 8 编译过的产物,用来规避接口不兼容的坑。

版本上还有一层匹配关系:Jedis 和 commons-pool2 必须配套。Jedis 2.x 系列要求 commons-pool2 也使用 2.x,如果你机器上已经有老版本的 commons-pool,比如 1.x,启动时连接池初始化就会报 NoSuchMethodError,报错位置通常在 org.apache.commons.pool2.impl.GenericObjectPoolConfig。验证方法很简单,Tomcat 的 lib 目录下同时存在 commons-pool 和 commons-pool2 时,优先把老版本移走,只留 pool2。

另外提一句,有人问能不能用 Lettuce 替代 Jedis。可以,但没必要。这个包集合的 Manager 内部是直接调用 Jedis API 的,换成 Lettuce 意味着要改核心 jar 的源码并重新编译,等于自己维护一个分支。下载包里已经配套好了,直接用现成组合是最省事的。

2.3 工作原理:Valve 拦截与 Manager 管理的双链路

把这个机制拆开看,一次请求的 Session 流转其实是两段代码配合。第一段是 Valve 链,Tomcat 在处理请求前会按顺序经过多个 Valve,容器自带的 Valve 负责访问日志、认证等,RedisSessionHandlerValve 被插入这条链,它在请求处理完成的 finally 阶段触发一次 session 持久化。第二段是 Manager,它负责和 Redis 交互,session 创建时往 Redis 写一条记录,请求进来时根据 sessionId 去 Redis 查,查不到就新建。

这里有个容易被忽略的细节:Valve 和 Manager 不是二选一,而是必须同时配置。只配 Manager 不配 Valve,Session 只有在创建或销毁时才会同步到 Redis,每次请求里对 Session 属性的修改不会及时写回;只配 Valve 不配 Manager,连往 Redis 写什么都不知道。两个类通过 Tomcat 容器内部的 Session 对象关联,Valve 拿到的 session 就是 Manager 管理的那个实例。

这套机制带来的副作用也值得知道:每次请求结束都会有一次 Redis 写操作,如果应用在单个请求里频繁修改 Session,写入次数会被放大。对大多数业务系统来说,Redis 的毫秒级响应可以接受;但对超高并发、单请求内 Session 读写极其频繁的场景,这个方案不是最优解,那是分布式会话中间件该管的范围。

3. 把 Session 托管给 Redis:配置步骤与参数详解

3.1 放 jar 和找配置文件:两个容易搞混的位置

先解决位置问题。三个 jar 要放到 Tomcat 的 lib 目录,即 $CATALINA_HOME/lib,不是应用的 WEB-INF/lib。区别在于类加载器:Tomcat 容器级代码使用公共类加载器加载 lib 下的类,Web 应用使用 WebApp 类加载器。如果把 jar 丢进应用的 WEB-INF/lib,容器在实例化 Valve 和 Manager 时找不到类,启动直接报 ClassNotFoundException;反过来,应用自己的业务代码也访问不到 lib 里的类,但这里我们并不需要业务代码直连 Redis,所以放 lib 是对的。

配置文件方面,有两个 context.xml 容易混淆。全局配置在 $CATALINA_HOME/conf/context.xml,对所有部署在这台 Tomcat 上的应用生效;应用级配置在应用的 META-INF/context.xml,只对当前应用生效。这个下载资源的使用场景是让整个 Tomcat 实例的 Session 都走 Redis,我一般建议改全局的 conf/context.xml,这样以后部署多个应用到这个 Tomcat 时不用每个都单独配。如果你只想让某一个应用用 Redis 存 Session,那才放到应用的 META-INF/context.xml 里。

3.2 标准配置片段:Valve 与 Manager 两行一起写

在 context.xml 的 Context 节点内部加入下面这段配置,注意是放在 标签里面,不是替换标签:

<Context> <Valve className="org.apache.catalina.session.RedisSessionHandlerValve"/> <Manager className="org.apache.catalina.session.RedisSessionManager" host="127.0.0.1" port="6379" database="0" password="你的Redis密码" maxInactiveInterval="60" serializationStrategy="JAVA"/> </Context>

这段配置的逻辑是:先声明一个 Valve,类名指向 RedisSessionHandlerValve,它不负责具体的 Redis 连接参数,只负责在请求结束时触发保存动作。接着声明 Manager,类名指向 RedisSessionManager,Redis 的连接参数全部在这里配置。host 和 port 指定 Redis 服务地址,database 选库,password 按需填写,maxInactiveInterval 控制 Session 空闲失效时间,serializationStrategy 决定用什么方式序列化 Session 对象。配置完成后重启 Tomcat,启动日志无异常,就说明 Valve 和 Manager 都被正确加载了。

3.3 参数逐个拆解:database、失效时间与序列化策略

参数里最容易踩坑的是 password 和 serializationStrategy。Redis 没设密码时,password 留空即可,但注意不要把这项直接删掉,有些版本的代码会默认读空字符串。database 参数指定 Redis 的逻辑库,Redis 默认有 16 个库,index 从 0 到 15,如果和其他业务共用同一个 Redis 实例,建议单独分配一个库,避免 Session key 和业务 key 混在一起被误删。maxInactiveInterval 的单位是秒,60 表示 60 秒无请求就失效,这个值决定了 Redis 里 session 对应 key 的过期时间,也决定了用户空闲多久后需要重新登录。

各参数的边界和适用场景列在下面:

参数默认行为建议设置说明
host127.0.0.1Redis 实际地址多节点部署时,各节点要指向同一个 Redis
port6379按实际端口改了 Redis 默认端口才需要动
database0单独库位尽量不和业务数据共用库
password空Redis 密码无密码可留空,不要删行
maxInactiveInterval60按业务会话时长过短会频繁踢用户,过长会让 Redis 堆积失效 key
serializationStrategyJAVA默认即可换 KRYO 需要额外引入依赖,见下文

serializationStrategy 这里要重点说。默认的 JAVA 策略使用的是 Java 原生序列化,要求 Session 里存放的所有对象实现 java.io.Serializable 接口,否则写入时报 NotSerializableException。如果 Session 里存的对象是你自己写的实体类,需要给这些类加上 implements Serializable。还有一种情况是 Session 里塞了第三方库的对象,而那个类本身没实现 Serializable,这时候 JAVA 序列化策略会一直报错。常见做法是改用 KRYO 序列化,它基于字节码生成,不需要对象实现 Serializable,但要注意:切换到 KRYO 之前,必须确认 Tomcat lib 目录下有 kryo 相关的 jar 包。这个下载包默认带的是 JAVA 策略需要的三件套,KRYO 依赖通常不在里面,如果你没有额外补充依赖就贸然切换,启动时反而多一个类找不到的错。

4. 常见问题排查:序列化失败、版本冲突与失效陷阱

4.1 现象一:Session 写不进 Redis,报 NotSerializableException

异常栈会明确告诉你哪个类没有实现 Serializable。我第一次跑通配置后,往 Session 里塞了一个自定义的 UserInfo 对象,启动很正常,一登录就报这个错。原因是这个对象的类没有实现序列化接口,JAVA 序列化策略无法把它转成字节流写入 Redis。

解决分两步。第一步是给实体类加上 implements Serializable,并定义一个 serialVersionUID,这属于常规操作。第二步是审视 Session 里到底存了什么,如果存的是第三方库的对象且无法修改源码,就换 KRYO 序列化策略。但如果只是偶尔一两个类不满足条件,改代码实现接口比引一堆 KRYO 依赖更干净,毕竟 KRYO 的序列化字节流和 JAVA 原生格式不互通,切换后旧 Session 全部失效,用户会被强制重新登录一次。

4.2 现象二:启动时报 NoSuchMethodError 或 ClassCastException

这类错误一般出现在 Tomcat 启动的过程中,而不是应用运行阶段。典型报错是 java.lang.NoSuchMethodError: org.apache.commons.pool2.impl.GenericObjectPoolConfig.setJmxEnabled,原因几乎可以锁定为 commons-pool 版本不对。Tomcat 自身某些组件依赖 commons-pool 1.x,而包集合里的 Jedis 需要 pool2,两个版本同时存在时,类加载器优先加载了旧版本。

解决方法是把 Tomcat lib 目录下 commons-pool 相关的 jar 清一遍,只保留 commons-pool2,并确认版本号是 2.x。另一种情况是 ClassCastException,报错在 org.apache.catalina.session.RedisSessionManager 附近,多半是核心 jar 编译版本不对,比如把 Tomcat 7 的 Manager jar 误当 8 的用了。这时需要重新确认下载包标注的适用版本,Tomcat 8 的内部 Session 管理接口和 7 不完全一致,混用必炸。

4.3 现象三:Redis 连接正常,但用户刷新页面后 Session 频繁失效

表现是:应用能正常登录,Redis 里也能看到 session 对应的 key,但用户操作稍微慢一点,下次请求就被要求重新登录。检查 Redis 里的 key 存活时间会发现,TTL 比 maxInactiveInterval 配置的短很多,甚至有些 key 直接消失了。

这类问题有两个高发原因。第一个是 Redis 端设置了 maxmemory 且淘汰策略选择了 allkeys-lru 或 volatile-lru,内存压力大的时候,Session 对应的 key 会被 Redis 主动干掉,和业务方自己设置的过期时间无关。解决方法是把淘汰策略改成 noeviction,或者至少避免使用 allkeys-lru,这种策略对 session 这种长期 key 极不友好。第二个原因是 maxInactiveInterval 配置的语义是“空闲后的活跃窗口”,如果业务要求用户 30 分钟内不操作都保持登录,那这里要写 1800 而不是 30,单位是秒,很多人在这里按分钟填错过了。

4.4 现象四:集群里节点 A 登录后,切到节点 B 还是掉线

这个现象最迷惑人,因为单节点测试时一切正常。我遇到过实际案例:两台 Tomcat 各自都装了包集合,Redis 配置看起来也没问题,但 A 上创建的 Session 在 B 上就是读不到。最后发现两台机器的 context.xml 配置里,database 参数一个指向 0,一个指向 1,两个库互相隔离,数据根本没写到一个地方。

解决时先做个回归检查:所有节点的 context.xml 里的 host、port、database、password 必须完全一致,尤其注意不要用 localhost 这种说法,集群里一台机器访问另一台的 Redis,必须写成对端可达的 IP。然后到 Redis 里验证 key 是否存在,用 redis-cli 登录后执行 keyssession或 SCAN 匹配 session 前缀,确认 A 写入的 key 在 B 的 Redis 实例里同样能查到。如果 key 能查到但 Session 还是丢,那就是序列化策略不一致,A 节点用了 JAVA、B 节点用了 KRYO,两者互相读不懂。

5. 验证与进阶:双节点实测与 Session 过期策略联动

5.1 双节点模拟验证:同一个 Cookie 跨节点访问

配完配置别急着上线,先在本地起两个 Tomcat 实例做验证。方法是在同一台机器上解压两份 Tomcat,修改其中一份的 server.xml,把 shutdown 端口、HTTP 端口和 AJP 端口改掉,比如第二份分别改成 8006、8081、8010,避免端口冲突。两份 Tomcat 的 context.xml 配置完全一样,都指向同一个 Redis。

启动后写一个最简单的登录接口,登录成功后在 Session 里放一个用户标记。用 curl 先访问第一个节点的登录接口,拿到带 JSESSIONID 的 Cookie:

curl -i -c cookies.txt http://127.0.0.1:8080/app/login curl -i -b cookies.txt http://127.0.0.1:8080/app/user/info curl -i -b cookies.txt http://127.0.0.1:8081/app/user/info

第一条命令登录并把 Cookie 保存到文件,第二条命令带着同一个 Cookie 访问节点 A 的接口,第三条命令改用节点 B 的端口访问同样的接口。第三条命令如果返回了登录用户的信息,说明 Session 已经成功跨越节点共享;如果提示未登录,回到第 4.4 节检查 Redis 配置一致性。这个验证脚本我至今留着,每次改动配置都跑一遍,比打开浏览器手动点快得多。

5.2 一个收尾习惯:定时检查 Redis 里的 Session key 分布

Session 写进 Redis 之后不是一劳永逸的。Redis 里 session key 的过期依赖 maxInactiveInterval,但如果某个节点的请求特别少,Redis 主动淘汰机制又没打开,过期的 key 会一直占用内存。常见做法是给 Redis 配置一个定时清理策略,或者在运维侧写一个小时级的脚本,用 SCAN 扫出存活时间超过阈值的 session key 手动删除。

从那以后我每次改完 Session 相关配置,都强制走一遍这套验证:先清空 Redis 里遗留的 session key,再跑双节点 curl 脚本,最后看一眼 catalina.out 里有没有序列化或连接异常。这套固定动作帮我挡过至少三次上线事故,有一次就是因为新加的对象没实现 Serializable,在压测时才暴露。把这个习惯分享给要做集群会话共享的人,能少熬夜排查很久。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进