概述
备忘录模式(Memento Pattern)是GoF行为型设计模式中的重要一员,其核心定义为:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后能将对象恢复到原先保存的状态。这一模式为我们提供了一种优雅的“后悔药”机制——它允许系统在任意时刻记录对象的内部快照,并在需要时将其回滚至历史状态,而整个过程完全不会暴露对象的内部实现细节。
备忘录模式解决的核心问题直指软件开发中的三大痛点:一是状态快照的保存与恢复,例如文本编辑器的内容回退;二是撤销/重做操作的实现,从办公软件到IDE均依赖此机制;三是对象状态的历史追溯,如游戏存档、调试断点等场景。传统的深拷贝加列表管理方式虽然能实现类似功能,但会严重破坏封装性,导致发起人类必须向外界暴露全部内部结构。
本文将采用由浅入深、层层递进的专家级解读方式。首先,我们将从模式的标准定义与UML结构出发,建立严谨的理论框架;接着,通过从原始代码到经典模式的代码演进,深刻体会封装原则的设计精髓;然后,深入到JDK、Spring、MyBatis等主流框架的源码层面,揭示备忘录模式在真实工业级产品中的精妙应用;随后,我们将视野拓展至分布式环境,剖析Seata、Redis、事件溯源架构中备忘录思想的延伸;此外,本文还设置了对比辨析章节和五个典型场景的完整实战Demo,并结合不少于10道专家级面试题进行深度解答。通过本文,你将全面掌握备忘录模式从基础到架构层的全貌。
一、模式定义与结构
1.1 GoF标准定义
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后能将对象恢复到原先保存的状态。
1.2 Mermaid UML类图
classDiagram
class Originator {
-state: String
+setState(state: String): void
+getState(): String
+createMemento(): Memento
+restoreMemento(m: Memento): void
}
class Memento {
-state: String
+Memento(state: String)
-getState(): String
}
class Caretaker {
-mementos: List~Memento~
+addMemento(m: Memento): void
+getMemento(index: int): Memento
}
Originator --> Memento : creates/uses
Caretaker --> Memento : stores/retrieves
1.3 角色职责与封装原则详解
上述UML类图清晰地勾勒出备忘录模式的三大核心角色及其相互关系,其设计精髓在于严格的封装边界控制。
Originator(发起人):它是需要被保存和恢复状态的核心业务对象。在本图中,Originator 拥有内部状态 state,并提供 createMemento() 方法用于创建一个包含当前状态快照的 Memento 对象,以及 restoreMemento(m: Memento) 方法用于根据传入的备忘录恢复自身状态。注意,只有 Originator 自己知道如何从备忘录中提取状态并还原,因为它对备忘录的内部结构拥有完全的访问权限。
Memento(备忘录):它是状态的“密封容器”。其构造函数接收状态并将其存储为私有字段,同时对外部(特别是 Caretaker)仅暴露极其有限的接口,甚至完全不暴露任何访问状态的方法(图中 getState() 是私有方法,仅供 Originator 访问)。这种设计被称为黑箱备忘录——Caretaker 只能持有备忘录对象的引用,却无法窥探或修改其内容,从而完美保护了 Originator 的封装性。如果备忘录对外暴露了状态访问方法,则成为白箱备忘录,虽然实现简单,但牺牲了封装性。
Caretaker(管理者):它是备忘录的“保管员”。它负责保存和管理 Memento 对象的集合(如列表、栈),提供 addMemento() 和 getMemento() 等接口用于存取。关键在于,Caretaker 对备忘录的内容完全不知情——它不能也不应该解析备忘录的内部数据。这种“不透明性”是备忘录模式封装原则的直接体现:Caretaker 只负责“何时保存”和“何时取出”,而“保存什么”和“如何恢复”则完全交由 Originator 自己处理。这种职责分离使得状态的维护逻辑高度内聚在发起人类内部,避免了外部代码的侵入。
二、代码演进与实现
2.1 不使用模式的原始代码:深拷贝与封装破坏
import java.util.ArrayList;
import java.util.List;
/**
* 原始方式:直接通过深拷贝对象并手动管理副本列表实现撤销
* 问题分析:暴露内部状态、破坏封装、代码耦合
*/
class Document {
private String content; // 内部状态
private String fontName; // 内部状态
private int fontSize; // 内部状态
// 构造与业务方法略...
// 为了被外部深拷贝,必须提供getter/setter,暴露了全部内部结构
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getFontName() { return fontName; }
public void setFontName(String fontName) { this.fontName = fontName; }
public int getFontSize() { return fontSize; }
public void setFontSize(int fontSize) { this.fontSize = fontSize; }
// 深拷贝方法:外部需要知道如何复制Document对象
public Document deepCopy() {
Document copy = new Document();
copy.setContent(this.content);
copy.setFontName(this.fontName);
copy.setFontSize(this.fontSize);
return copy;
}
@Override
public String toString() {
return "Document{content='" + content + "', fontName='" + fontName + "', fontSize=" + fontSize + "}";
}
}
// 客户端直接管理副本列表
public class NaiveUndoDemo {
public static void main(String[] args) {
Document doc = new Document();
doc.setContent("Hello");
doc.setFontName("Arial");
doc.setFontSize(12);
List<Document> history = new ArrayList<>(); // 手动管理历史状态
history.add(doc.deepCopy()); // 保存初始状态
// 第一次修改
doc.setContent("Hello World");
doc.setFontSize(14);
history.add(doc.deepCopy());
// 第二次修改
doc.setContent("Hello World!");
doc.setFontName("Times New Roman");
history.add(doc.deepCopy());
System.out.println("Current: " + doc);
System.out.println("History size: " + history.size());
// 撤销:从历史列表中取出上一个副本并覆盖当前对象字段
Document previous = history.get(history.size() - 2);
doc.setContent(previous.getContent());
doc.setFontName(previous.getFontName());
doc.setFontSize(previous.getFontSize());
System.out.println("After undo: " + doc);
}
}
问题分析:
- 封装破坏:
Document类被迫暴露了全部getter/setter,外部代码可以直接读取和修改其内部状态,这与面向对象的封装原则背道而驰。 - 高耦合:客户端必须清楚了解
Document的所有字段,并手动编写字段逐个复制的恢复逻辑。一旦Document增加新字段(如boolean isBold),所有涉及深拷贝和恢复的客户端代码都必须同步修改,维护噩梦。 - 职责不清:历史记录的管理逻辑直接耦合在客户端代码中,违反了单一职责原则。
2.2 经典备忘录模式重构
import java.util.Stack;
/**
* Originator: 文档编辑器,负责创建备忘录和从备忘录恢复状态
*/
class Editor {
private String content;
private String fontName;
private int fontSize;
// 业务方法:修改状态
public void setContent(String content) { this.content = content; }
public void setFont(String fontName, int fontSize) {
this.fontName = fontName;
this.fontSize = fontSize;
}
// 创建备忘录:将当前状态打包成一个Memento对象
public EditorMemento createMemento() {
return new EditorMemento(this.content, this.fontName, this.fontSize);
}
// 从备忘录恢复状态:只有Originator自己能解析Memento的内部数据
public void restoreMemento(EditorMemento memento) {
this.content = memento.getContent(); // 直接访问Memento的包级私有方法
this.fontName = memento.getFontName();
this.fontSize = memento.getFontSize();
}
@Override
public String toString() {
return "Editor{content='" + content + "', font='" + fontName + "', size=" + fontSize + "}";
}
/**
* Memento: 备忘录类,作为Editor的静态内部类。
* 所有字段和方法都是private或包级私有,对外部Caretaker完全隐藏。
*/
public static class EditorMemento {
private final String content; // final保证不可变性,防止意外修改
private final String fontName;
private final int fontSize;
private EditorMemento(String content, String fontName, int fontSize) {
this.content = content;
this.fontName = fontName;
this.fontSize = fontSize;
}
// 仅允许同包下的Editor访问这些getter(包级私有)
private String getContent() { return content; }
private String getFontName() { return fontName; }
private int getFontSize() { return fontSize; }
}
}
/**
* Caretaker: 历史记录管理者,只负责保存备忘录,不关心其内容
*/
class HistoryCaretaker {
// 使用双栈实现撤销/重做:undoStack存放已保存的历史,redoStack存放被撤销的状态
private final Stack<Editor.EditorMemento> undoStack = new Stack<>();
private final Stack<Editor.EditorMemento> redoStack = new Stack<>();
public void saveState(Editor.EditorMemento memento) {
undoStack.push(memento);
redoStack.clear(); // 新状态产生后,重做栈失效
}
public Editor.EditorMemento undo() {
if (!undoStack.isEmpty()) {
Editor.EditorMemento current = undoStack.pop();
redoStack.push(current);
return undoStack.isEmpty() ? null : undoStack.peek();
}
return null;
}
public Editor.EditorMemento redo() {
if (!redoStack.isEmpty()) {
Editor.EditorMemento redoState = redoStack.pop();
undoStack.push(redoState);
return redoState;
}
return null;
}
public boolean canUndo() { return undoStack.size() > 1; }
public boolean canRedo() { return !redoStack.isEmpty(); }
}
// 客户端演示
public class MementoPatternDemo {
public static void main(String[] args) {
Editor editor = new Editor();
HistoryCaretaker caretaker = new HistoryCaretaker();
// 初始状态
editor.setContent("Hello");
editor.setFont("Arial", 12);
caretaker.saveState(editor.createMemento());
System.out.println("Initial: " + editor);
// 第一次修改
editor.setContent("Hello World");
editor.setFont("Arial", 14);
caretaker.saveState(editor.createMemento());
System.out.println("After edit1: " + editor);
// 第二次修改
editor.setContent("Hello World!");
editor.setFont("Times New Roman", 14);
caretaker.saveState(editor.createMemento());
System.out.println("After edit2: " + editor);
// 执行撤销
Editor.EditorMemento undoState = caretaker.undo();
if (undoState != null) {
editor.restoreMemento(undoState);
System.out.println("After undo: " + editor);
}
// 再次撤销
undoState = caretaker.undo();
if (undoState != null) {
editor.restoreMemento(undoState);
System.out.println("After second undo: " + editor);
}
// 执行重做
Editor.EditorMemento redoState = caretaker.redo();
if (redoState != null) {
editor.restoreMemento(redoState);
System.out.println("After redo: " + editor);
}
}
}
2.3 备忘录模式的进阶特性
a. 多状态快照管理(版本跳转)
import java.util.*;
// 支持多版本管理的Originator
class DocumentVersion {
private String content;
private int versionNumber;
public void setContent(String content) { this.content = content; this.versionNumber++; }
public String getContent() { return content; }
public int getVersionNumber() { return versionNumber; }
public DocumentMemento createMemento() {
return new DocumentMemento(this.content, this.versionNumber);
}
public void restoreMemento(DocumentMemento memento) {
this.content = memento.getContent();
this.versionNumber = memento.getVersionNumber();
}
public static class DocumentMemento {
private final String content;
private final int versionNumber;
private DocumentMemento(String content, int version) {
this.content = content;
this.versionNumber = version;
}
private String getContent() { return content; }
private int getVersionNumber() { return versionNumber; }
}
}
// 支持跳转到任意历史版本的Caretaker
class VersionCaretaker {
private final Map<Integer, DocumentVersion.DocumentMemento> versionMap = new HashMap<>();
private int currentVersion = 0;
public void saveVersion(DocumentVersion.DocumentMemento memento, int version) {
versionMap.put(version, memento);
currentVersion = version;
}
public DocumentVersion.DocumentMemento getVersion(int version) {
return versionMap.get(version);
}
public List<Integer> getAvailableVersions() {
return new ArrayList<>(versionMap.keySet());
}
}
b. 增量备忘录(只保存变更部分)
// 增量备忘录:仅记录与前一版本的差异
class IncrementalEditor {
private StringBuilder content = new StringBuilder();
private int cursorPos = 0;
public void insert(String text) {
content.insert(cursorPos, text);
cursorPos += text.length();
}
public void delete(int length) {
if (cursorPos >= length) {
content.delete(cursorPos - length, cursorPos);
cursorPos -= length;
}
}
// 创建增量备忘录:只记录本次操作类型和参数,而非完整状态
public OperationMemento createOperationMemento(String opType, String data, int pos) {
return new OperationMemento(opType, data, pos);
}
// 应用反向操作来恢复(增量恢复)
public void applyReverse(OperationMemento memento) {
if ("INSERT".equals(memento.opType)) {
// 反向操作:删除之前插入的内容
content.delete(memento.position, memento.position + memento.data.length());
cursorPos = memento.position;
} else if ("DELETE".equals(memento.opType)) {
// 反向操作:重新插入之前删除的内容
content.insert(memento.position, memento.data);
cursorPos = memento.position + memento.data.length();
}
}
static class OperationMemento {
final String opType;
final String data;
final int position;
OperationMemento(String opType, String data, int pos) {
this.opType = opType;
this.data = data;
this.position = pos;
}
}
}
c. 与原型模式的结合
// 原型模式结合:利用克隆生成状态副本作为备忘录
class GraphicObject implements Cloneable {
private String shape;
private int x, y;
// ... 构造器和业务方法
@Override
protected GraphicObject clone() {
try {
return (GraphicObject) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// 备忘录直接使用克隆对象
public GraphicObject createMemento() {
return this.clone();
}
public void restoreMemento(GraphicObject memento) {
this.shape = memento.shape;
this.x = memento.x;
this.y = memento.y;
}
}
d. 白箱与黑箱备忘录对比
| 特性 | 白箱备忘录 | 黑箱备忘录 |
|---|---|---|
| 封装性 | 弱,备忘录向外界提供访问内部状态的接口(如getter) | 强,备忘录对外界完全不透明,仅Originator可访问 |
| 实现复杂度 | 简单,Caretaker可直接操作备忘录数据 | 较复杂,通常需要将Memento设计为Originator的内部类 |
| 安全性 | 低,任何持有备忘录的对象都可以窥探状态 | 高,状态得到严格保护 |
| Java实现 | 独立的公共类,提供public getter | 静态内部类,私有构造器,包级私有访问器 |
2.4 状态保存与恢复时序图
sequenceDiagram
participant Client
participant Originator
participant Memento
participant Caretaker
Note over Client: 业务操作执行前/后保存状态
Client->>Originator: 1. 执行业务操作()
Originator->>Originator: 修改内部状态
Client->>Originator: 2. createMemento()
Originator->>Memento: 3. new Memento(state)
Memento-->>Originator: 4. 返回备忘录实例
Originator-->>Client: 5. 返回备忘录
Client->>Caretaker: 6. saveState(memento)
Caretaker->>Caretaker: 存入集合(如栈)
Note over Client: 需要撤销时恢复状态
Client->>Caretaker: 7. getState()
Caretaker-->>Client: 8. 返回指定备忘录
Client->>Originator: 9. restoreMemento(memento)
Originator->>Memento: 10. 访问私有getter获取状态
Memento-->>Originator: 11. 返回保存的状态数据
Originator->>Originator: 用获取的数据覆盖当前状态
Originator-->>Client: 12. 状态恢复完成
时序图说明:
上图完整描绘了备忘录模式在一次完整的“保存-恢复”周期中的对象交互流程。整个流程分为两个清晰的阶段:
阶段一:状态保存(步骤1-6)。客户端首先触发 Originator 执行某些业务操作,导致其内部状态发生变化。随后,客户端调用 createMemento() 方法请求保存当前状态。Originator 此时充当备忘录的工厂,它创建一个新的 Memento 对象,并将自身当前状态的副本(通常是不可变数据)传入备忘录的构造函数。Memento 对象被创建后返回给客户端,客户端并不解析其内容,而是立即将其传递给 Caretaker 的 saveState() 方法,由 Caretaker 将其存入内部集合(如栈、列表或Map)中进行持久化管理。
阶段二:状态恢复(步骤7-12)。当需要回滚时,客户端向 Caretaker 请求一个历史备忘录。Caretaker 从集合中取出相应的 Memento 对象并返回。客户端随即调用 Originator 的 restoreMemento(memento) 方法,并将备忘录作为参数传入。此时,封装性的关键体现在于:只有 Originator 知道自己曾经往 Memento 里存了什么,也只有它能通过调用 Memento 的私有或包级私有访问器(如 getState())来提取状态数据。Originator 获取到这些数据后,用它们覆盖自身的当前状态,最终完成恢复并向客户端确认。
整个过程中,Caretaker 仅仅是 Memento 对象的“搬运工”,它从未(也无法)窥视 Memento 的内容,从而将状态管理的复杂度完全封装在 Originator 和 Memento 的内部。
三、源码级应用分析
3.1 JDK中的备忘录模式
java.io.Serializable(序列化机制)
import java.io.*;
// Originator: 实现了Serializable的业务对象
class UserProfile implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient字段不会被默认序列化
private int age;
// 自定义序列化,可看作创建备忘录的过程
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 保存非transient字段
// 可以加密保存password
out.writeObject(encrypt(password));
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.password = decrypt((String) in.readObject());
}
// 省略加密解密细节...
}
// Caretaker: 文件系统或ByteArrayOutputStream
public class SerializationMementoDemo {
public static void main(String[] args) throws Exception {
UserProfile user = new UserProfile("alice", "secret", 25);
// 将对象状态写入字节数组(创建备忘录)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(user);
byte[] mementoData = baos.toByteArray(); // 这就是备忘录
// 从字节数组恢复对象
ByteArrayInputStream bais = new ByteArrayInputStream(mementoData);
ObjectInputStream ois = new ObjectInputStream(bais);
UserProfile restored = (UserProfile) ois.readObject();
}
}
分析:Serializable 机制本质上就是备忘录模式在JVM层面的标准实现。writeObject() 相当于 createMemento(),它将对象的状态转换为字节序列(备忘录);而 readObject() 相当于 restoreMemento(),从字节序列重建对象状态。ObjectOutputStream 和 ByteArrayOutputStream 共同扮演了 Caretaker 的角色,负责存储和提供序列化数据。
javax.swing.undo.UndoManager
// Swing中的撤销管理器是备忘录模式与命令模式的经典结合
JTextArea textArea = new JTextArea();
UndoManager undoManager = new UndoManager();
textArea.getDocument().addUndoableEditListener(undoManager);
// UndoableEdit接口的实现类(如DefaultDocumentEvent)就是备忘录
// UndoManager就是Caretaker
分析:UndoableEdit 接口的实现类记录了文档编辑操作前后的状态差异(增量备忘录)。UndoManager 作为 Caretaker 维护这些编辑事件的列表,并提供 undo() 和 redo() 方法。文档本身(Document)是 Originator,它通过 UndoableEdit 的 undo() 和 redo() 方法恢复状态。
java.util.Date等不可变类
// 不可变对象天然具备备忘录的特性
Date original = new Date();
// 想要保存当前时刻,只需持有该对象的引用
Date snapshot = original; // 实际上Date不可变,无需深拷贝
// 但由于Date是可变的(有setTime),所以实际中应使用新对象
Date memento = new Date(original.getTime());
3.2 Spring框架深度剖析
StateManageableMessageContext与ConversationScope
Spring Web Flow 中,ConversationScope 管理的流程状态本质上就是一系列备忘录。当用户在一个向导式流程中前进或后退时,框架会将每个步骤的表单数据(称为 ViewState 的快照)保存在会话中。StateManageableMessageContext 负责管理这些状态消息的备忘录,确保在流程回退时能够恢复正确的提示信息。
@SessionAttributes与RedirectAttributes
@Controller
@SessionAttributes("formData") // 将formData对象存储在会话中作为备忘录
public class WizardController {
@ModelAttribute("formData")
public FormData formData() {
return new FormData();
}
@PostMapping("/step1")
public String step1(@ModelAttribute("formData") FormData data, RedirectAttributes attrs) {
// RedirectAttributes.addFlashAttribute 将对象序列化到Session中作为临时备忘录
attrs.addFlashAttribute("message", "Step 1 completed");
return "redirect:/step2";
}
}
分析:@SessionAttributes 将模型对象持久化在HTTP Session中,相当于一个跨请求的备忘录管理器。RedirectAttributes 的 addFlashAttribute 则利用了Flash Scope,将数据保存到会话中并在下一个请求后立即销毁,这是一种“一次性备忘录”。
Spring StateMachine中的StateContext
Spring StateMachine 中的 StateContext 对象在状态流转过程中被创建,它包含了当前状态、触发事件、扩展属性等信息。当状态机持久化时,StateContext 会被序列化存储(如存入Redis或数据库),这正是一个包含状态机完整上下文的备忘录。当状态机从存储中恢复时,框架会根据这个上下文备忘录重建状态机实例的当前状态。
3.3 MyBatis框架
一级缓存与二级缓存
// MyBatis的PerpetualCache实现了缓存,本质是备忘录的存储
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>(); // 存储查询结果的备忘录
@Override
public void putObject(Object key, Object value) {
cache.put(key, value); // 保存查询结果快照
}
@Override
public Object getObject(Object key) {
return cache.get(key); // 获取历史查询结果
}
}
分析:MyBatis 的一级缓存(SqlSession级别)和二级缓存(Mapper级别)将SQL查询的结果集作为备忘录缓存起来。当相同的查询再次执行时,可以直接从缓存(备忘录)中恢复结果,而无需再次访问数据库。这里的 CacheKey 是备忘录的唯一标识,MappedStatement 和结果集是备忘录内容。
ResultMap嵌套结果映射暂存
在处理嵌套结果映射(如一对多、多对多)时,MyBatis 会使用 ResultContext 暂存中间处理状态,并在处理完成后将完整对象图返回。这种暂存机制也是一种临时的备忘录应用。
3.4 其他框架
Hibernate Entity状态快照
Hibernate 的一级缓存(持久化上下文)在加载实体时会保存一份实体属性的快照(通常是一个对象数组)。当事务提交时,Hibernate 会将当前实体属性与快照进行脏检查(Dirty Checking),只有发生变化的属性才会生成UPDATE语句。这份快照就是典型的备忘录。
Git版本控制
Git 是最为经典的备忘录模式大规模应用实例:
- Originator:工作区(Working Directory)
- Memento:Commit 对象(包含树对象、父提交指针、提交信息等完整快照)
- Caretaker:
.git目录(存储所有commit对象的数据库)
每次 git commit 就是创建当前工作区状态的一个备忘录,git checkout <commit-hash> 就是根据备忘录恢复工作区状态。
四、分布式环境下的备忘录模式
4.1 分布式事务中的状态回滚:Seata AT模式
Seata AT模式的核心理念正是备忘录模式。在事务分支(Branch Transaction)执行前,Seata 会解析SQL语句并执行**前置镜像(Before Image)**查询,将修改前的数据作为备忘录保存在 undo_log 表中。若事务需要回滚,Seata 会读取 undo_log 中的备忘录数据,生成反向SQL(如INSERT的反向是DELETE,UPDATE的反向是根据before image恢复)来还原数据。
-- Seata AT模式的undo_log表结构(简化)
CREATE TABLE undo_log (
id BIGINT PRIMARY KEY,
branch_id BIGINT,
xid VARCHAR(100),
context VARCHAR(512),
rollback_info LONGBLOB, -- 序列化后的备忘录数据(包含before image和after image)
log_status INT
);
4.2 分布式工作流引擎:Flowable/Camunda
Flowable 工作流引擎在流程实例挂起时,会将流程变量(Process Variables)序列化并存储到 ACT_RU_VARIABLE 表中。当流程恢复执行时,引擎从数据库读取变量并反序列化到流程上下文中。这是一种典型的将备忘录持久化到关系型数据库的做法。
4.3 配置中心的版本回滚:Apollo/Nacos
Apollo配置中心为每个配置项维护完整的发布历史记录。当管理员需要回滚到历史版本时,只需选择目标发布版本,系统会将该版本的配置内容(备忘录)重新发布。Nacos的配置管理同样提供了历史版本列表和一键回滚功能。
4.4 分布式缓存中的热备与恢复:Redis RDB与AOF
- RDB快照:Redis定期将内存中的数据全量保存到
.rdb文件中,这是一个全量备忘录。恢复时直接加载该文件。 - AOF日志:记录每条写命令,这是一种增量备忘录序列。恢复时通过重放AOF日志来重建数据状态。
4.5 事件溯源(Event Sourcing)与备忘录模式
在事件溯源架构中,聚合根的状态不是直接存储的,而是由一系列不可变的事件(Event)推导而来。为了避免每次重建状态都需要重放所有历史事件(性能开销巨大),系统会定期生成快照(Snapshot)。快照就是聚合根在某个时间点的状态备忘录。恢复时,先加载最近的快照,然后仅重放快照之后产生的事件即可。
4.6 基于Redis存储备忘录的分布式撤销系统示例
import redis.clients.jedis.Jedis;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.UUID;
// 分布式Originator
class DistributedDocument {
private String docId;
private String content;
private int version;
// ... 构造器和业务方法
public DocumentMemento createMemento() {
return new DocumentMemento(this.docId, this.content, this.version);
}
public void restoreMemento(DocumentMemento memento) {
this.content = memento.content;
this.version = memento.version;
}
// 备忘录类需实现序列化接口以便存入Redis
public static class DocumentMemento {
public String docId;
public String content;
public int version;
// 无参构造器和getter/setter供Jackson使用
public DocumentMemento() {}
public DocumentMemento(String docId, String content, int version) {
this.docId = docId;
this.content = content;
this.version = version;
}
}
}
// 分布式Caretaker:基于Redis
class RedisCaretaker {
private final Jedis jedis;
private final ObjectMapper mapper = new ObjectMapper();
private static final String KEY_PREFIX = "doc:memento:";
public RedisCaretaker(String redisHost, int port) {
this.jedis = new Jedis(redisHost, port);
}
// 保存备忘录,使用Redis List作为历史记录栈
public void pushMemento(String docId, DistributedDocument.DocumentMemento memento) {
try {
String json = mapper.writeValueAsString(memento);
jedis.lpush(KEY_PREFIX + docId, json);
// 可选:限制历史记录数量,如只保留最近20个
jedis.ltrim(KEY_PREFIX + docId, 0, 19);
} catch (Exception e) {
throw new RuntimeException("Failed to save memento", e);
}
}
// 获取上一个版本的备忘录(用于撤销)
public DistributedDocument.DocumentMemento popMemento(String docId) {
// 先弹出当前版本(栈顶),再获取上一个版本
jedis.lpop(KEY_PREFIX + docId);
String json = jedis.lpop(KEY_PREFIX + docId);
if (json == null) return null;
try {
return mapper.readValue(json, DistributedDocument.DocumentMemento.class);
} catch (Exception e) {
throw new RuntimeException("Failed to restore memento", e);
}
}
// 获取指定版本(用于跳转)
public DistributedDocument.DocumentMemento getMementoAt(String docId, int index) {
String json = jedis.lindex(KEY_PREFIX + docId, index);
if (json == null) return null;
try {
return mapper.readValue(json, DistributedDocument.DocumentMemento.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// 演示跨会话回滚
public class DistributedUndoDemo {
public static void main(String[] args) {
RedisCaretaker caretaker = new RedisCaretaker("localhost", 6379);
String docId = UUID.randomUUID().toString();
DistributedDocument doc = new DistributedDocument(docId, "Initial", 1);
caretaker.pushMemento(docId, doc.createMemento());
doc.setContent("Version 2");
caretaker.pushMemento(docId, doc.createMemento());
// 模拟另一个服务节点读取备忘录并恢复
DistributedDocument.DocumentMemento previous = caretaker.popMemento(docId);
if (previous != null) {
doc.restoreMemento(previous);
System.out.println("Rolled back to: " + doc.getContent());
}
}
}
4.7 分布式备忘录架构流程图
flowchart TD
subgraph ServiceNode1 [服务节点A]
A[业务操作] --> B[创建备忘录 createMemento]
B --> C[序列化备忘录对象]
C --> D[存入共享存储]
end
subgraph SharedStorage [共享存储层]
E[(Redis 集群)]
F[(关系型数据库)]
D --> E
D --> F
end
subgraph ServiceNode2 [服务节点B 或 后续请求]
G[从共享存储读取备忘录] --> H[反序列化]
H --> I[调用 restoreMemento]
I --> J[状态恢复完成]
end
D -.-> G
E -.-> G
F -.-> G
流程图说明:
上图展示了在分布式环境下,备忘录模式如何支持跨服务节点和跨会话的状态恢复。整个架构分为三个逻辑层次:
服务节点层:包含多个独立运行的服务实例(如Node A和Node B)。当服务节点A执行业务操作(如编辑文档、提交订单)后,它会调用 Originator.createMemento() 创建当前状态的快照对象。随后,通过序列化技术(如JSON、Kryo、Protobuf)将该备忘录对象转换为可传输、可持久化的字节流或字符串。
共享存储层:序列化后的备忘录数据被存入高可用的共享存储中。这里通常使用 Redis集群 作为热数据缓存,利用其高性能和丰富的数据结构(List、Hash、Sorted Set)实现版本栈管理;同时也可持久化到 关系型数据库(如MySQL)中作为长期归档。存储层确保了备忘录在分布式环境下的可见性与持久性。
恢复流程:当同一个业务上下文(例如同一个文档ID)在另一个服务节点B上被访问,或者用户发起撤销操作时,服务节点B会从共享存储中读取对应的备忘录数据(例如从Redis List中LPOP获取上一个版本)。数据被反序列化还原为 Memento 对象,然后传递给 Originator.restoreMemento() 方法。Originator 解析备忘录内容并覆盖当前状态,从而完成跨节点的状态回滚。这种设计使得用户无论被负载均衡到哪个后端节点,都能获得一致的状态恢复体验。
五、对比辨析
5.1 备忘录模式 vs 原型模式
| 维度 | 备忘录模式 | 原型模式 |
|---|---|---|
| 核心意图 | 保存对象状态快照,以便将来恢复 | 通过克隆原型对象来创建新实例 |
| 关注点 | 状态的时态管理(过去、现在) | 对象的创建成本优化 |
| 典型操作 | createMemento() → 存储 → restoreMemento(m) | clone() → 获得新对象 |
| 对象关系 | 一个Originator对应多个Memento(不同时刻的快照) | 一个原型可克隆出多个相同状态的独立实例 |
| 结合点 | 原型模式常用于实现备忘录的创建(克隆当前对象作为备忘录) |
5.2 备忘录模式 vs 命令模式
命令模式与备忘录模式经常配合实现可撤销操作。命令模式将“操作请求”封装为对象,而备忘录模式提供“操作结果状态”的保存。典型协作流程:
- 客户端调用命令对象的
execute()前,命令对象主动向Originator请求一个Memento并自己持有。 execute()执行,修改Originator状态。- 需要撤销时,调用命令对象的
undo(),命令对象将之前持有的Memento还给Originator进行恢复。
区别:命令模式关注行为的封装,备忘录模式关注状态的捕获。
5.3 备忘录模式 vs 状态模式
- 备忘录模式:关注对象在某一个时刻的状态值(静态快照)。
- 状态模式:关注对象在不同条件下的行为变化以及状态间的转换逻辑(动态行为)。
状态模式中的 State 对象本身可以看作一种行为型的备忘录,但它通常不关心如何回退到之前的状态,而是关注如何根据当前状态决定下一步行为。
5.4 备忘录模式 vs 序列化
序列化是将对象转换为字节流的技术手段,而备忘录模式是一种设计思想。序列化是实现备忘录模式最自然、最强大的方式之一,但它不等同于备忘录模式:
- 备忘录模式强调封装性和职责分离(Caretaker不能访问内部状态)。
- 序列化生成的字节流如果不加保护,任何拿到字节流的人都能反序列化窥探状态。因此,在严格的黑箱备忘录实现中,可能需要在序列化前进行加密。
5.5 白箱备忘录 vs 黑箱备忘录
| 对比维度 | 白箱备忘录 | 黑箱备忘录 |
|---|---|---|
| 实现方式 | 备忘录提供公共 getState() 方法 | 备忘录所有访问器为 private 或包级私有 |
| Caretaker | 可以读取备忘录内容,耦合度高 | 完全不能读取内容,只能持有引用 |
| 封装性 | 差,暴露了Originator的内部状态结构 | 好,完全隐藏Originator的实现细节 |
| 语言支持 | 任意语言均可实现 | 需要语言支持内部类或友元机制(Java内部类、C++ friend) |
| 适用场景 | 简单应用,或序列化场景(序列化后数据必然可读) | 对安全性、封装性要求高的企业级应用 |
六、适用场景分析(含独立完整Demo)
场景一:文本编辑器撤销/重做
Demo代码
import java.util.Stack;
// Originator: 文本编辑器
class TextEditor {
private StringBuilder text = new StringBuilder();
private int cursorPosition = 0;
public void insert(String str) {
text.insert(cursorPosition, str);
cursorPosition += str.length();
}
public void delete(int length) {
if (cursorPosition >= length) {
text.delete(cursorPosition - length, cursorPosition);
cursorPosition -= length;
}
}
public void moveCursor(int pos) {
if (pos >= 0 && pos <= text.length()) {
this.cursorPosition = pos;
}
}
public String getText() { return text.toString(); }
public int getCursor() { return cursorPosition; }
// 创建全量快照备忘录
public TextMemento createMemento() {
return new TextMemento(text.toString(), cursorPosition);
}
public void restoreMemento(TextMemento memento) {
this.text = new StringBuilder(memento.text);
this.cursorPosition = memento.cursorPos;
}
// 黑箱备忘录
public static class TextMemento {
private final String text;
private final int cursorPos;
private TextMemento(String text, int cursor) {
this.text = text;
this.cursorPos = cursor;
}
// 只有TextEditor可以访问
private String getText() { return text; }
private int getCursorPos() { return cursorPos; }
}
}
// Caretaker: 历史管理器(双栈实现撤销/重做)
class EditorHistory {
private Stack<TextEditor.TextMemento> undoStack = new Stack<>();
private Stack<TextEditor.TextMemento> redoStack = new Stack<>();
// 任何状态改变后调用,保存当前状态
public void push(TextEditor.TextMemento memento) {
undoStack.push(memento);
redoStack.clear(); // 新操作使重做历史失效
}
public boolean canUndo() { return undoStack.size() > 1; }
public boolean canRedo() { return !redoStack.isEmpty(); }
public TextEditor.TextMemento undo(TextEditor.TextMemento current) {
if (canUndo()) {
redoStack.push(undoStack.pop()); // 当前状态入重做栈
return undoStack.peek(); // 返回上一个状态
}
return current;
}
public TextEditor.TextMemento redo() {
if (canRedo()) {
TextEditor.TextMemento m = redoStack.pop();
undoStack.push(m);
return m;
}
return null;
}
}
// 客户端演示
public class TextEditorUndoDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
EditorHistory history = new EditorHistory();
// 初始状态快照
history.push(editor.createMemento());
editor.insert("Hello");
history.push(editor.createMemento());
System.out.println("After insert: " + editor.getText());
editor.insert(" World");
history.push(editor.createMemento());
System.out.println("After insert2: " + editor.getText());
editor.delete(6);
history.push(editor.createMemento());
System.out.println("After delete: " + editor.getText());
// 撤销
TextEditor.TextMemento undoState = history.undo(editor.createMemento());
editor.restoreMemento(undoState);
System.out.println("Undo 1: " + editor.getText());
undoState = history.undo(editor.createMemento());
editor.restoreMemento(undoState);
System.out.println("Undo 2: " + editor.getText());
// 重做
TextEditor.TextMemento redoState = history.redo();
editor.restoreMemento(redoState);
System.out.println("Redo 1: " + editor.getText());
}
}
Mermaid类图
classDiagram
class TextEditor {
-text: StringBuilder
-cursorPosition: int
+insert(str: String): void
+delete(length: int): void
+createMemento(): TextMemento
+restoreMemento(m: TextMemento): void
}
class TextMemento {
-text: String
-cursorPos: int
-TextMemento(text: String, cursor: int)
-getText(): String
-getCursorPos(): int
}
class EditorHistory {
-undoStack: Stack~TextMemento~
-redoStack: Stack~TextMemento~
+push(m: TextMemento): void
+undo(current: TextMemento): TextMemento
+redo(): TextMemento
+canUndo(): boolean
+canRedo(): boolean
}
TextEditor --> TextMemento
EditorHistory --> TextMemento
场景分析与文字说明
文本编辑器是备忘录模式最经典的应用场景。本Demo通过双栈结构(undoStack 和 redoStack)实现了标准的多级撤销与重做功能。
双栈结合备忘录模式的原理:EditorHistory 作为管理者维护了两个栈。当用户执行编辑操作(插入、删除)后,系统会立即调用 editor.createMemento() 生成当前文档状态的完整快照(备忘录),并将其推入 undoStack。此时,redoStack 会被清空,因为新的操作分支使得之前的“未来”状态不再可达。当用户触发撤销时,history.undo() 会将 undoStack 栈顶元素(即当前状态)弹出并压入 redoStack,然后返回新的栈顶元素(即上一个历史状态)供 TextEditor 恢复。重做操作则是反向过程:从 redoStack 弹出备忘录,压回 undoStack,并恢复。
备忘录对象的粒度选择:本Demo采用了全量快照策略,即每次保存都生成包含全部文本内容和光标位置的 TextMemento 对象。这种方式实现简单、恢复速度快,但在处理超长文档(如数十万字)时可能带来内存压力。在真实文本编辑器(如VSCode、IntelliJ IDEA)中,通常会采用增量操作备忘录(保存的是操作本身及其反向操作所需的信息,类似于2.3节中的增量备忘录),或者结合Piece Table数据结构仅记录变化区间,从而极大降低内存占用。但无论采用哪种粒度,备忘录模式提供的封装边界使得 TextEditor 的状态管理逻辑与历史管理逻辑完全解耦,保证了系统的可维护性和扩展性。
场景二:游戏角色存档系统
Demo代码
import java.util.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
// Originator: 游戏角色
class GameCharacter {
private String name;
private int health;
private int mana;
private int posX, posY;
private transient long lastActiveTime; // 临时状态,不存档
public GameCharacter(String name) {
this.name = name;
this.health = 100;
this.mana = 50;
this.posX = 0;
this.posY = 0;
this.lastActiveTime = System.currentTimeMillis();
}
public void move(int dx, int dy) { this.posX += dx; this.posY += dy; updateActivity(); }
public void takeDamage(int dmg) { this.health = Math.max(0, health - dmg); updateActivity(); }
public void castSpell(int cost) { this.mana = Math.max(0, mana - cost); updateActivity(); }
private void updateActivity() { lastActiveTime = System.currentTimeMillis(); }
// 创建存档备忘录
public CharacterMemento createMemento(String description) {
return new CharacterMemento(name, health, mana, posX, posY, description);
}
public void restoreMemento(CharacterMemento memento) {
this.name = memento.name;
this.health = memento.health;
this.mana = memento.mana;
this.posX = memento.posX;
this.posY = memento.posY;
// 注意:lastActiveTime是临时状态,不被恢复,保持当前时间
updateActivity();
}
@Override
public String toString() {
return String.format("Character{name='%s', health=%d, mana=%d, pos=(%d,%d), lastActive=%d}",
name, health, mana, posX, posY, lastActiveTime);
}
// 备忘录
public static class CharacterMemento {
private final String name;
private final int health, mana, posX, posY;
private final String description;
private final String timestamp;
private CharacterMemento(String name, int h, int m, int x, int y, String desc) {
this.name = name;
this.health = h;
this.mana = m;
this.posX = x;
this.posY = y;
this.description = desc;
this.timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
public String getDescription() { return description; }
public String getTimestamp() { return timestamp; }
// 其他字段仅GameCharacter可访问
private String getName() { return name; }
private int getHealth() { return health; }
private int getMana() { return mana; }
private int getPosX() { return posX; }
private int getPosY() { return posY; }
}
}
// Caretaker: 存档管理器,支持多槽位
class SaveSlotManager {
private final Map<String, GameCharacter.CharacterMemento> slots = new HashMap<>();
private final int maxSlots;
public SaveSlotManager(int maxSlots) {
this.maxSlots = maxSlots;
}
public boolean saveToSlot(String slotId, GameCharacter.CharacterMemento memento) {
if (slots.size() >= maxSlots && !slots.containsKey(slotId)) {
System.out.println("存档槽已满,请先删除旧存档");
return false;
}
slots.put(slotId, memento);
System.out.println("存档成功: 槽位=" + slotId + ", 描述=" + memento.getDescription());
return true;
}
public GameCharacter.CharacterMemento loadFromSlot(String slotId) {
return slots.get(slotId);
}
public void deleteSlot(String slotId) {
slots.remove(slotId);
}
public Set<String> listSlots() {
return slots.keySet();
}
}
// 演示
public class GameSaveLoadDemo {
public static void main(String[] args) throws InterruptedException {
GameCharacter hero = new GameCharacter("Arthas");
SaveSlotManager saveManager = new SaveSlotManager(5);
// 初始存档
saveManager.saveToSlot("AutoSave", hero.createMemento("游戏开始"));
hero.move(10, 5);
hero.takeDamage(20);
saveManager.saveToSlot("Slot1", hero.createMemento("击败第一波怪物后"));
Thread.sleep(100); // 模拟时间流逝
hero.castSpell(30);
hero.move(-5, 10);
saveManager.saveToSlot("Slot2", hero.createMemento("学习新技能后"));
System.out.println("\n当前角色状态: " + hero);
// 加载Slot1存档
System.out.println("\n--- 加载Slot1 ---");
GameCharacter.CharacterMemento memento = saveManager.loadFromSlot("Slot1");
hero.restoreMemento(memento);
System.out.println("恢复后角色状态: " + hero);
System.out.println("注意: lastActiveTime是临时状态,未被恢复,保持为当前时间");
}
}
Mermaid时序图
sequenceDiagram
participant Player as 玩家
participant Game as 游戏主循环(Originator)
participant Memento as CharacterMemento
participant SaveMgr as 存档管理器(Caretaker)
Player->>Game: 游戏进行中,角色状态变化
Game->>Game: move(), takeDamage()等
Player->>Game: 触发存档操作(slotId)
Game->>Game: createMemento(description)
Game->>Memento: new CharacterMemento(当前状态)
Memento-->>Game: 返回存档对象
Game->>SaveMgr: saveToSlot(slotId, memento)
SaveMgr->>SaveMgr: 存入Map(slotId -> memento)
SaveMgr-->>Player: 存档成功提示
Note over Player,SaveMgr: 后续游戏过程...
Player->>SaveMgr: 请求读档 loadFromSlot(slotId)
SaveMgr-->>Player: 返回对应的Memento对象
Player->>Game: restoreMemento(memento)
Game->>Memento: 获取私有状态字段(name, health等)
Memento-->>Game: 返回保存时的状态值
Game->>Game: 用快照数据覆盖当前状态
Game-->>Player: 读档完成,继续游戏
场景分析与文字说明
游戏存档系统是备忘录模式在持久化场景中的直观体现。本Demo实现了一个支持多存档槽位的RPG角色状态管理系统,核心机制围绕“状态的捕获与恢复”展开。
多存档槽位管理策略:SaveSlotManager 使用 Map<String, CharacterMemento> 作为底层存储结构,以槽位标识(如“AutoSave”、“Slot1”)为键,备忘录对象为值。这种设计允许玩家在不同游戏进度下创建多个独立的存档点,并可以随时选择任意槽位进行读档。Demo中通过 maxSlots 参数限制存档数量,模拟了实际游戏中有限的存档空间。此外,CharacterMemento 还记录了存档的描述信息和时间戳(通过 getDescription() 和 getTimestamp() 向外界暴露有限的元数据),但这些信息并不涉及角色的核心状态(生命值、坐标等),从而在提供友好用户体验的同时维持了封装性。
排除临时状态的设计:注意 GameCharacter 类中的 lastActiveTime 字段被标记为 transient(模拟),并且在 createMemento() 方法中并未被存入备忘录。这体现了存档设计中的一个重要考量:并非所有对象状态都需要被持久化。临时状态(如本次游戏会话的运行时长、网络连接状态、特效动画帧等)在恢复存档时应该被重置或重新初始化,否则可能导致逻辑错误。在 restoreMemento() 中,我们可以看到角色核心属性被恢复,但 lastActiveTime 被更新为当前系统时间,确保了时间相关逻辑的正确性。这种选择性保存正是备忘录模式灵活性的体现。
场景三:数据库事务回滚模拟
Demo代码
import java.util.*;
// 模拟数据库表的一行记录
class AccountRow {
private String accountId;
private double balance;
public AccountRow(String id, double balance) {
this.accountId = id;
this.balance = balance;
}
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
public AccountMemento createMemento() {
return new AccountMemento(accountId, balance);
}
public void restoreMemento(AccountMemento memento) {
this.balance = memento.balance;
}
// 备忘录
public static class AccountMemento {
private final String accountId;
private final double balance;
private AccountMemento(String id, double bal) {
this.accountId = id;
this.balance = bal;
}
private double getBalance() { return balance; }
}
@Override
public String toString() {
return "Account{" + accountId + ", balance=" + balance + "}";
}
}
// 模拟事务管理器
class TransactionManager {
// 存储before image备忘录
private final Map<String, AccountRow.AccountMemento> beforeImages = new HashMap<>();
private boolean inTransaction = false;
public void beginTransaction() {
if (inTransaction) throw new IllegalStateException("Transaction already active");
beforeImages.clear();
inTransaction = true;
System.out.println("事务开始");
}
// 在更新前保存快照
public void registerBeforeImage(AccountRow row) {
if (!inTransaction) throw new IllegalStateException("No active transaction");
if (!beforeImages.containsKey(row.accountId)) {
beforeImages.put(row.accountId, row.createMemento());
}
}
public void commit() {
if (!inTransaction) throw new IllegalStateException("No active transaction");
beforeImages.clear();
inTransaction = false;
System.out.println("事务提交,快照已清除");
}
public void rollback(Map<String, AccountRow> tableData) {
if (!inTransaction) throw new IllegalStateException("No active transaction");
System.out.println("事务回滚,根据备忘录恢复数据");
for (Map.Entry<String, AccountRow.AccountMemento> entry : beforeImages.entrySet()) {
String id = entry.getKey();
AccountRow row = tableData.get(id);
if (row != null) {
row.restoreMemento(entry.getValue());
System.out.println(" 回滚账户 " + id + " 到余额: " + row.getBalance());
}
}
beforeImages.clear();
inTransaction = false;
}
}
// 模拟数据库操作
public class TransactionRollbackDemo {
private static Map<String, AccountRow> database = new HashMap<>();
private static TransactionManager txManager = new TransactionManager();
public static void main(String[] args) {
// 初始化数据
database.put("A001", new AccountRow("A001", 1000.0));
database.put("A002", new AccountRow("A002", 500.0));
System.out.println("初始数据: " + database);
try {
txManager.beginTransaction();
AccountRow acc1 = database.get("A001");
AccountRow acc2 = database.get("A002");
// 操作前注册before image
txManager.registerBeforeImage(acc1);
txManager.registerBeforeImage(acc2);
// 执行转账操作
acc1.setBalance(acc1.getBalance() - 200);
acc2.setBalance(acc2.getBalance() + 200);
System.out.println("转账后(未提交): " + database);
// 模拟异常发生
if (true) {
throw new RuntimeException("网络异常,事务失败!");
}
txManager.commit();
} catch (Exception e) {
System.out.println("捕获异常: " + e.getMessage());
txManager.rollback(database);
}
System.out.println("最终数据: " + database);
}
}
Mermaid流程图
flowchart TD
Start(["开始事务"]) --> BeginTx["TransactionManager.beginTransaction"]
BeginTx --> RegBefore["遍历涉及的行,调用 registerBeforeImage"]
RegBefore --> CreateMemento["每个AccountRow创建自己的备忘录"]
CreateMemento --> StoreMemento["备忘录存入 beforeImages Map"]
StoreMemento --> ExecSQL["执行业务操作,修改 AccountRow 状态"]
ExecSQL --> Check{"操作是否成功?"}
Check -->|"成功"| Commit["调用 commit 清除备忘录"]
Commit --> End1(["事务结束"])
Check -->|"失败/异常"| Rollback["调用 rollback"]
Rollback --> Iterate["遍历 beforeImages 中的备忘录"]
Iterate --> Restore["对每个 AccountRow 调用 restoreMemento"]
Restore --> Clear["清除 beforeImages 并结束事务"]
Clear --> End2(["事务回滚完成"])
场景分析与文字说明
本Demo模拟了数据库事务中基于备忘录模式实现ACID特性中**原子性(Atomicity)**的简化版本,其核心思想与真实数据库的Undo日志机制高度一致。
对比数据库Undo日志实现原理:在MySQL InnoDB或PostgreSQL等关系型数据库中,当执行UPDATE语句时,存储引擎会在修改数据页之前,将被修改行的**前镜像(Before Image)**记录到Undo Log(回滚段)中。这个Undo Log正是备忘录模式中的 Memento 对象。在本Demo中,TransactionManager 充当了Undo Log管理器的角色,beforeImages Map就是内存中的Undo日志表。registerBeforeImage() 方法模拟了数据库在执行DML前捕获旧版本数据的过程。当事务需要回滚时(无论是显式ROLLBACK还是由于异常导致),系统遍历Undo Log中的备忘录,并通过 restoreMemento() 将数据行恢复到修改前的状态,从而实现事务的原子性回滚。
备忘录模式在事务ACID中的作用:备忘录模式在此场景中主要保障了原子性(要么全做要么全不做)和一致性(事务前后数据满足完整性约束)。通过保存修改前的快照,系统获得了一种“后悔”的能力——即使业务操作已经修改了内存(或磁盘缓存)中的数据,也能凭借备忘录无损失地还原。值得注意的是,真实数据库的Undo机制远比此Demo复杂,它还需要支持MVCC(多版本并发控制)、崩溃恢复时的前滚与回滚等高级特性,但核心的设计哲学——在不破坏数据页封装的前提下捕获并外部化状态——与备忘录模式如出一辙。
场景四:表单草稿自动保存
Demo代码(前端风格,Java模拟)
import java.util.*;
import java.util.concurrent.*;
// Originator: 表单数据模型
class FormModel {
private String username;
private String email;
private String bio;
private int age;
// getters and setters ...
public FormMemento createMemento() {
return new FormMemento(username, email, bio, age);
}
public void restoreMemento(FormMemento memento) {
this.username = memento.username;
this.email = memento.email;
this.bio = memento.bio;
this.age = memento.age;
}
// 黑箱备忘录
public static class FormMemento {
private final String username, email, bio;
private final int age;
private final long timestamp;
private FormMemento(String u, String e, String b, int a) {
this.username = u;
this.email = e;
this.bio = b;
this.age = a;
this.timestamp = System.currentTimeMillis();
}
public long getTimestamp() { return timestamp; }
// 其他字段私有
}
@Override
public String toString() {
return "FormModel{username='" + username + "', email='" + email + "', bio='" + bio + "', age=" + age + "}";
}
}
// Caretaker: 模拟前端存储(localStorage/IndexedDB的抽象)
class DraftStorage {
// 使用LinkedHashMap实现LRU淘汰策略
private final Map<String, FormModel.FormMemento> storage = new LinkedHashMap<String, FormModel.FormMemento>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, FormModel.FormMemento> eldest) {
return size() > 5; // 最多保存5个草稿,LRU自动淘汰最老的
}
};
public void saveDraft(String key, FormModel.FormMemento memento) {
storage.put(key, memento);
System.out.println("草稿已保存: " + key + " at " + new Date(memento.getTimestamp()));
}
public FormModel.FormMemento loadDraft(String key) {
return storage.get(key);
}
public Set<String> listDrafts() {
return storage.keySet();
}
}
// 模拟防抖自动保存触发器
class AutoSaveScheduler {
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> lastTask;
private final Runnable saveTask;
public AutoSaveScheduler(Runnable saveTask) {
this.saveTask = saveTask;
}
// 防抖:每次用户输入都重新计时,1秒无操作后执行保存
public void trigger() {
if (lastTask != null && !lastTask.isDone()) {
lastTask.cancel(false);
}
lastTask = scheduler.schedule(saveTask, 1, TimeUnit.SECONDS);
}
public void shutdown() {
scheduler.shutdown();
}
}
public class FormAutoSaveDemo {
public static void main(String[] args) throws InterruptedException {
FormModel form = new FormModel();
DraftStorage storage = new DraftStorage();
final String DRAFT_KEY = "user_profile_form";
AutoSaveScheduler autoSave = new AutoSaveScheduler(() -> {
FormModel.FormMemento memento = form.createMemento();
storage.saveDraft(DRAFT_KEY, memento);
});
// 模拟用户输入
form.setUsername("john_doe");
autoSave.trigger();
Thread.sleep(200);
form.setEmail("john@example.com");
autoSave.trigger();
Thread.sleep(200);
form.setBio("Software Developer");
autoSave.trigger();
Thread.sleep(1200); // 等待自动保存触发
// 模拟页面刷新后加载草稿
System.out.println("\n--- 模拟页面刷新 ---");
FormModel.FormMemento draft = storage.loadDraft(DRAFT_KEY);
if (draft != null) {
form.restoreMemento(draft);
System.out.println("草稿恢复: " + form);
}
autoSave.shutdown();
}
}
Mermaid流程图
flowchart TD
A["用户在表单字段输入"] --> B["防抖触发器 debounce"]
B --> C{"1秒内是否有新输入?"}
C -->|"有"| B
C -->|"无"| D["触发自动保存任务"]
D --> E["FormModel.createMemento 创建当前状态快照"]
E --> F["序列化备忘录为JSON字符串"]
F --> G["存入 LocalStorage / IndexedDB"]
G --> H["存储层 LRU 淘汰旧快照"]
I["页面刷新或重新打开"] --> J["从存储读取草稿数据"]
J --> K["反序列化为 FormMemento 对象"]
K --> L["FormModel.restoreMemento 恢复状态"]
L --> M["表单字段回填,用户继续编辑"]
场景分析与文字说明
表单草稿自动保存是现代Web应用中提升用户体验的关键功能,而备忘录模式为此提供了清晰的前端状态管理方案。
前端场景下的序列化与反序列化策略:在真实的前端环境中,JavaScript对象无法直接存入 localStorage 或 IndexedDB,必须经过序列化(通常为JSON格式)。本Demo虽以Java编写,但模拟了这一过程:FormMemento 本质上就是一个数据容器,在存入存储前会被转换为可持久化的格式。在浏览器中,这一步骤由 JSON.stringify() 完成,反序列化则由 JSON.parse() 实现。需要特别注意的是,序列化过程会丢失对象的方法和原型链信息,因此备忘录应当设计为纯数据对象(POJO/POCO),仅包含需要保存的字段。恢复时,FormModel 利用这些数据重新填充自身状态。
存储容量管理与LRU淘汰:由于 localStorage 通常有5-10MB的容量限制,无限制地保存历史草稿可能导致存储溢出。本Demo通过 LinkedHashMap 的 removeEldestEntry 方法实现了LRU(最近最少使用)淘汰策略,当草稿数量超过阈值(如5个)时,最久未被访问的草稿将被自动删除。在生产环境中,还可以结合 IndexedDB 实现更大容量和更复杂的查询能力。此外,自动保存通常结合**防抖(debounce)**技术,避免在用户连续快速输入时频繁触发昂贵的序列化和存储操作,本Demo中的 AutoSaveScheduler 正是对这一机制的模拟。
场景五:代码调试中的断点快照
Demo代码
import java.util.*;
// 模拟程序变量上下文
class DebugContext {
private Map<String, Object> variables = new HashMap<>();
public void setVariable(String name, Object value) {
variables.put(name, value);
}
public Object getVariable(String name) {
return variables.get(name);
}
public DebugMemento createMemento() {
// 创建变量表的深度快照
Map<String, Object> snapshot = new HashMap<>();
for (Map.Entry<String, Object> entry : variables.entrySet()) {
// 模拟基本类型与不可变对象的浅拷贝足够,实际调试器会更复杂
snapshot.put(entry.getKey(), entry.getValue());
}
return new DebugMemento(snapshot);
}
public void restoreMemento(DebugMemento memento) {
this.variables = new HashMap<>(memento.snapshot);
}
public void printVariables() {
System.out.println("Variables: " + variables);
}
// 备忘录
public static class DebugMemento {
private final Map<String, Object> snapshot;
private final long timestamp;
private DebugMemento(Map<String, Object> snapshot) {
this.snapshot = new HashMap<>(snapshot);
this.timestamp = System.currentTimeMillis();
}
public long getTimestamp() { return timestamp; }
private Map<String, Object> getSnapshot() { return snapshot; }
}
}
// Caretaker: 断点快照管理器
class BreakpointManager {
private final Map<Integer, List<DebugContext.DebugMemento>> breakpointSnapshots = new HashMap<>();
private int currentLine = 0;
public void hitBreakpoint(int lineNumber, DebugContext ctx) {
currentLine = lineNumber;
DebugContext.DebugMemento memento = ctx.createMemento();
breakpointSnapshots.computeIfAbsent(lineNumber, k -> new ArrayList<>()).add(memento);
System.out.println("断点命中,行 " + lineNumber + ",变量快照已保存");
}
public List<DebugContext.DebugMemento> getSnapshotsAtLine(int line) {
return breakpointSnapshots.getOrDefault(line, Collections.emptyList());
}
public void compareSnapshots(int line, int index1, int index2) {
List<DebugContext.DebugMemento> snaps = getSnapshotsAtLine(line);
if (snaps.size() <= Math.max(index1, index2)) return;
System.out.println("对比快照 #" + index1 + " 和 #" + index2 + ":");
// 实际实现会比较Map差异...
}
}
public class DebuggerSnapshotDemo {
public static void main(String[] args) {
DebugContext ctx = new DebugContext();
BreakpointManager bpManager = new BreakpointManager();
// 模拟程序执行
ctx.setVariable("i", 0);
ctx.setVariable("sum", 0);
bpManager.hitBreakpoint(10, ctx); // 第一次命中断点
// 执行一些操作
ctx.setVariable("i", 5);
ctx.setVariable("sum", 15);
bpManager.hitBreakpoint(10, ctx); // 循环中再次命中同一断点
ctx.setVariable("i", 10);
ctx.setVariable("sum", 55);
bpManager.hitBreakpoint(10, ctx);
// 查看历史快照
List<DebugContext.DebugMemento> history = bpManager.getSnapshotsAtLine(10);
System.out.println("断点行10共有 " + history.size() + " 个历史快照");
for (int i = 0; i < history.size(); i++) {
System.out.println("快照 #" + i + " 时间戳: " + history.get(i).getTimestamp());
}
}
}
Mermaid流程图
flowchart TD
A["程序开始执行"] --> B["解释器/虚拟机逐行执行字节码"]
B --> C{"当前行是否设置了断点?"}
C -->|"否"| B
C -->|"是"| D["触发断点事件,挂起所有线程"]
D --> E["调试器接口 JVM TI 捕获当前栈帧"]
E --> F["遍历当前作用域内所有变量"]
F --> G["为每个变量创建值快照 处理对象引用深度"]
G --> H["组装 DebugMemento 备忘录对象"]
H --> I["将备忘录添加到断点管理器的历史列表"]
I --> J["IDE 界面展示变量值"]
J --> K{"用户操作"}
K -->|"继续执行"| B
K -->|"查看历史快照"| L["从 BreakpointManager 获取指定版本备忘录"]
L --> M["在变量视图中渲染备忘录数据"]
M --> K
场景分析与文字说明
IDE(如IntelliJ IDEA、Eclipse)的调试器提供了强大的断点变量查看与历史对比功能,其底层正是备忘录模式的深度应用。
JVM TI与调试器接口中的状态捕获原理:Java调试器依赖于 JVM Tool Interface (JVM TI) 这一底层接口。当程序执行到断点时,JVM会暂停所有应用线程,并通过JVM TI向调试器前端发送事件。调试器随后可以通过JVM TI查询当前线程的调用栈、每个栈帧的局部变量表、以及堆上对象的字段值。这个过程本质上是在捕获程序执行状态的快照——即创建备忘录。在本Demo中,DebugContext 模拟了一个简化的变量上下文,createMemento() 方法遍历所有变量并将其值存入一个不可变Map中。真实场景更为复杂:调试器需要处理对象引用图、防止无限递归、处理大对象的延迟加载等。JVM TI提供了分层的内存访问API,调试器可以按需获取字段,这类似于一种懒加载的备忘录。
备忘录模式在IDE调试功能中的应用价值:现代IDE(如IntelliJ IDEA的"Memory View"或"Jump to Line"时的状态)允许开发者在多个断点命中之间切换,查看不同时刻的变量状态,甚至进行值对比。这正是 BreakpointManager 维护历史备忘录列表的价值所在。通过保存每次断点命中时的状态快照,调试器可以:
- 支持历史回溯:在不重新运行程序的情况下回顾之前的变量状态。
- 支持状态对比:快速定位两次循环迭代间变量的差异,辅助发现逻辑错误。
- 支持表达式评估:基于某个历史快照的上下文计算表达式结果。
备忘录模式在此场景中完美地将“状态捕获”与“程序执行”解耦,使得调试器能够在不干扰程序正常逻辑的前提下,自由地检查、记录和比较任意时间点的程序状态。
七、面试题精选与专家级解答
Q1: 备忘录模式如何在不破坏封装的前提下保存和恢复对象状态?
专家解答:备忘录模式通过引入一个专门的 Memento 对象来承载状态数据,并利用语言特性(如Java的内部类、包级私有访问控制)实现封装保护。具体而言:
- 职责分离:
Originator全权负责创建备忘录和从备忘录恢复,它知道备忘录的内部结构。 - 窄接口与宽接口:
Memento对外部(特别是Caretaker)仅暴露窄接口——通常只有无参构造或极少的元数据方法,使得Caretaker只能传递备忘录而不能访问其内容。对Originator则暴露宽接口(如包级私有的getter),允许其全面读取状态。 - Java实现技巧:将
Memento定义为Originator的private static内部类,所有字段为private final,访问器为private或包级私有。这样,Caretaker只能持有Memento的引用,却无法调用任何修改或访问其状态的方法,从而完美保护了Originator的封装边界。
Q2: 备忘录模式与深拷贝实现状态保存有什么区别?各自的优缺点是什么?
专家解答:
| 对比维度 | 备忘录模式 | 深拷贝 + 列表管理 |
|---|---|---|
| 封装性 | 高。状态保存逻辑封装在Originator内部 | 低。外部必须知道对象所有字段才能正确拷贝 |
| 耦合度 | 低。Caretaker与Originator内部结构解耦 | 高。客户端与对象字段强耦合 |
| 灵活性 | 高。可选择性保存部分状态,支持增量快照 | 低。通常只能全量拷贝 |
| 性能 | 可优化(如只保存变更部分) | 全量深拷贝开销大,易产生大量临时对象 |
| 复杂度 | 需要额外设计Memento类和Caretaker | 实现简单直接 |
优缺点总结:备忘录模式是面向对象设计的产物,强调职责与封装;深拷贝是技术手段,虽简单但牺牲了可维护性。在企业级应用中,应优先使用备忘录模式或其变体(如结合序列化)。
Q3: 命令模式如何与备忘录模式配合实现可撤销的操作?请简述协作流程。
专家解答:命令模式与备忘录模式的协作是实现可撤销操作的经典范式,流程如下:
- 命令执行前:客户端创建具体命令对象(
ConcreteCommand),命令对象在执行execute()方法前,首先向Originator请求当前的备忘录:Memento m = originator.createMemento();,并将该备忘录保存在命令对象内部。 - 命令执行:命令对象调用
originator.businessMethod()修改状态。 - 撤销操作:客户端调用命令对象的
undo()方法。undo()方法内部将之前保存的备忘录交还给发起人:originator.restoreMemento(m);,从而将状态恢复至命令执行前。 - 重做操作:若需重做,可再次执行
execute()(但通常会重新生成备忘录或使用另一个栈管理)。
这种协作中,命令对象充当了临时 Caretaker 的角色,将状态快照与操作绑定在一起。
Q4: JDK中哪些地方使用了备忘录模式?请至少列举三个并分析实现细节。
专家解答:
java.io.Serializable:序列化机制本质上是将对象状态转换为字节流(备忘录)并外部化。ObjectOutputStream.writeObject()是createMemento(),ObjectInputStream.readObject()是restoreMemento()。字节数组或文件充当Caretaker。javax.swing.undo.UndoManager与UndoableEdit:UndoableEdit接口的实现类(如AbstractDocument.DefaultDocumentEvent)记录了文档编辑前后的状态差异,是备忘录。UndoManager管理这些编辑事件的列表,并提供undo()/redo(),是Caretaker。JTextComponent的Document是Originator。- 不可变类(如
String,Integer,LocalDate):不可变对象天然具备备忘录的特性——一旦创建,其状态就固定不变。当需要保存某个时刻的状态时,只需持有该对象的引用即可,因为其内容永远不会被改变。这可以看作是一种极简的、零拷贝的备忘录实现。
Q5: 如何优化备忘录模式的内存占用?请列举几种优化策略。
专家解答:
- 增量快照:不保存对象的全量状态,而只保存与上一个版本的差异(Delta)。例如,对于文档编辑器,仅记录插入/删除的字符串和位置,而非整个文本。
- 共享不可变对象:如果状态中包含大量不可变对象(如字符串常量),可利用享元模式共享这些对象,备忘录只存储引用。
- 压缩存储:对于序列化后的备忘录数据(如JSON或字节流),进行GZIP压缩后再存入内存或磁盘。
- 弱引用(WeakReference):对于非关键的历史状态(如很久以前的撤销记录),
Caretaker可使用WeakReference持有备忘录。当JVM内存紧张时,这些早期快照会被自动回收,牺牲历史深度换取内存安全。 - 持久化到外部存储:将较旧的备忘录持久化到磁盘或远程缓存(Redis),仅在内存中保留最近几个版本。需要时再从外部存储加载。
Q6: 在分布式系统中,如何利用备忘录模式实现跨服务的状态回滚?请举例说明。
专家解答:在分布式系统中,可利用共享存储(如Redis、数据库)作为中心化的 Caretaker 来实现跨服务的状态回滚。
实例:一个电商订单创建流程涉及订单服务、库存服务、支付服务。采用Saga模式或Seata AT模式。
- Seata AT模式:在每个本地事务提交前,Seata会解析SQL,生成前置快照(Before Image),并作为备忘录存入全局的
undo_log表(该表可被所有服务访问)。当全局事务协调器决定回滚时,它会通知各参与者服务。各服务从undo_log中读取对应的备忘录,解析并执行反向补偿SQL(例如,将库存数量恢复为快照中的值),从而实现跨服务的数据一致性回滚。这里的undo_log表就是分布式的Caretaker。
Q7: Spring StateMachine中的状态持久化是如何体现备忘录模式的?
专家解答:Spring StateMachine 支持将状态机的当前状态持久化到外部存储(如Redis、JPA),并在之后恢复执行。其核心机制正是备忘录模式:
- Originator:
AbstractStateMachine实例,包含当前状态、历史状态、扩展上下文变量。 - Memento:
DefaultStateMachineContext对象,它包含了状态机在某一时刻的完整快照:state、event、extendedState变量等。这个上下文对象是可序列化的。 - Caretaker:
StateMachinePersister接口的实现类(如RedisStateMachinePersister)。它负责将StateMachineContext序列化并存储到Redis,并在需要时读取、反序列化,然后通过restore()方法将状态机恢复到该上下文所代表的时刻。这使得状态机实例可以在服务重启或跨节点迁移后,继续从断点处执行。
Q8: 白箱备忘录和黑箱备忘录有何区别?在Java中分别如何实现?
专家解答:
| 特性 | 白箱备忘录 | 黑箱备忘录 |
|---|---|---|
| 封装性 | 差,备忘录对外暴露状态访问接口 | 好,备忘录对外界完全隐藏内部状态 |
| 实现方式 | 独立的公共类,提供 public 的 getState() 方法 | 作为 Originator 的静态内部类,构造器和访问器均为 private 或包级私有 |
| 使用效果 | Caretaker 可以直接读取备忘录内容,耦合度高,但便于调试和序列化 | Caretaker 只能持有引用,完全不能访问内容,保护了 Originator 的实现细节 |
| Java示例 | public class Memento { private String state; public String getState() { return state; } } | private static class Memento { private final String state; private Memento(String s){...} private String getState(){...} } |
在实际开发中,若需要将备忘录序列化到JSON或数据库(如分布式场景),数据必然需要被读取,此时倾向于使用白箱备忘录或通过反射/序列化框架绕过封装。但若仅在单机内存中使用,应优先考虑黑箱备忘录以维护良好的面向对象设计。
Q9: Git版本控制系统是如何体现备忘录模式的?commit对象、工作区、暂存区分别对应哪些角色?
专家解答:Git是备忘录模式在分布式版本控制系统中的典范应用,角色映射清晰:
- Originator(发起人):工作区(Working Directory) 和 暂存区(Staging Area / Index) 共同构成当前工作状态。开发者通过修改文件改变工作区状态。
- Memento(备忘录):Commit对象。每个commit包含一个指向项目文件树快照的指针(tree对象)、父commit指针、作者信息、提交信息。它记录了项目在某个时刻的完整状态快照,且commit对象是不可变的(内容哈希确定),完美符合备忘录的不可变性要求。
- Caretaker(管理者):.git目录。
.git/objects目录下存储了所有的commit对象、tree对象和blob对象,构成了一个内容寻址的键值对数据库。git checkout <commit-hash>就是从.git数据库中取出指定的commit备忘录,并用它覆盖工作区的过程。 - 辅助角色:
HEAD指针、分支指针等相当于Caretaker用于追踪当前使用的是哪个备忘录的“书签”。
Q10: 在高并发场景下使用备忘录模式需要注意哪些线程安全问题?如何设计线程安全的备忘录管理器?
专家解答:
-
问题分析:
Originator的状态修改操作(如setState)若被多线程并发调用,可能导致状态不一致。Caretaker内部的备忘录集合(如List、Stack)若不加锁,在并发push/pop时会抛出ConcurrentModificationException或导致数据错乱。- 备忘录对象本身应是不可变的(Immutable),否则一个线程修改了共享的备忘录对象,会影响其他线程的恢复结果。
-
线程安全设计策略:
- 不可变备忘录:将
Memento类的所有字段声明为final,并且在构造时进行防御性拷贝(对于可变对象字段)。确保备忘录一旦创建便无法修改。 - 使用并发集合:
Caretaker内部使用ConcurrentLinkedDeque(双端队列)或CopyOnWriteArrayList代替普通的Stack/ArrayList,或使用Collections.synchronizedList()包装。 - 显式锁控制:对于复杂的多步操作(如先检查后保存),可使用
ReentrantReadWriteLock。读多写少的场景(如查看历史版本)适合用读写锁优化。 - ThreadLocal封闭:如果每个线程都有自己独立的操作上下文(如Web应用中的用户会话),可将
Originator和Caretaker实例存储在ThreadLocal或 Session 中,从根本上避免跨线程竞争。 - 原子操作:
Caretaker提供原子性的复合操作接口,例如saveAndTrim(),内部使用synchronized关键字确保操作的原子性。
- 不可变备忘录:将
八、总结
备忘录模式以其优雅的封装设计和强大的状态回溯能力,在软件开发中占据着不可替代的地位。从单机应用的内存撤销栈,到分布式事务的全局回滚日志;从IDE的调试变量快照,到Git的版本控制哲学——备忘录模式的思想无处不在。掌握这一模式,不仅意味着理解三个角色的协作关系,更意味着培养一种“关注状态生命周期管理”的架构思维。希望本文从理论、演进、源码、分布式到实战的全方位剖析,能够助你成为备忘录模式乃至状态管理领域的专家级开发者。