行为型模式-备忘录模式

3 阅读46分钟

概述

备忘录模式(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);
    }
}

问题分析

  1. 封装破坏Document 类被迫暴露了全部 getter/setter,外部代码可以直接读取和修改其内部状态,这与面向对象的封装原则背道而驰。
  2. 高耦合:客户端必须清楚了解 Document 的所有字段,并手动编写字段逐个复制的恢复逻辑。一旦 Document 增加新字段(如 boolean isBold),所有涉及深拷贝和恢复的客户端代码都必须同步修改,维护噩梦。
  3. 职责不清:历史记录的管理逻辑直接耦合在客户端代码中,违反了单一职责原则。

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 对象被创建后返回给客户端,客户端并不解析其内容,而是立即将其传递给 CaretakersaveState() 方法,由 Caretaker 将其存入内部集合(如栈、列表或Map)中进行持久化管理。

阶段二:状态恢复(步骤7-12)。当需要回滚时,客户端向 Caretaker 请求一个历史备忘录。Caretaker 从集合中取出相应的 Memento 对象并返回。客户端随即调用 OriginatorrestoreMemento(memento) 方法,并将备忘录作为参数传入。此时,封装性的关键体现在于:只有 Originator 知道自己曾经往 Memento 里存了什么,也只有它能通过调用 Memento 的私有或包级私有访问器(如 getState())来提取状态数据。Originator 获取到这些数据后,用它们覆盖自身的当前状态,最终完成恢复并向客户端确认。

整个过程中,Caretaker 仅仅是 Memento 对象的“搬运工”,它从未(也无法)窥视 Memento 的内容,从而将状态管理的复杂度完全封装在 OriginatorMemento 的内部。

三、源码级应用分析

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(),从字节序列重建对象状态。ObjectOutputStreamByteArrayOutputStream 共同扮演了 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,它通过 UndoableEditundo()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中,相当于一个跨请求的备忘录管理器。RedirectAttributesaddFlashAttribute 则利用了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 命令模式

命令模式与备忘录模式经常配合实现可撤销操作。命令模式将“操作请求”封装为对象,而备忘录模式提供“操作结果状态”的保存。典型协作流程:

  1. 客户端调用命令对象的 execute() 前,命令对象主动向 Originator 请求一个 Memento 并自己持有。
  2. execute() 执行,修改 Originator 状态。
  3. 需要撤销时,调用命令对象的 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通过双栈结构(undoStackredoStack)实现了标准的多级撤销与重做功能。

双栈结合备忘录模式的原理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对象无法直接存入 localStorageIndexedDB,必须经过序列化(通常为JSON格式)。本Demo虽以Java编写,但模拟了这一过程:FormMemento 本质上就是一个数据容器,在存入存储前会被转换为可持久化的格式。在浏览器中,这一步骤由 JSON.stringify() 完成,反序列化则由 JSON.parse() 实现。需要特别注意的是,序列化过程会丢失对象的方法和原型链信息,因此备忘录应当设计为纯数据对象(POJO/POCO),仅包含需要保存的字段。恢复时,FormModel 利用这些数据重新填充自身状态。

存储容量管理与LRU淘汰:由于 localStorage 通常有5-10MB的容量限制,无限制地保存历史草稿可能导致存储溢出。本Demo通过 LinkedHashMapremoveEldestEntry 方法实现了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 维护历史备忘录列表的价值所在。通过保存每次断点命中时的状态快照,调试器可以:

  1. 支持历史回溯:在不重新运行程序的情况下回顾之前的变量状态。
  2. 支持状态对比:快速定位两次循环迭代间变量的差异,辅助发现逻辑错误。
  3. 支持表达式评估:基于某个历史快照的上下文计算表达式结果。

备忘录模式在此场景中完美地将“状态捕获”与“程序执行”解耦,使得调试器能够在不干扰程序正常逻辑的前提下,自由地检查、记录和比较任意时间点的程序状态。

七、面试题精选与专家级解答

Q1: 备忘录模式如何在不破坏封装的前提下保存和恢复对象状态?

专家解答:备忘录模式通过引入一个专门的 Memento 对象来承载状态数据,并利用语言特性(如Java的内部类、包级私有访问控制)实现封装保护。具体而言:

  1. 职责分离Originator 全权负责创建备忘录和从备忘录恢复,它知道备忘录的内部结构。
  2. 窄接口与宽接口Memento 对外部(特别是 Caretaker)仅暴露窄接口——通常只有无参构造或极少的元数据方法,使得 Caretaker 只能传递备忘录而不能访问其内容。对 Originator 则暴露宽接口(如包级私有的getter),允许其全面读取状态。
  3. Java实现技巧:将 Memento 定义为 Originatorprivate static 内部类,所有字段为 private final,访问器为 private 或包级私有。这样,Caretaker 只能持有 Memento 的引用,却无法调用任何修改或访问其状态的方法,从而完美保护了 Originator 的封装边界。

Q2: 备忘录模式与深拷贝实现状态保存有什么区别?各自的优缺点是什么?

专家解答

对比维度备忘录模式深拷贝 + 列表管理
封装性。状态保存逻辑封装在Originator内部。外部必须知道对象所有字段才能正确拷贝
耦合度。Caretaker与Originator内部结构解耦。客户端与对象字段强耦合
灵活性。可选择性保存部分状态,支持增量快照。通常只能全量拷贝
性能可优化(如只保存变更部分)全量深拷贝开销大,易产生大量临时对象
复杂度需要额外设计Memento类和Caretaker实现简单直接

优缺点总结:备忘录模式是面向对象设计的产物,强调职责与封装;深拷贝是技术手段,虽简单但牺牲了可维护性。在企业级应用中,应优先使用备忘录模式或其变体(如结合序列化)。

Q3: 命令模式如何与备忘录模式配合实现可撤销的操作?请简述协作流程。

专家解答:命令模式与备忘录模式的协作是实现可撤销操作的经典范式,流程如下:

  1. 命令执行前:客户端创建具体命令对象(ConcreteCommand),命令对象在执行 execute() 方法前,首先向 Originator 请求当前的备忘录:Memento m = originator.createMemento();,并将该备忘录保存在命令对象内部。
  2. 命令执行:命令对象调用 originator.businessMethod() 修改状态。
  3. 撤销操作:客户端调用命令对象的 undo() 方法。undo() 方法内部将之前保存的备忘录交还给发起人:originator.restoreMemento(m);,从而将状态恢复至命令执行前。
  4. 重做操作:若需重做,可再次执行 execute()(但通常会重新生成备忘录或使用另一个栈管理)。

这种协作中,命令对象充当了临时 Caretaker 的角色,将状态快照与操作绑定在一起。

Q4: JDK中哪些地方使用了备忘录模式?请至少列举三个并分析实现细节。

专家解答

  1. java.io.Serializable:序列化机制本质上是将对象状态转换为字节流(备忘录)并外部化。ObjectOutputStream.writeObject()createMemento()ObjectInputStream.readObject()restoreMemento()。字节数组或文件充当 Caretaker
  2. javax.swing.undo.UndoManagerUndoableEditUndoableEdit 接口的实现类(如 AbstractDocument.DefaultDocumentEvent)记录了文档编辑前后的状态差异,是备忘录。UndoManager 管理这些编辑事件的列表,并提供 undo()/redo(),是 CaretakerJTextComponentDocumentOriginator
  3. 不可变类(如 String, Integer, LocalDate:不可变对象天然具备备忘录的特性——一旦创建,其状态就固定不变。当需要保存某个时刻的状态时,只需持有该对象的引用即可,因为其内容永远不会被改变。这可以看作是一种极简的、零拷贝的备忘录实现。

Q5: 如何优化备忘录模式的内存占用?请列举几种优化策略。

专家解答

  1. 增量快照:不保存对象的全量状态,而只保存与上一个版本的差异(Delta)。例如,对于文档编辑器,仅记录插入/删除的字符串和位置,而非整个文本。
  2. 共享不可变对象:如果状态中包含大量不可变对象(如字符串常量),可利用享元模式共享这些对象,备忘录只存储引用。
  3. 压缩存储:对于序列化后的备忘录数据(如JSON或字节流),进行GZIP压缩后再存入内存或磁盘。
  4. 弱引用(WeakReference):对于非关键的历史状态(如很久以前的撤销记录),Caretaker 可使用 WeakReference 持有备忘录。当JVM内存紧张时,这些早期快照会被自动回收,牺牲历史深度换取内存安全。
  5. 持久化到外部存储:将较旧的备忘录持久化到磁盘或远程缓存(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),并在之后恢复执行。其核心机制正是备忘录模式:

  • OriginatorAbstractStateMachine 实例,包含当前状态、历史状态、扩展上下文变量。
  • MementoDefaultStateMachineContext 对象,它包含了状态机在某一时刻的完整快照:stateeventextendedState 变量等。这个上下文对象是可序列化的。
  • CaretakerStateMachinePersister 接口的实现类(如 RedisStateMachinePersister)。它负责将 StateMachineContext 序列化并存储到Redis,并在需要时读取、反序列化,然后通过 restore() 方法将状态机恢复到该上下文所代表的时刻。这使得状态机实例可以在服务重启或跨节点迁移后,继续从断点处执行。

Q8: 白箱备忘录和黑箱备忘录有何区别?在Java中分别如何实现?

专家解答

特性白箱备忘录黑箱备忘录
封装性差,备忘录对外暴露状态访问接口好,备忘录对外界完全隐藏内部状态
实现方式独立的公共类,提供 publicgetState() 方法作为 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: 在高并发场景下使用备忘录模式需要注意哪些线程安全问题?如何设计线程安全的备忘录管理器?

专家解答

  1. 问题分析

    • Originator 的状态修改操作(如 setState)若被多线程并发调用,可能导致状态不一致。
    • Caretaker 内部的备忘录集合(如 ListStack)若不加锁,在并发 push/pop 时会抛出 ConcurrentModificationException 或导致数据错乱。
    • 备忘录对象本身应是不可变的(Immutable),否则一个线程修改了共享的备忘录对象,会影响其他线程的恢复结果。
  2. 线程安全设计策略

    • 不可变备忘录:将 Memento 类的所有字段声明为 final,并且在构造时进行防御性拷贝(对于可变对象字段)。确保备忘录一旦创建便无法修改。
    • 使用并发集合Caretaker 内部使用 ConcurrentLinkedDeque(双端队列)或 CopyOnWriteArrayList 代替普通的 Stack/ArrayList,或使用 Collections.synchronizedList() 包装。
    • 显式锁控制:对于复杂的多步操作(如先检查后保存),可使用 ReentrantReadWriteLock。读多写少的场景(如查看历史版本)适合用读写锁优化。
    • ThreadLocal封闭:如果每个线程都有自己独立的操作上下文(如Web应用中的用户会话),可将 OriginatorCaretaker 实例存储在 ThreadLocal 或 Session 中,从根本上避免跨线程竞争。
    • 原子操作Caretaker 提供原子性的复合操作接口,例如 saveAndTrim(),内部使用 synchronized 关键字确保操作的原子性。

八、总结

备忘录模式以其优雅的封装设计和强大的状态回溯能力,在软件开发中占据着不可替代的地位。从单机应用的内存撤销栈,到分布式事务的全局回滚日志;从IDE的调试变量快照,到Git的版本控制哲学——备忘录模式的思想无处不在。掌握这一模式,不仅意味着理解三个角色的协作关系,更意味着培养一种“关注状态生命周期管理”的架构思维。希望本文从理论、演进、源码、分布式到实战的全方位剖析,能够助你成为备忘录模式乃至状态管理领域的专家级开发者。