当前位置: 首页 > news >正文

Go 协作文档冲突解决:OT 算法和 CRDT 的并发编辑实现

Go 协作文档冲突解决:OT 算法和 CRDT 的并发编辑实现

一、两个人同时改同一行,保存后其中一个人的修改丢了

协作文档(类似 Google Docs/飞书文档)的核心技术挑战是并发编辑冲突。当用户 A 在第 5 行插入"项目延期了",用户 B 正在第 5 行删除"进度正常",两个操作几乎同时到达服务器。如果简单按到达顺序处理,必然有一个人的操作被覆盖。

解决这个问题的经典方案有两种:OT(Operational Transformation,操作变换)和 CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)。OT 通过变换操作保证一致性,CRDT 通过数据结构设计保证操作可交换。两者在工程上有不同的取舍。

二、OT 算法的核心原理

OT 的核心想法是:当两个并发操作冲突时,不是拒绝其中一个,而是"变换"其中一个操作,使其在另一个操作之后执行仍能产生正确的结果。

OT 的核心是transform(op1, op2)函数,它对操作做位置偏移变换。这个算法在 Google Docs 中使用多年,成熟稳定。

三、Go 实现:简化的 OT 引擎

package ot import ( "fmt" "sync" "time" ) // OpType 操作类型 type OpType int const ( OpInsert OpType = iota OpDelete ) // Operation 编辑操作 type Operation struct { Type OpType `json:"type"` Position int `json:"position"` // 操作位置 Content string `json:"content"` // 插入的文本(Delete 时为空) Length int `json:"length"` // 删除的长度(Insert 时为0) UserID string `json:"user_id"` Timestamp int64 `json:"timestamp"` Revision int `json:"revision"` // 基于的版本号 } // Document 协作文档 type Document struct { mu sync.RWMutex content string revision int history []*Operation } // NewDocument 创建文档 func NewDocument(content string) *Document { return &Document{ content: content, revision: 0, history: make([]*Operation, 0), } } // Apply 应用一个操作到文档 func (d *Document) Apply(op *Operation) error { d.mu.Lock() defer d.mu.Unlock() // 版本检查 if op.Revision != d.revision { return fmt.Errorf( "版本冲突: 期望 %d, 当前 %d", op.Revision, d.revision, ) } switch op.Type { case OpInsert: if op.Position < 0 || op.Position > len(d.content) { return fmt.Errorf("插入位置越界: %d", op.Position) } d.content = d.content[:op.Position] + op.Content + d.content[op.Position:] case OpDelete: if op.Position < 0 || op.Position+op.Length > len(d.content) { return fmt.Errorf("删除范围越界") } d.content = d.content[:op.Position] + d.content[op.Position+op.Length:] } d.revision++ d.history = append(d.history, op) return nil } // Transform 变换两个并发操作 // 返回变换后的 op2(使 op2 在 op1 之后仍正确) func Transform(op1, op2 *Operation) (*Operation, error) { if op1.Position > op2.Position { // op1 在 op2 之后,不影响 op2 的位置 return op2, nil } transformed := &Operation{ Type: op2.Type, UserID: op2.UserID, Timestamp: op2.Timestamp, Revision: op2.Revision, } switch op1.Type { case OpInsert: // op1 在 op2 前面插入,op2 的位置需要后移 transformed.Position = op2.Position + len([]rune(op1.Content)) transformed.Content = op2.Content transformed.Length = op2.Length case OpDelete: if op1.Position+op1.Length <= op2.Position { // op1 删除的内容完全在 op2 前面 transformed.Position = op2.Position - op1.Length } else if op1.Position >= op2.Position+op2.Length { // op1 删除的内容完全在 op2 后面,不影响 transformed.Position = op2.Position } else { return nil, fmt.Errorf( "操作冲突: op1删除范围与op2重叠", ) } transformed.Content = op2.Content transformed.Length = op2.Length } return transformed, nil } // OTEngine OT 引擎(服务端) type OTEngine struct { documents sync.Map // docID -> *Document } // NewOTEngine 创建 OT 引擎 func NewOTEngine() *OTEngine { return &OTEngine{} } // HandleOperation 处理客户端发来的操作 func (e *OTEngine) HandleOperation( docID string, op *Operation, ) (*Operation, bool, error) { docInterface, _ := e.documents.LoadOrStore( docID, NewDocument(""), ) doc := docInterface.(*Document) doc.mu.Lock() // 检查是否需要变换 if op.Revision < doc.revision { // 客户端的版本落后,需要对操作做变换 pendingOps := doc.history[op.Revision:] for _, pendingOp := range pendingOps { var err error op, err = Transform(pendingOp, op) if err != nil { doc.mu.Unlock() return nil, false, fmt.Errorf( "操作变换失败: %w", err, ) } } op.Revision = doc.revision } // 应用操作 if err := doc.Apply(op); err != nil { doc.mu.Unlock() return nil, false, err } doc.mu.Unlock() // 返回变换后的操作(用于同步给其他客户端) return op, true, nil } // GetContent 获取文档内容 func (e *OTEngine) GetContent(docID string) string { docInterface, ok := e.documents.Load(docID) if !ok { return "" } doc := docInterface.(*Document) doc.mu.RLock() defer doc.mu.RUnlock() return doc.content }

四、边界分析与 Trade-offs

OT vs CRDT 的选择:OT 需要中心服务器(Google Docs 模式),适合对一致性要求极高的文档协作。CRDT 支持离线编辑后再同步(Figma 模式),适合需要离线能力的场景。但 CRDT 的存储开销大(需要保留所有历史操作),且某些复杂操作(如富文本格式)的 CRDT 实现极其复杂。企业文档协作场景下 OT 是更务实的选择。

操作粒度的影响:按"字符"做 OT 变换会导致操作极多(一次粘贴可能产生几百个 Insert 操作)。实际使用中通常按"词"或"块"做操作合并——客户端将连续插入合并为一个操作后再发给服务端。但这可能导致变量——如果两个用户在不同位置修改同一个"词块",仍然需要 OT 处理。

版本管理的存储增长:OT 的历史操作列表会无限增长。可以定期(如每天)做快照——将当前文档内容保存为新的基线版本,清理之前的操作历史。下一个版本从快照开始重新计数。

网络延迟下的用户体验:OT 要求操作要先过服务端才能看到效果,这在网络延迟大时体验很差。优化:客户端本地先套用操作(乐观更新),服务端确认后再修正。如果服务端变换后的结果和客户端不同,再做本地修正——这就是 Google Docs 的用户体验设计。

五、总结

OT 算法的核心是transform(op1, op2)函数,它的作用是"如果两个操作同时发生,变换其中一个使两者都能正确执行"。代码实现上要注意位置偏移的计算(Insert 导致位置增加,Delete 导致位置减少),以及对重叠删除的冲突处理。Go 语言实现 OT 的优势是并发安全(sync.Mutex 保护好文档的临界区)和性能(不需要处理复杂的异步回调)。如果要从零实现协作文档,建议从纯文本 OT 开始,跑通后再扩展到富文本——那是另一个维度的复杂度。

http://www.cnnetsun.cn/news/3621966.html

相关文章:

  • MATLAB零基础跑通MNIST手写数字识别:含原始数据解析、预处理与训练脚本
  • MATLAB 2019a即用型EMD分解工具包:含双版本核心算法(emd1/emd2)与Python兼容脚本
  • 基于DeepSeek的本地化RAG审计方案实践
  • 专科生论文写作AI工具全流程解决方案
  • Python毕设选题推荐:轻量化美食资源推荐与后台管理系统实现 基于 Python 的美食分类推荐与评分系统设计【附源码、mysql、文档、调试+代码讲解+全bao等】
  • Sora 2 AI视频生成核心技术解析与实践指南
  • 低代码构建智能对话Agent,Dify核心能力全解析,手把手教会你3天上线生产级应用
  • AI代理管理困境与解决方案:从技术到管理的跨越
  • AI辅助毕业论文写作:四步工作法提升效率
  • C++ Win32 API窗体开发:从消息驱动到透明窗口实现
  • 架构决策记录(ADR):让架构决策有据可查
  • SpringBoot调用Azkaban的轻量级封装库:Java代码直连调度中心,免UI操作完成任务流创建与执行
  • DM505处理器CAN与千兆以太网接口设计实战:从协议到PCB布局
  • 目标检测标签分配策略优化与工程实践
  • LLM的层数和参数分布
  • 揭秘Transformer中7大关键参数:从hidden_size到num_layers,90%工程师都误解的底层逻辑
  • UE4拖影效果实现:蓝图与渲染管线方案深度解析与实战
  • C++统一内存管理实战:原理、优化与异构计算应用
  • 基于YOLO与SpringBoot的安全锥智能检测系统实践
  • 蓝桥杯油漆面积题解:扫描线算法与线段树实现矩形面积并计算
  • TI ADC12DJ3200低功耗背景校准(LPBG)模式详解与配置实战
  • AO3镜像站:轻松访问全球最大同人创作平台的实用指南
  • 2026年AI论文写作辅助平台评测与使用指南
  • 基于SimpleLink MCU的MSP430 UART Bootloader实现与远程升级方案
  • VQFN封装PCB设计与生产实战:以LMK05028时钟发生器为例
  • 用 GitHub 做技术营销的一点小经验
  • 智能科学毕业设计选题方向与实现方案
  • 卷积神经网络认证训练:防御卷积扰动的PyTorch实战指南
  • 谷歌突然发大招!没网站的网红、自媒体也能白嫖搜索引擎流量了!
  • 拍照搜题、作业批改讲解app如何选择?让家长头大的辅导问题一次性说清楚