第6章 会话状态
比较无状态价值以及客户端、服务器、数据库三种会话状态位置的可靠性和成本。
学习目标
- 能解释无状态请求、会话标识和会话数据之间的责任边界
- 能编写 TypeScript 会话存储端口,覆盖过期、并发更新和恢复语义
- 能根据安全性、状态量、扩缩容和持久性要求,选择客户端、服务器或数据库会话状态
为什么第6章会话状态值得单独学习
第6章会话状态的核心不是背诵某个 Web 框架的 session API,而是决定一次跨请求的业务过程由谁保存、谁验证以及谁在故障后恢复。订单向导从填写地址到确认付款可能跨越多个请求;如果状态权威、过期时刻和恢复责任不清晰,系统即使在单节点上正常工作,也无法解释扩容、重放和节点故障后的行为。
↡请求处理不依赖某个应用进程内保存的用户上下文,跨请求数据必须由客户端凭证或外部存储重新提供的服务特征。不是完全没有状态,而是把状态的权威位置和生命周期从请求处理器中显式分离。本页以 2024 年中文版公开目录限定第6章的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写;目录只决定要讲什么,案例、实验、判断题和答案均为本课程原创。
先建立直觉:问题、机制与代价
↡跨越多个 HTTP 请求、代表同一用户或业务流程进度的数据集合,例如订单向导的步骤、购物车版本和过期时间。需要一个可验证的权威副本。把数据放进客户端可以减少服务器存储,却增加签名、加密、大小和重放防护责任;放进服务器内存读写快,却要处理节点亲和、复制和故障丢失;放进数据库容易由任意节点恢复,却要承担读写延迟、清理和容量成本。
对第6章会话状态,优先比较的替代路线是:无状态请求加客户端凭证、服务器会话加共享存储、数据库会话加明确过期策略。选择之前先记录状态量、敏感程度、最长恢复时间和节点数量;如果实验只能显示一次请求成功,不能说明失效或故障后的行为,就不能据此选择存储位置。
目录单元到教学证据
第6章 会话状态
第6章会话状态要求把“状态放在哪里”变成可观察的架构决定:先标出权威副本、读写者、生命周期和恢复者,再用过期、节点切换与重放测试验证决定。只有这些证据能在订单向导会话中复现,会话状态才不是部署配置上的偶然结果。
6.1 无状态的价值
6.1 无状态的价值在于让任意节点都能处理请求,减少粘性会话和进程内记忆带来的扩容限制。它并不消除会话数据,而是要求请求携带可验证的会话标识,或从共享存储读取权威状态;因此必须同时检查凭证大小、篡改防护和重放风险。
6.2 会话状态
6.2 会话状态关注跨请求数据的权威性与生命周期。订单向导的步骤、购物车版本和付款意图不能只依赖页面传回的字段;服务端需要确认会话属于正确用户、没有过期,并以明确的版本或条件更新防止旧请求覆盖新进度。
6.3 存储会话状态的方法
6.3 存储会话状态的方法比较客户端、服务器和数据库三种位置的成本。客户端适合小而可验证的非敏感状态,服务器内存适合低延迟但要承担复制或丢失,数据库适合跨节点恢复但需要过期清理、索引和容量监控;选择必须由状态证据推动。
专属代码案例:可恢复的订单向导会话
把订单向导会话切成“请求 → 会话标识 → 状态读取 → 条件更新 → 过期恢复”五个观察点。第6章会话状态的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及无状态、会话状态、存储位置、伸缩和失效中哪个指标最先提示当前方案不再适用。
设计记录采用五个可换行字段:单元键为 poeaa24-chapter-06-session-state;模式族为 session;裁决是“状态小且非敏感时保持无状态,跨节点恢复或状态较大时使用共享存储”;观测项包括无状态、会话状态、存储位置、伸缩、失效;拒绝条件是“把未经签名的客户端字段当作付款流程的权威状态”。
先预测:如果两个请求同时把订单向导从 shipping 推进到不同步骤,最后一次写入是否总是正确?先写下版本检查、过期判断和节点故障后的恢复结果,再阅读实现;验证时要能区分“请求成功”与“会话状态被安全更新”。
type CheckoutSession = {
sessionId: string;
userId: string;
step: "shipping" | "payment" | "review";
cartVersion: number;
version: number;
expiresAt: number;
};
interface SessionStore {
read(sessionId: string): CheckoutSession | undefined;
compareAndWrite(
next: CheckoutSession,
expectedVersion: number,
): "updated" | "conflict";
remove(sessionId: string): void;
}
function advanceCheckout(
store: SessionStore,
sessionId: string,
userId: string,
nextStep: CheckoutSession["step"],
now: number,
): "updated" | "conflict" | "expired" | "not-found" | "forbidden" {
const current = store.read(sessionId);
if (!current) return "not-found";
if (current.userId !== userId) return "forbidden";
if (current.expiresAt <= now) {
store.remove(sessionId);
return "expired";
}
const result = store.compareAndWrite(
{ ...current, step: nextStep, version: current.version + 1 },
current.version,
);
return result;
}这段代码把会话所有权、过期和并发更新变成显式结果。真实适配器可以把 SessionStore 接到 Redis 或数据库,但端口不应把存储细节泄漏给订单流程;冲突应让调用者重新读取,过期应清理或重新开始,未经授权的用户不能通过猜测会话标识读取状态。
三种存储位置解剖
会话状态的核心决策是权威副本存在哪里。客户端、服务器和数据库并不是从低级到高级的固定升级路径,而是安全性、状态量、可恢复性和运维成本之间的取舍。下面的图把三种位置的优势、代价与故障恢复放在同一张证据图中。
选择时至少记录状态大小、敏感字段、节点数量、允许丢失的时间和过期清理策略;“请求返回 200”不能证明会话状态已经持久化或能在另一节点恢复。
状态位置决策图
先问是否能接受状态丢失、是否需要多节点共享、是否允许客户端持有数据,再决定存储位置。客户端状态要有签名与大小上限,服务器状态要有复制或粘性策略,数据库状态要有索引、过期清理和容量预算。
选择与拒绝矩阵
| 评审问题 | 选择会话状态方案的证据 | 应拒绝当前方案的信号 |
|---|---|---|
| 责任 | 权威副本、验证者和恢复者清晰 | 页面字段、应用内存和数据库都能修改同一状态 |
| 安全 | 会话标识可验证,敏感字段不被客户端任意篡改 | 只编码不签名,或把用户标识完全信任于请求参数 |
| 伸缩 | 多节点读取、复制或粘性策略经过故障测试 | 扩容后请求随机落点导致会话丢失 |
| 生命周期 | 过期、撤销、清理和重新开始都有观测 | 过期只在页面显示,后端仍接受旧会话 |
常见误区
可验证练习
练习
本组练习覆盖 6.1 无状态的价值、6.2 会话状态和 6.3 存储会话状态的方法,并要求把无状态、会话状态、存储位置、伸缩和失效映射到订单向导会话。
问题 1:判断客户端状态。 订单向导把步骤、用户编号和折扣金额全部放进客户端字段,只做 Base64 编码。可以接受吗?
问题 2:选择存储位置。 服务需要多节点扩容,购物车状态较大且允许跨请求恢复,应该比较什么方案?
问题 3:处理并发和过期。 用户打开两个标签页,旧标签页在会话过期前提交,服务端应如何避免旧进度覆盖新进度?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 无状态
请求处理不依赖某个应用进程内保存的用户上下文,跨请求数据由客户端凭证或外部存储重新提供的服务特征。
- 会话状态
跨越多个请求、代表同一用户或业务流程进度的数据集合,例如订单向导步骤、购物车版本和过期时间。
- 会话标识
用于定位会话数据的稳定凭证;它本身不等于授权,必须配合用户校验、签名或服务端查找。
- 失效策略
规定会话何时过期、如何撤销、如何清理以及过期请求如何恢复的生命周期规则。
本章小结
掌握第6章会话状态的标志不是记住客户端、服务器或数据库的排列顺序,而是能在订单向导会话中解释“请求 → 会话标识 → 状态读取 → 条件更新 → 过期恢复”的责任链。学习者应利用无状态、会话状态、存储位置、伸缩和失效作出可证伪的选择,并在没有签名、版本和后端过期检查时明确拒绝当前实现。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对模式名称、所属模式族和作者公开摘要。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。