备忘录模式如何控制内存成本?
简化版
备忘录模式可能因为保存大量快照造成内存压力。常见优化包括限制历史步数、保存增量快照、压缩快照、使用不可变结构共享数据、定期清理过期快照、把快照持久化到磁盘或数据库。
详细版
内存成本来源:
- 每次操作都保存完整状态。
- 状态对象很大。
- 快照数量无限增长。
- 快照中包含重复数据。
- 没有及时清理 redo 栈或过期历史。
优化方式:
- 限制撤销栈大小。
- 合并连续小操作。
- 保存差异而不是完整状态。
- 使用结构共享减少重复。
- 对大快照压缩或持久化。
- 对不需要恢复的字段不保存。
控制内存的前提是明确撤销粒度和恢复需求。
完整版教学
一、为什么备忘录容易耗内存
备忘录保存的是历史状态。用户操作越多,历史越长。如果每一步都保存完整对象,内存会线性增长。
对象越大,问题越明显。比如图像编辑、文档编辑、流程编排、地图编辑,完整快照可能非常重。
二、限制历史步数
最简单有效的方法是限制 undo 栈大小。例如只保留最近 50 步。超过上限时丢弃最旧快照。
这会牺牲无限撤销能力,但换来可控内存。多数产品也不会真的支持无限撤销。
三、增量快照和结构共享
增量快照只记录变化部分。例如文本编辑器记录插入了哪些字符、删除了哪些范围。
结构共享则让多个快照共享未变化的数据,只为变化部分创建新节点。不可变数据结构常用这种思路。
四、常见误区与工程判断
不要把所有字段都放进备忘录。有些字段是缓存、临时计算结果、连接句柄、UI 临时状态,恢复时可以重新计算或重新创建。
工程中要先定义“恢复到过去”到底恢复哪些语义。恢复业务数据和恢复所有运行时细节不是一回事。
五、内存优化要围绕快照粒度设计
备忘录模式的性能问题通常不是模式本身造成的,而是快照策略过粗。每次状态变化都保存完整对象,会很快产生大量历史数据。尤其是文档编辑、画布编辑、游戏存档这类状态很大的场景,完整快照会带来明显内存压力。
常见优化有四类。第一,限制历史栈大小,只保留最近 N 步。第二,合并连续小操作,例如连续输入合并成一次历史记录。第三,保存增量变化,只记录这次操作改了哪些字段。第四,使用不可变数据结构或结构共享,让多个快照复用未变化部分。
面试时可以补一句取舍:完整快照实现简单、恢复快,但占内存;增量快照省空间,但恢复可能需要回放多个变更,逻辑更复杂。工程设计要根据撤销频率、状态大小、响应时间要求来选。
如果面试官问“怎么选优化方案”,可以按状态规模回答:小对象、低频撤销可以直接完整快照;大对象、高频变更要考虑增量记录;多人协作或版本管理场景还要考虑持久化、压缩和过期清理。不要把所有场景都套成一个栈。
还可以区分内存快照和持久化快照。短期撤销通常放内存,强调速度;长期版本恢复可能要落数据库或对象存储,强调可靠性和可追溯。二者都叫历史状态,但存储策略、清理策略和一致性要求完全不同。
六、用工程约束检验答案
内存预算应先量化:每份完整快照 200 KB、保留 100 步约占 20 MB;若限制为 20 步则约 4 MB。增量快照能继续降低空间,但恢复第 N 步可能需要重放多个差量,形成时间与空间交换。
| 检查项 | 核心判断 | 工程含义 |
|---|---|---|
| 定长历史 | 空间上界清晰 | 淘汰最旧快照 |
| 增量快照 | 只存变化字段 | 恢复链更复杂 |
| 结构共享 | 复用不可变节点 | 要求严格不可变 |
把关键关系压缩成一条可复述的路径:
估算单快照 S
完整历史空间约 N × S
设置容量或混合全量/增量
压测恢复延迟与峰值内存
不要只说“限制栈大小”,面试中给出 S×N 的预算和恢复延迟,方案才具有工程可信度。
落地前可以再按下面 3 步复核:
- 先说明“定长历史”的核心机制:空间上界清晰;再交代边界:淘汰最旧快照。
- 接着分析“增量快照”:只存变化字段;不能遗漏对应代价或结果:恢复链更复杂。
- 最后用“结构共享”检查方案:复用不可变节点;验收时确认要求严格不可变。
这三项构成完整判断链:先讲清定长历史,再说明增量快照,最后用结构共享检验实现是否越界。
面试中若能给出违反“要求严格不可变”的反例,再说明修正办法,答案就从模式定义落到了可验证的工程决策。
七、常见误区与追问
- 误区:只看到“定长历史”就认为方案成立。 必须同时说明核心机制“空间上界清晰”和工程边界“淘汰最旧快照”。
- 误区:把“增量快照”当成无条件结论。 只有在“只存变化字段”成立时,才能据此讨论“恢复链更复杂”。
- 追问:增量快照总是更省吗? 元数据和碎片也有成本,变化比例高时未必优于全量。
- 追问:多久做一次全量快照? 根据重放上限确定,例如最多允许重放 20 个差量。
- 追问:压缩是否免费? 不是,它以 CPU 和恢复延迟换空间。
- 追问:软引用适合历史吗? 不适合保证撤销语义,因为 GC 可在不可预测时机清除。
- 追问:磁盘持久化能解决内存吗? 能转移容量压力,但引入 I/O、加密、版本和清理问题。
八、加强记忆
记忆时抓住这条主线:完整快照多了会吃内存;限制历史步数是最直接办法;增量快照和结构共享能减少重复数据;只保存恢复真正需要的状态。面试回答先给出模式意图,再用调用链或数据流说明角色协作,最后主动交代适用边界与工程代价。