第6章 会话状态

比较无状态价值以及客户端、服务器、数据库三种会话状态位置的可靠性和成本。

学习目标

  • 能解释无状态请求、会话标识和会话数据之间的责任边界
  • 能编写 TypeScript 会话存储端口,覆盖过期、并发更新和恢复语义
  • 能根据安全性、状态量、扩缩容和持久性要求,选择客户端、服务器或数据库会话状态

为什么第6章会话状态值得单独学习

第6章会话状态的核心不是背诵某个 Web 框架的 session API,而是决定一次跨请求的业务过程由谁保存、谁验证以及谁在故障后恢复。订单向导从填写地址到确认付款可能跨越多个请求;如果状态权威、过期时刻和恢复责任不清晰,系统即使在单节点上正常工作,也无法解释扩容、重放和节点故障后的行为。

不是完全没有状态,而是把状态的权威位置和生命周期从请求处理器中显式分离。本页以 2024 年中文版公开目录限定第6章的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写;目录只决定要讲什么,案例、实验、判断题和答案均为本课程原创。

先建立直觉:问题、机制与代价

需要一个可验证的权威副本。把数据放进客户端可以减少服务器存储,却增加签名、加密、大小和重放防护责任;放进服务器内存读写快,却要处理节点亲和、复制和故障丢失;放进数据库容易由任意节点恢复,却要承担读写延迟、清理和容量成本。

对第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 或数据库,但端口不应把存储细节泄漏给订单流程;冲突应让调用者重新读取,过期应清理或重新开始,未经授权的用户不能通过猜测会话标识读取状态。

三种存储位置解剖

会话状态的核心决策是权威副本存在哪里。客户端、服务器和数据库并不是从低级到高级的固定升级路径,而是安全性、状态量、可恢复性和运维成本之间的取舍。下面的图把三种位置的优势、代价与故障恢复放在同一张证据图中。

会话状态:三种存储位置客户端会话Client Session State存储位置浏览器 / 移动设备实现方式Cookie · URL · 隐藏字段优势服务器无状态,易扩展代价数据量受限,安全性差故障恢复清除缓存 = 状态丢失服务器会话Server Session State存储位置应用服务器内存实现方式Session ID → 内存对象优势读写快,数据量不限代价服务器故障 = 状态丢失故障恢复需粘性会话或复制数据库会话Database Session State存储位置数据库表实现方式Session ID → 表行优势持久化,任意节点可恢复代价每次读写都走数据库故障恢复性能瓶颈,需定期清理简单 / 无状态持久 / 可恢复客户端服务器数据库选择轴:状态量 × 持久性需求 × 扩展性要求
客户端会话让服务器无状态但数据量受限;服务器会话读写快但故障会丢失; 数据库会话最持久但性能最差。选择取决于状态量、持久性需求和扩展性要求。

选择时至少记录状态大小、敏感字段、节点数量、允许丢失的时间和过期清理策略;“请求返回 200”不能证明会话状态已经持久化或能在另一节点恢复。

状态位置决策图

先问是否能接受状态丢失、是否需要多节点共享、是否允许客户端持有数据,再决定存储位置。客户端状态要有签名与大小上限,服务器状态要有复制或粘性策略,数据库状态要有索引、过期清理和容量预算。

会话状态位置:安全性 × 状态量 × 恢复目标小且非敏感客户端签名状态大小上限 + 过期不存付款权威数据低延迟读写共享服务器会话复制或粘性策略测试节点故障跨节点恢复数据库会话索引 + 过期清理预算读写成本拒绝条件:无签名、无版本、无后端过期检查存储位置由状态证据决定,而不是由框架默认值决定
客户端、服务器和数据库会话各有边界;先明确安全性、状态量与恢复目标,再选择权威位置。

选择与拒绝矩阵

评审问题选择会话状态方案的证据应拒绝当前方案的信号
责任权威副本、验证者和恢复者清晰页面字段、应用内存和数据库都能修改同一状态
安全会话标识可验证,敏感字段不被客户端任意篡改只编码不签名,或把用户标识完全信任于请求参数
伸缩多节点读取、复制或粘性策略经过故障测试扩容后请求随机落点导致会话丢失
生命周期过期、撤销、清理和重新开始都有观测过期只在页面显示,后端仍接受旧会话

常见误区

可验证练习

练习

本组练习覆盖 6.1 无状态的价值、6.2 会话状态和 6.3 存储会话状态的方法,并要求把无状态、会话状态、存储位置、伸缩和失效映射到订单向导会话。

问题 1:判断客户端状态。 订单向导把步骤、用户编号和折扣金额全部放进客户端字段,只做 Base64 编码。可以接受吗?

问题 2:选择存储位置。 服务需要多节点扩容,购物车状态较大且允许跨请求恢复,应该比较什么方案?

问题 3:处理并发和过期。 用户打开两个标签页,旧标签页在会话过期前提交,服务端应如何避免旧进度覆盖新进度?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

无状态

请求处理不依赖某个应用进程内保存的用户上下文,跨请求数据由客户端凭证或外部存储重新提供的服务特征。

会话状态

跨越多个请求、代表同一用户或业务流程进度的数据集合,例如订单向导步骤、购物车版本和过期时间。

会话标识

用于定位会话数据的稳定凭证;它本身不等于授权,必须配合用户校验、签名或服务端查找。

失效策略

规定会话何时过期、如何撤销、如何清理以及过期请求如何恢复的生命周期规则。

本章小结

掌握第6章会话状态的标志不是记住客户端、服务器或数据库的排列顺序,而是能在订单向导会话中解释“请求 → 会话标识 → 状态读取 → 条件更新 → 过期恢复”的责任链。学习者应利用无状态、会话状态、存储位置、伸缩和失效作出可证伪的选择,并在没有签名、版本和后端过期检查时明确拒绝当前实现。

前后导航

来源与改写范围

资料与写作方式声明

本章以Martin Fowler《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…