🤯 前言:为什么这玩意儿这么难?

想象一个场景:
文档内容是 "Hello World"

  • 用户 A 想在位置 0 插入 "Java " -> 变成 "Java Hello World"
  • 用户 B 同时想在位置 6 删除 "World" -> 变成 "Hello "

如果没有算法介入,服务器只是简单地执行这两个命令:

  1. A 执行完:"Java Hello World"
  2. B 的命令是“删除位置 6 的 5 个字符”。
  3. 结果:删掉了 "Hello",变成了 "Java World"
    错啦!用户 B 明明想删的是 "World"

这就是并发意图冲突。要解决它,必须引入 OT 算法


🧠 一、 核心魔法:OT 算法图解

OT 的核心思想是:基于已经发生的操作,转换(Transform)当前的操作。

如果服务器先执行了 A 的“插入”,那么 B 的“删除”操作必须发生变化
因为 A 在前面插入了 5 个字符("Java "),所以 B 的删除位置必须 +5

OT 转换流程图 (Mermaid):

服务端 OT 引擎

用户 B 操作

用户 A 操作

先到达

基于 OpA 进行转换

生成新操作

初始状态: 'AB'

OpA: Insert 'X' at 0

本地状态: 'XAB'

OpB: Delete 'B' at 1

本地状态: 'A'

应用 OpA

中间状态: 'XAB'

Transform(OpB, OpA)

OpB': Delete 'B' at 2 (1+1)

应用 OpB'

最终一致状态: 'XA'

公式表达:
意思是:在 已经执行的情况下,把 转换成 ,使其能达到预期的效果。


🏗️ 二、 系统架构设计

我们需要一个全双工的通信通道,WebSocket 是不二之选。

  1. Frontend: 监听用户输入,生成 Operation (Insert/Delete),发送给后端。同时接收后端推过来的 Operation 并应用。
  2. Backend (Spring Boot): 维护文档的版本号 (Version)操作历史 (History)
  3. OT Engine: 核心算法层,负责“变造”操作。

💻 三、 硬核代码:手写 OT 算法 (Java 版)

为了演示,我们简化模型,只处理两种操作:InsertDelete

定义操作对象:

public class Operation {
    public enum Type { INSERT, DELETE }
    public Type type;
    public int position;
    public String text; // 插入的内容
    public int length;  // 删除的长度
    public int version; // 基于哪个版本发出的操作
}

OT 转换核心逻辑 (Transform):

public static Operation transform(Operation opB, Operation opA) {
    // 复制一份 opB 作为结果
    Operation opBPrime = new Operation(opB);

    if (opA.type == Operation.Type.INSERT) {
        // Case 1: A 插入,B 插入/删除
        // 如果 A 插入的位置在 B 操作位置之前,B 的位置需要后移
        if (opA.position <= opB.position) {
            opBPrime.position = opB.position + opA.text.length();
        }
    } else if (opA.type == Operation.Type.DELETE) {
        // Case 2: A 删除,B 插入/删除
        // 如果 A 删除的位置在 B 之前
        if (opA.position < opB.position) {
            // 需要减去 A 删除的长度,但要处理重叠情况(这里简化处理)
            opBPrime.position = Math.max(opA.position, opB.position - opA.length);
        }
    }
    return opBPrime;
}

注:真实场景的 OT 还要处理 Retain(保持)操作和复杂的字符串重叠逻辑,这里展示的是核心灵魂。


🔌 四、 Spring Boot WebSocket 集成

1. 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2. WebSocket 处理器

我们需要处理并发。当多个用户同时发来操作时,必须排队处理(使用 synchronized 或单线程队列)。

@Component
public class DocHandler extends TextWebSocketHandler {

    // 模拟数据库:存储文档内容
    private StringBuilder document = new StringBuilder("");
    // 存储操作历史,用于版本对齐
    private List<Operation> history = new CopyOnWriteArrayList<>();
    // 当前文档版本
    private AtomicInteger version = new AtomicInteger(0);
    
    // 所有的 Session
    private List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1. 解析客户端发来的 Operation
        Operation clientOp = json.parse(message.getPayload());

        synchronized (this) {
            // 2. 版本检查 (关键步骤!)
            // 如果客户端的版本(clientOp.version) < 当前服务器版本(this.version)
            // 说明服务器已经处理了其他人的操作,客户端落后了
            if (clientOp.version < this.version.get()) {
                // 3. 追溯历史,进行连续变换
                for (int i = clientOp.version; i < this.version.get(); i++) {
                    Operation pastOp = history.get(i);
                    // 核心:把客户端的过时操作,变成新操作
                    clientOp = OTUtils.transform(clientOp, pastOp);
                }
            }

            // 4. 应用操作到文档
            applyToDocument(clientOp);

            // 5. 记录历史,版本+1
            history.add(clientOp);
            this.version.incrementAndGet();

            // 6. 广播给所有人 (包括发送者,确认操作已生效)
            broadcast(clientOp);
        }
    }

    private void broadcast(Operation op) {
        // 发送 JSON: { type: "BROADCAST", op: ... }
        // 前端收到后,根据 op 更新自己的编辑器
    }
}


🧩 五、 前端的挑战:光标不能乱跳

后端做好了,前端其实更难。前端通常使用 CodeMirrorMonaco Editor
当后端发来一个 Insert 指令时,你不能直接 setValue(newText),那样用户的光标会回到开头。

你需要使用编辑器提供的 API:

  • Monaco: executeEdits
  • CodeMirror: replaceRange

同时,前端也需要一套 OT 逻辑,来处理“我发出的操作还在路上,服务器又推来了一个新操作”的 Pending 状态


🎯 总结与进阶

通过 Spring Boot + WebSocket + 简易 OT,我们实现了一个多人文档的雏形。但要做到 Google Docs 的级别,还有几座大山:

  1. GOTO 算法 (Inclusion Transformation): 处理更复杂的网状并发。
  2. CRDT (无冲突复制数据类型): 现代协同软件(如 Figma)更倾向于用 CRDT 代替 OT,因为它去中心化,但由于数据结构膨胀,文本编辑场景下 OT 依然是王。
  3. 光标协同: 把别人的光标位置也广播出来,显示名字标签。

面试必杀技:
如果你能在面试中讲清楚 “客户端版本落后时,如何利用 OT 算法追溯历史操作进行 Transform”,面试官绝对会对你刮目相看。这是真正的技术深水区。

Next Step:
不要只看,去 GitHub 找一个开源的 OT 库(如 ot-java),把它塞进你的 Spring Boot 项目里,看着两个浏览器窗口里的字同步跳动,那种成就感无与伦比!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐