17.3 数据库会话状态
把会话数据作为已提交记录存入数据库,以持久性和共享访问换取数据库负担。
学习目标
- 能解释数据库会话状态如何把会话记录交给共享数据库,并说清持久性、扩展性与数据库负担之间的取舍
- 能编写带版本条件、过期检查和事务边界的 TypeScript 读写草图,拒绝旧版本覆盖新状态
- 能根据会话大小、节点数量、恢复目标和热点指标,判断订单向导何时采用或拒绝数据库会话状态
为什么 17.3 数据库会话状态值得单独学习
订单向导会跨越多个请求,可能经历节点重启、发布切换和重复提交。把会话放在进程内很快,却无法让下一台节点或重启后的服务看到同一份状态;把全部状态放进客户端,又会把隐私、大小和撤销责任推给不可信输入。数据库会话状态把记录交给共享数据库,换来持久性和节点独立性,同时新增 SQL、连接池、清理任务与热点治理。
↡把跨请求的会话记录持久化到共享数据库,由应用节点按会话键读写的会话状态方案。的关键不在于“用了 ORM 还是 SQL”,而在于确定谁拥有状态、何时提交、如何过期,以及数据库故障时请求应暂停、回退还是拒绝。数据库保存的是会话上下文,不是让会话记录替代订单、库存或权限等业务权威。
本章只覆盖 2024 年中文版公开目录中的 17.3 数据库会话状态,依据作者图书页和公开模式目录独立重写。案例、实验、代码、练习和答案均为本课程原创,不复现原书正文、插图或代码。
先建立直觉:共享存储换来恢复能力
一次订单向导请求可以拆成“读键、校验版本、修改草稿、事务提交、设置过期”五个观察点。↡用于定位一份会话记录、但不应单独承担身份授权的稳定标识。只负责找到记录;服务器仍要验证当前用户、租户和流程权限。若把会话键当成授权凭证,数据库再可靠也无法修复边界错误。
数据库记录通常包含小型上下文、↡用来阻止旧请求覆盖新写入的单调递增或等价版本标记。与到期时间。一次更新必须把读取、权限判断和写入的责任说清楚;发现版本冲突时,应返回可重试或重新读取的结果,而不是静默覆盖另一台节点刚提交的状态。
持久化也会引入新的观测项:连接池等待、热点会话、慢查询、清理滞后和数据库不可用率。↡同一条记录被频繁读写,导致锁等待、缓存失效或单分片压力集中的现象。不是“数据库方案必然错误”,却是提醒团队缩小会话、调整索引、分片或更换存储边界的证据。
目录单元到教学证据
17.3 数据库会话状态
本章精确对应 manifest 单元 poeaa24-pattern-40-database-session-state。学习者要能用同一条订单向导责任链回答:哪个节点可以读取、哪个事务可以写入、旧版本如何拒绝、过期记录如何清理、数据库不可用时业务是否允许继续。
完成本单元要交出三样证据:一张标出客户端、应用节点和数据库责任的图;一段带版本与过期检查的读写代码;一个故障样本,证明冲突、热点或数据库不可用时系统会在明确边界停止,而不是把错误状态继续传播。
专属案例:跨节点的订单向导
假设访客先保存配送地址,再在另一台应用节点选择付款方式。会话记录保存 cartDraftId、当前步骤、租户键和短期草稿;订单金额、库存、优惠资格与付款结果仍从权威服务读取。两台节点应该看到同一份已提交记录,但不能因为共享数据库就绕过当前用户的授权。
评审卡记录如下:单元键为 poeaa24-pattern-40-database-session-state;模式族为 session;采用条件是“需要跨节点共享、可恢复且记录大小可控的会话”;观测项包括读写延迟、版本冲突率、连接池等待、热点键和过期清理滞后;拒绝条件是“会话成为大对象、数据库无法承受峰值,或故障时没有安全回退”。
专属可视化实验:一条记录怎样跨节点生效
先预测:节点 A 读到版本 7 后,节点 B 先提交版本 8,节点 A 再写入时应该发生什么?点击主图观察会话键如何定位记录、版本条件如何阻止旧写入,以及过期清理为什么是容量责任的一部分。再把数据库改成不可用,判断请求应返回可重试错误还是继续执行付款。
三个阶段快照
下面的 Stepper 把主图拆成三个确定性阶段。先写下每一步的责任和失败结果,再对照图中的数据流,避免把“能读到记录”误认为“已经获得业务授权”。
1. 会话键定位权威记录
节点收到 sid 后只用它定位记录,再校验租户、用户和流程权限。会话键不是价格、角色或付款状态的来源。
代码实践:带版本和过期边界的读写
下面的 TypeScript 草图把数据库适配器收窄成可测试接口。真实项目应使用参数化查询、事务、连接池和成熟的重试策略;这里的重点是权限、版本和过期检查的顺序。
type SessionRow = {
id: string;
ownerId: string;
tenantId: string;
step: 1 | 2 | 3;
cartDraftId: string;
version: number;
expiresAt: number;
};
type SessionStore = {
get(id: string): Promise<SessionRow | null>;
updateIfVersion(
id: string,
expectedVersion: number,
patch: Pick<SessionRow, "step" | "cartDraftId" | "expiresAt">,
): Promise<"updated" | "conflict">>;
};
async function advanceWizard(
store: SessionStore,
input: { sessionId: string; ownerId: string; tenantId: string; now: number },
): Promise<"ok" | "not-found" | "forbidden" | "expired" | "conflict"> {
const row = await store.get(input.sessionId);
if (!row) return "not-found";
if (row.ownerId !== input.ownerId || row.tenantId !== input.tenantId) {
return "forbidden";
}
if (row.expiresAt <= input.now) return "expired";
const result = await store.updateIfVersion(
row.id,
row.version,
{ step: 2, cartDraftId: row.cartDraftId, expiresAt: input.now + 15 * 60_000 },
);
return result === "updated" ? "ok" : "conflict";
}四个代码边界可直接测试:不存在的键不能创建隐含授权;用户和租户必须匹配;过期记录不能进入业务动作;版本冲突不能被重试逻辑伪装成成功。若把 updateIfVersion 改成无条件 UPDATE sessions SET ...,就应把它标成会覆盖并发写入的拒绝样本。
选择与拒绝矩阵
| 评审问题 | 采用数据库会话状态的证据 | 应拒绝或改用其他方案的信号 |
|---|---|---|
| 共享 | 任意节点都能读取同一份已提交记录 | 只能依赖粘性路由或进程内缓存 |
| 完整性 | 权限、版本与事务边界可测试 | 会话键直接充当授权凭证 |
| 容量 | 记录小、索引明确、清理可观测 | 大对象把数据库变成文件存储 |
| 故障 | 数据库不可用时有安全回退和告警 | 连接池耗尽仍继续付款或发货 |
| 生命周期 | expiresAt、清理任务和恢复路径明确 | 过期数据永久增长且没有责任人 |
常见误区
本章小结
- 数据库会话状态以共享、持久化和节点独立性换取 SQL、连接池、热点与清理责任。
- 会话键只定位记录;权限、租户、版本和过期检查必须在业务动作前完成。
- 乐观版本让并发冲突可见,事务和参数化查询让状态变化可审计、可测试。
- 当会话很大、数据库无法承受峰值或故障没有安全回退时,应拒绝当前方案并重新划分状态边界。
可验证练习
练习
问题 1:找出错误边界。 advanceWizard 只接收 sessionId,查到记录后就把 step 改成 3。请指出至少两个缺失检查,并说明它们防止什么问题。
问题 2:解释冲突结果。 节点 A 和 B 都读到版本 7,B 先提交版本 8,A 的更新返回 conflict。为什么不能直接重试同一个 patch?
问题 3:选择替代方案。 会话包含 2MB 的草稿、强隐私字段,并要求数据库故障时付款仍继续。请给出三个拒绝信号和一个拆分方向。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 数据库会话状态
把跨请求会话记录保存到共享数据库,由应用节点按键读写并承担事务、清理与故障责任。
- 会话键
定位一条会话记录的标识;它不等于身份、租户或业务授权。
- 乐观版本
随记录递增的版本标记,用条件更新发现旧请求并拒绝覆盖。
- 数据库热点
同一记录或分片被频繁访问,造成锁等待、缓存失效或集中压力的现象。
- 过期清理
依据到期时间删除或归档不再有效的会话记录,并以指标监控清理滞后。
前后导航
参考资料
- Martin Fowler 作者图书页:核对全书主题、章节范围和模式参考结构。
- Martin Fowler 模式目录:核对数据库会话状态的公开模式定位与模式族。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。