简介:这份包集合面向在 Tomcat 8 与 JDK 1.8 环境下开发、需要解决分布式会话共享问题的 Java 开发者,核心是把 Redis 接入 Tomcat 的 Session 管理流程,让多实例之间能够读取一致的会话数据,从而减轻单机内存压力并提升横向扩展能力。压缩包共 6 个文件,包含 4 个 jar 依赖、1 个 xml 配置示例和 1 个 txt 说明文档,整体约 445KB,便于直接补齐集成所需组件并对照配置项。已有 941 人学习或下载。包内将 Redis 会话存储所需的依赖与基础配置集中在一起,读者可据此了解 SessionManager 替换、连接参数设置和序列化选择等关键环节,减少逐个查找版本与依赖的时间,也方便在测试环境中快速验证会话迁移和共享效果。
1. tomcat8 双节点登录态乱跳:这套 Redis session 包到底顶不顶用
单机 Tomcat8 跑得好好的,一上两台就出现登录态乱跳,十有八九不是登录逻辑坏了,而是 session 还留在各自 JVM 里。tomcat-redis-session-manager 这类包集合解决的就是把 session 从 Tomcat 本地内存搬到 Redis,让 tomcat8 集群里任意节点都能认同一份会话。它适合已经在用 Redis、又不想把应用改成无状态 token 的团队,尤其是老系统只换会话存储、不动业务代码的场景。
这套包不是 Spring Session,不依赖框架改造,核心是替换 Tomcat 的 Manager 和 Valve。好处是接入面小,代价是 jar 必须由容器类加载器可见,序列化边界也得自己兜住。后面按包集合、配置、链路、参数和排查顺序拆,新手能照做,熟手重点看第 4、5 章的边界。
2. 包集合落进 Tomcat8:jar 放哪、context.xml 怎么写、启动看什么
拿到包集合后,先别急着把 jar 复制到应用的 WEB-INF/lib。Tomcat8 里 context.xml 声明的 Manager 是容器组件,实例化它的是容器类加载器,不是应用类加载器。放错位置,启动日志会直接抛 ClassNotFoundException,很多人会误以为是包不完整,实际上是类加载器没看见。
2.1 包集合的组成与类加载位置
包集合通常由主 jar、Jedis 客户端和 commons-pool2 连接池组成,有的分支还会带示例 context.xml 或 README。主 jar 提供 RedisSessionManager 和 RedisSessionHandlerValve,Jedis 负责跟 Redis 通信,commons-pool2 负责连接池。这三类东西必须让 Tomcat 容器加载到,所以我一般统一放 $CATALINA_HOME/lib,而不是 WEB-INF/lib。
业务应用里不要再带同名不同版本的 Jedis 和 commons-pool2。容器 lib 里有一份,应用里再带一份,类加载时容易出现 NoSuchMethodError 或 LinkageError,这种冲突比配置写错更难查。血泪经验是:先备份 lib,再复制,最后用 find 扫一遍 webapps 下有没有残留 jar。备份不是形式,回滚时没有备份就得重新找包。
# 进入 Tomcat 安装目录,先备份原 lib,避免回滚时抓瞎 cd /opt/tomcat8 tar -czf lib-backup-$(date +%F).tar.gz lib # 把包集合里的主 jar、Jedis、commons-pool2 复制到容器 lib cp /tmp/tomcat-redis-session-manager-package/*.jar "$CATALINA_HOME/lib/" # 确认 webapps 下没有残留的 Jedis 或连接池 jar,输出为空最好 find "$CATALINA_HOME/webapps" -name 'jedis-*.jar' -o -name 'commons-pool2-*.jar'上面三段命令分别做备份、复制、冲突检查。$CATALINA_HOME 要指向实际的 Tomcat8 目录,不要照抄 /opt/tomcat8。cp 用通配符是为了少写文件名,但复制完要确认目标目录里没有同名旧 jar 被覆盖后版本错位。find 的输出如果非空,说明应用自带同类依赖,需要在打包时排除,或者统一到容器 lib 的版本。
提示:主 jar 和依赖 jar 都放 $CATALINA_HOME/lib,不要只放主 jar。漏了 Jedis 或 commons-pool2,启动时同样会报类找不到。
还有一个容易忽略的点是文件权限。用 root 复制、用 tomcat 用户启动时,jar 权限至少要可读。复制完可以顺手看一眼 ls -l,确认 tomcat 用户能读。权限问题不会在编译期暴露,只会在容器启动加载 Manager 时翻车。
2.2 context.xml 的 Manager/Valve 配置
配置位置有三种:conf/context.xml 是全局,conf/Catalina/localhost/ .xml 是应用级,war 内 META-INF/context.xml 会被 Tomcat 复制到 conf/Catalina/localhost。全局配置会影响同一个 Tomcat 下所有应用,多应用共用一个容器时很容易串 session,所以我更倾向应用级配置。文件名要和 context path 对上,比如应用部署为 app.war,就写 conf/Catalina/localhost/app.xml。
<Context> <!-- Valve 负责在请求结束时把变更后的 session 写回 Redis --> <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" /> <!-- Manager 负责 session 的创建、查找、过期和持久化 --> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" host="127.0.0.1" port="6379" database="0" password="" timeout="2000" maxInactiveInterval="1800" /> </Context>className 以包内实际类名为准,不同分支可能改过包名,配置前先看 jar 里的类。Valve 放在 Manager 前面,两个都要配:只配 Manager 常见表现是 session 能读但写回不及时,或者过期时间不刷新。host、port、database、password 是 Redis 连接四件套,timeout 是连接和读写超时,maxInactiveInterval 是会话空闲过期秒数。
| 参数 | 示例值 | 作用 | 边界 |
|---|---|---|---|
| host | 127.0.0.1 | Redis 地址 | 排查时别混用 localhost 和 IPv6 解析 |
| port | 6379 | Redis 端口 | 和实际实例一致 |
| database | 0 | 选库 | session 单独库,避免和缓存混用 |
| password | 空 | 认证 | 有密码必须填,空字符串不等于没配置 |
| timeout | 2000 | 连接/读写超时 | 太小频繁超时,太大请求堆积 |
| maxInactiveInterval | 1800 | 会话过期秒数 | 与业务登录超时对齐 |
属性名不同分支可能有差异,比如连接池参数叫 maxTotal 还是 maxTotalConnections,以类里的 setter 为准。配置改完要重启 Tomcat,不要只热部署应用。war 内 META-INF/context.xml 被复制到 conf/Catalina/localhost 后,后续改 war 里的文件不一定生效,得看实际部署目录下的那份。
注意:同一个 context 不要同时保留旧的 Manager 配置。两个 Manager 同时存在时,行为不可预测,日志也不一定报错。
2.3 启动与最小验证
启动前先确认 Redis 可达,别让 Tomcat 背锅。用 redis-cli ping 返回 PONG,再启动 Tomcat8,tail catalina.out 看 RedisSessionManager 初始化有没有异常。访问一次应用后,用 redis-cli --scan 找 session key。生产环境不要用 keys *,扫描大库会阻塞。
# 确认 Redis 可达 redis-cli -h 127.0.0.1 -p 6379 ping # 启动 Tomcat8,观察容器日志中的 Manager 初始化记录 "$CATALINA_HOME/bin/startup.sh" tail -n 100 "$CATALINA_HOME/logs/catalina.out" # 访问一次应用,观察是否出现 session key,前缀以包内实现为准 redis-cli -h 127.0.0.1 -p 6379 --scan --pattern '*session*' | headping 返回 PONG 只说明网络和认证通,不代表 database 选对了。--scan 的 pattern 要根据实际 key 前缀调整,不同实现可能带 tomcat:session: 之类的固定前缀。如果扫描不到,先确认应用有没有创建 session:JSP 默认会创建,Spring 应用可能延迟到第一次写 session 属性。
最小验证还要看 Cookie 和 TTL。用 curl 拿一次 JSESSIONID,再带 Cookie 访问,观察 key 的 TTL 有没有续期。
# 第一次访问,保存 Cookie curl -i -c /tmp/session.cookie http://127.0.0.1:8080/app/ # 带 Cookie 再访问,确认 JSESSIONID 没变 curl -i -b /tmp/session.cookie http://127.0.0.1:8080/app/如果第二次返回了新的 JSESSIONID,说明 session 没被 Manager 接管,或者应用主动 invalidate 了。TTL 用 redis-cli ttl 看,返回 -1 表示没有过期时间,返回 -2 表示 key 不存在。TTL 应该接近 maxInactiveInterval,访问后变小再变大才说明续期生效。
3. RedisSessionManager 的请求链路:一次 session 读写到底经过哪些对象
配置能启动只是第一关,真正要确认的是请求链路有没有走到 Redis。Tomcat8 的 session 管理不是单一类完成的,Valve 和 Manager 各管一段:Valve 管请求边界,Manager 管 session 的创建、查找、写回和过期。链路不清楚,排查时只能看日志猜。
3.1 请求拦截与 session 查找
请求进来先过 RedisSessionHandlerValve,Valve 在调用业务前向 Manager 要 session。Manager 拿 JSESSIONID 去 Redis 查 key,命中就反序列化,未命中就创建新 session 并生成新 id。业务执行完,Valve 在请求结束阶段判断 session 有没有变更,有就写回 Redis,同时刷新 TTL。只配 Manager 不配 Valve,常见表现是 session 能读但写回不及时,或者过期时间不刷新。
Tomcat8 的 Manager 接口里 createSession、findSession、add、remove 是核心动作,RedisSessionManager 的实现就是把这些动作映射到 Redis 的 get、set、expire、del。排查时先看请求有没有带 JSESSIONID,再看 Manager 有没有查 Redis,最后才怀疑序列化。日志级别调低能看到 key 和异常,但生产不要长期开 debug。
# 先确认请求链路是否进入 RedisSessionManager,关键字以日志实际输出为准 grep -n 'RedisSessionManager\|RedisSessionHandlerValve' "$CATALINA_HOME/logs/catalina.out" | tail -n 50grep 的关键字要看包内日志实际打印什么,有的分支只打印初始化,不打印每次读写。tail 看最近 50 行,避免翻整份日志。如果日志里只有初始化没有后续,说明请求可能没进 Valve,或者应用根本没创建 session。
3.2 序列化与过期时间
Redis 里存的是序列化后的 session 属性,常见实现用 Java 序列化。放进 session 的对象必须实现 Serializable,属性类里的字段也要能序列化。有人塞数据库连接、线程池、InputStream,平时不报错,一重启或换节点就 NotSerializableException,登录态直接丢。过期时间是双轨:Tomcat 的 maxInactiveInterval 管本地判断,Redis 的 TTL 管键过期;每次访问是否续期取决于实现,验证时务必看 TTL 有没有变。
public class LoginUser implements Serializable { private static final long serialVersionUID = 1L; // 版本变化时反序列化不炸 private String userId; private String tenantId; // 不要塞 Connection、ThreadLocal、InputStream 这类不可序列化对象 }serialVersionUID 固定下来,发布时新增字段给默认值,删除字段先灰度。属性类改包名、改字段类型,都可能让旧 session 反序列化失败。包内如果提供 sessionSerializerClass 或 PersistFactory,可以换兼容序列化实现;没有就管住属性类。序列化是黑匣子,出错日志不一定直观,最好在测试环境模拟一次重启和类变更。
3.3 连接池与 Redis 选库
Manager 每次读写 session 都要拿 Jedis 连接,连接池参数直接决定高并发下会不会排队。常见参数有 maxTotal、maxIdle、minIdle、maxWaitMillis,不同分支属性名可能不同,以类里的 setter 为准。maxTotal 不是越大越好,Redis 单线程处理命令,连接太多反而增加上下文切换;maxWaitMillis 不要设 -1 无限等,否则 Redis 抖动会把 Tomcat 线程拖死。
session 建议单独 database,不要和业务缓存混用。混用之后一次 flushdb 就可能把登录态清空,恢复起来很麻烦。连接池打满的典型现象是请求变慢、日志里出现 JedisConnectionException 或超时,Redis 的 connected_clients 接近 maxTotal。
# 观察 Redis 客户端连接数和命中情况,判断连接池是否打满 redis-cli -h 127.0.0.1 -p 6379 -n 0 info clients redis-cli -h 127.0.0.1 -p 6379 -n 0 info stats | grep -E 'keyspace_hits|keyspace_misses'connected_clients 持续接近 maxTotal,要查连接泄漏或并发估算偏低。keyspace_hits 低、misses 高,可能是 key 前缀不对、session 过期太快,或者请求根本没带 JSESSIONID。生产环境不要频繁执行 info,观察频率按监控系统来。
4. 参数不是拍脑袋:maxInactiveInterval、连接池与序列化边界怎么定
参数配错不一定启动报错,更多是运行期表现:用户填一半表单掉登录、Redis 内存慢慢涨、高并发时 Tomcat 线程被拖住。下面三个参数组是实际落地时最常调的,先定边界再压测,不要照抄示例值。
4.1 maxInactiveInterval 与 Redis TTL
这个参数单位是秒,Tomcat 默认 1800 秒。设太短,用户填表单填一半掉登录;设太长,Redis 里堆积大量僵尸 session,内存和序列化开销都上来。我一般先跟业务确认登录态允许空闲多久,再把这个值对齐,比如 1800 或 3600。Redis TTL 最好和它一致或略大,给写回留一点余量。验证时不要只看第一次 TTL,访问一次再 ttl 一次,确认续期。
| 场景 | maxInactiveInterval | 说明 |
|---|---|---|
| 后台管理系统 | 1800 | 空闲 30 分钟重新登录 |
| 面向用户的长表单 | 3600 | 避免填写过程掉线 |
| 内部低频系统 | 7200 | 降低登录频率但增加堆积 |
内存估算可以用 session 平均序列化大小乘以峰值在线用户数,再留 1.5 到 2 倍冗余。不要拍脑袋给 Redis 很小内存,session 膨胀比缓存膨胀更隐蔽。TTL 返回 -1 表示没有过期时间,必须查配置;返回 -2 表示 key 已不存在,可能过期太快或根本没写进去。
4.2 Jedis 连接池参数
maxTotal 可以粗算:单节点 QPS 乘以一次 session 操作平均占用连接的时间秒数,再乘安全系数 2。比如 200 QPS、每次 2ms,2000.0022≈0.8,理论值很小,但突发流量和慢查询要留余量,实际会给到几十。maxIdle 接近 maxTotal 的 50% 到 80%,minIdle 留少量热连接。maxWaitMillis 设 1000 到 3000ms,超时就抛异常,不要无限等;timeout 设 2000ms 左右,Redis 慢时快速失败比拖死 Tomcat 好。
<!-- 连接池参数名以包内实现为准,这里只示意常见写法 --> <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager" host="127.0.0.1" port="6379" database="0" timeout="2000" maxTotal="64" maxIdle="32" minIdle="8" maxWaitMillis="2000" />maxTotal 64 只是示例,要按实际并发调。属性名不支持时启动会报错或忽略,配置后看日志和 Redis connected_clients 验证。连接泄漏的典型现象是 connected_clients 持续上涨,重启 Tomcat 才降。Manager 内部一般会归还连接,但自定义 Valve 或序列化实现里手动 getResource 没 close 就会漏。
4.3 序列化兼容与 session 属性治理
session 里只放登录标识、权限摘要、少量上下文。不要把查询结果集、大列表、文件流塞进去,序列化后体积大,跨节点传输和 Redis 内存都吃不消。属性类加 serialVersionUID,发布时不要随意改包名和字段类型;新增字段给默认值,删除字段先灰度。新旧版本
本文还有配套的精品资源,点击获取