Tomcat 会话管理与会话持久化
概述
HTTP 是无状态协议,会话(Session)机制为它补充了"记忆"。Tomcat 作为 Servlet 容器,负责 Session 的创建、存储、传递与回收。单机部署时 Session 保存在 JVM 内存;一旦应用要横向扩展,Session 就成了第一个需要解决的分布式问题。本文从 Session 原理出发,覆盖 Manager 实现、集群复制与 Redis 会话三大主题。
一、Session 工作原理
1.1 会话的建立
浏览器 Tomcat
│ 1. 首次请求(无 Cookie) │
├──────────────────────────▶│
│ │ 2. 创建 Session 对象(id 随机生成)
│ │ 3. 响应头 Set-Cookie: JSESSIONID=xxx
│◀──────────────────────────┤
│ 4. 后续请求带上 Cookie │
├──────────────────────────▶│ 5. 根据 JSESSIONID 找到 Session
│ │ 6. 读写会话属性
│◀──────────────────────────┤核心对象:
| 对象 | 作用 |
|---|---|
HttpSession | 会话的规范接口,setAttribute / getAttribute 读写数据 |
StandardSession | Tomcat 默认实现 |
JSESSIONID | 会话标识,默认通过 Cookie 传递 |
SessionIdGenerator | 生成唯一会话 ID |
1.2 会话 ID 的两种传递方式
| 方式 | 机制 | 优缺点 |
|---|---|---|
| Cookie(默认) | Set-Cookie: JSESSIONID=xxx; Path=/ | 简洁、默认推荐 |
| URL 重写 | URL 追加 ;jsessionid=xxx | 浏览器禁用 Cookie 时兜底 |
URL 重写示例:http://localhost:8080/app/order/list;jsessionid=F01A2B...
Cookie 方式的注意点:
- Cookie 域与路径:
sessionCookiePath决定会话 Cookie 生效范围,多应用同域名需区分路径,避免串会话 - HttpOnly 与 Secure:
useHttpOnly="true"(默认)防 XSS 窃取;HTTPS 下开Secure - SameSite:Tomcat 9.0.31+ 支持
sameSiteCookies,跨站场景需配置
二、会话管理器 Manager
Tomcat 用 Manager 管理 Session 的创建、查找、过期与持久化,对应接口 org.apache.catalina.Manager。
2.1 默认实现与生命周期
<Context ... >
<Manager className="org.apache.catalina.session.StandardManager" />
</Context>| Manager | 说明 |
|---|---|
StandardManager | 默认,内存存储;正常关闭时会话序列化到 SESSIONS.ser |
PersistentManager | 持久化会话到文件/数据库 |
DeltaManager | 集群会话增量复制 |
BackupManager | 集群会话备份式复制(9.0.33+) |
Session 生命周期状态机:
创建(第一次访问) → 活跃(有请求访问) → 过期(超时未访问) → 销毁
└─ 显式 invalidate()2.2 超时与过期
| 配置位置 | 写法 | 作用 |
|---|---|---|
web.xml | <session-config><session-timeout>30</session-timeout></session-config> | 超时分钟数 |
| 代码 | session.setMaxInactiveInterval(1800) | 覆盖为秒数 |
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only>
<secure>true</secure>
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>过期检查由后台线程周期性扫描,时间粒度由 Manager 的 processExpiresFrequency 控制。
2.3 会话监听器
监听会话生命周期事件:
| 监听器 | 触发时机 |
|---|---|
HttpSessionListener | 创建 / 销毁 |
HttpSessionAttributeListener | 属性增删改 |
HttpSessionBindingListener | 对象绑定/解绑到会话 |
HttpSessionActivationListener | 会话持久化与恢复 |
监听器常被用来做登录在线统计、清理用户级缓存。
三、StandardManager:内存会话与优雅关闭
默认情况下 Session 只存在于 JVM 内存,Tomcat 正常关闭(shutdown.sh)时:
- 会话属性只有实现
Serializable的对象会被序列化到work/Catalina/<host>/<app>/SESSIONS.ser - 下次启动自动加载,用户"不掉线"
// StandardManager.stopInternal() 的关键动作
// 遍历所有会话 → 序列化写入 SESSIONS.ser → 清空内存两个坑:
- 非 Serializable 的会话属性在序列化时抛异常,可能导致整个会话未保存
- 强制 kill(
kill -9)不会触发序列化,会话直接丢失
四、PersistentManager:文件/数据库持久化
<Manager className="org.apache.catalina.session.PersistentManager"
maxIdleBackup="60" maxIdleSwap="120">
<Store className="org.apache.catalina.session.FileStore"
directory="../session" />
</Manager>| 属性 | 说明 |
|---|---|
maxIdleBackup | 空闲多久后备份到 Store(秒) |
maxIdleSwap | 空闲多久后从内存移除,仅留 Store(秒) |
minIdleSwap | 最少存活时间,防止频繁交换 |
Store 实现:
| Store | 存储 |
|---|---|
FileStore | 每个会话一个文件 |
JDBCStore | 数据库表 |
PersistentManager 解决的是单机重启丢会话问题,但注意:文件/数据库持久化在集群场景下仍有单点与一致性问题,横向扩展通常转向会话复制或外部会话存储。
五、集群会话复制
5.1 复制方案对比
| 方案 | 机制 | 优点 | 缺点 |
|---|---|---|---|
| DeltaManager | 会话属性变更即广播增量到集群 | 数据近实时同步 | 广播风暴、属性需 Serializable |
| BackupManager | 每个会话固定一个备份节点 | 降低广播量 | 备份节点故障需重新备份 |
| 外部存储(Redis) | 会话放 Redis,所有节点读写 | 无复制、易扩展 | 引入外部依赖与序列化开销 |
5.2 DeltaManager 配置
集群广播需要 SimpleTcpCluster 支持:
<Context ...>
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true" />
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
<Manager className="org.apache.catalina.ha.session.DeltaManager" />
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto" port="4000" />
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
</Sender>
</Channel>
<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
</Cluster>
</Context>JvmRouteBinderValve 的作用:配合 jvmRoute 属性,当粘性会话的节点故障时,把请求重新绑定到存活节点,实现"粘性 + 故障转移"。
Node A(jvmRoute=a)◀── 粘性请求
Node B(jvmRoute=b)
故障切换:请求到 B → JvmRouteBinderValve 检测到 session 的 jvmRoute 失效
→ 用 JSESSIONID 中的 route 后缀重新定位 → 绑定到 B5.3 复制方案的局限
- 会话属性必须
Serializable - 高并发写会话时广播开销大
- 集群规模大时复制风暴明显
- 需要多播/单播网络支持
因此现代微服务架构中,集群复制逐步让位于外部会话存储。
六、Redis Session Manager
把会话迁移到 Redis,是分布式场景的主流方案。社区常用 tomcat-redis-session-manager 或 Spring Session。
6.1 核心思路
浏览器 ──JSESSIONID──▶ Nginx ──▶ Tomcat A / Tomcat B(无本地 Session)
│ 读写
▼
Redis(会话统一存储)- Session 数据统一放 Redis,任意节点可处理任意请求
- 节点无状态化,横向扩展、滚动发布不再受会话约束
6.2 Spring Session 接入(推荐)
Spring Session 不是替换 Manager,而是通过 SessionRepositoryFilter 把 HttpSession 的实现替换为 Redis 支持:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
// RedisConnectionFactory 由 Spring Boot 自动装配
}Spring Session 的优势:
- 无需改动业务代码,API 仍是
HttpSession - 自动处理 Session 事件、Cookie 配置
- 与 Spring Security、WebSocket 集成良好
- 支持自定义序列化(JDK / JSON)
6.3 其他方案对比
| 方案 | 存储 | 侵入性 | 适用场景 |
|---|---|---|---|
| 集群复制(Delta/Backup) | 各节点内存 | 低(配置级) | 小集群、对一致性要求不极端 |
| Redis(Spring Session) | Redis | 低(依赖+配置) | 微服务、需要横向扩展 |
| Redis(tomcat-redis-session-manager) | Redis | 中(改 Manager) | 老项目快速迁移 |
| 数据库(JDBCStore) | 数据库 | 低 | 低频会话、对性能不敏感 |
七、会话安全的常见问题
| 风险 | 防护 |
|---|---|
| 会话固定攻击 | 登录成功后 session.invalidate() 并重建(Session Fixation) |
| Cookie 窃取(XSS) | HttpOnly + CSP + 输入输出过滤 |
| 中间人窃听 | HTTPS + Cookie Secure 属性 |
| 会话 ID 可预测 | 默认 SessionIdGenerator 随机性足够,勿自定义弱算法 |
| 多应用串会话 | sessionCookiePath 按应用路径隔离 |
// 防会话固定:登录成功后重建会话
HttpSession old = request.getSession();
old.invalidate();
HttpSession fresh = request.getSession(true);八、生产实践建议
- 会话里只存少量必要数据:Session 是 JVM 内存/Redis 成本,大量业务数据尽量走数据库或缓存 key。
- 统一超时策略:web.xml 与 Redis 的过期时间保持一致,避免"Session 还在、数据已过期"。
- 序列化友好:会话对象实现
Serializable,字段精简。 - 监控会话数:
/manager或 JMX 观察活跃会话曲线,异常增长多半是泄漏(未调 invalidate)。 - 无状态优先:能用 Token/JWT 替代 Session 的场景(尤其是 REST API),尽量不依赖服务端会话,让扩展更简单。
参考链接: