[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fcAcWw5C15ZMf7eAg9tmPV0AObxoNKcr7m8IukVxcfZo":3},{"item":4,"related":40},{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":31,"sourceName":32,"sourceUrl":32,"status":33,"seoTitle":32,"seoDescription":32,"canonicalUrl":32,"isFeatured":34,"sno":35,"sortOrder":36,"publishedAt":37,"updatedAt":38,"createdAt":39},"23cd6a50-6716-4620-99f3-d5307511c8ee","article","CRDT：两个人同时改同一段文字，为什么不会互相覆盖","crdt-collaborative-editing-explained","CRDT 允许多个副本先本地修改，再通过可合并的数据结构最终收敛。本文用计数器和协同编辑讲清 CvRDT、CmRDT、并发插入、删除标记、元数据成本与业务语义冲突。","两个人同时编辑同一段文字时，最朴素的同步方式是“谁最后提交，谁覆盖谁”。网络一抖、有人离线，冲突就会变成一场人工抢救。CRDT 的思路更像是：允许每个副本先本地修改，网络恢复后再合并，并且让合并规则保证所有副本最终收敛到同一个状态。\n\n![分布式网络中的节点与连接](https:\u002F\u002Fcommons.wikimedia.org\u002Fwiki\u002FSpecial:FilePath\u002FDistributed-networks.svg)\n\nCRDT 是 Conflict-free Replicated Data Type 的缩写，中文通常译为“无冲突复制数据类型”。它不是某一个数据库，也不是一条网络协议，而是一类带有数学性质的数据结构。经典研究把它分成两条路线：基于状态合并的 CvRDT，以及基于操作传播的 CmRDT。[CRDT 研究论文](https:\u002F\u002Fdsf.berkeley.edu\u002Fcs286\u002Fpapers\u002Fcrdt-tr2011.pdf)\n\n## 先看一个不会让人头疼的例子：计数器\n\n假设 Alice 和 Bob 都看到计数器是 10。Alice 离线加 1，得到 11；Bob 同时加 1，也得到 11。若直接传“新值是 11”，服务器无法知道两次加法都发生过，最后可能只剩 11。\n\nCRDT 计数器不把更新表达成“把值改成 11”，而是表达成“我的副本增加了 1”。每个副本维护自己的增量，合并时把各副本的增量相加。Alice 的增量和 Bob 的增量都被保留下来，最终结果是 12。\n\n这个例子体现了一个重要原则：同步的不是容易覆盖的最终快照，而是能安全合并的事实。\n\n## CRDT 的“无冲突”到底是什么意思\n\n它并不是说任何业务冲突都能神奇消失，而是说：在数据结构规定的操作范围内，只要不同副本最终收到同一批更新，它们会按照确定的规则得到同一个状态。\n\n对于状态型 CRDT，合并函数通常需要满足：\n\n- 交换律：先合并 Alice 再合并 Bob，与反过来相同。\n- 结合律：分组方式不影响最终结果。\n- 幂等性：同一个状态重复收到，结果不会越来越大。\n\n这让网络层可以重试、乱序甚至重复发送更新，而不会因为传输细节把副本推向不同结果。操作型 CRDT 则把重点放在操作可交换，以及传输层满足必要的投递和因果关系条件。\n\n```mermaid\nflowchart LR\n    A[副本 Alice] -->|本地操作| C[可合并更新]\n    B[副本 Bob] -->|本地操作| C\n    C --> D[网络异步传播]\n    D --> E[副本 Alice 合并]\n    D --> F[副本 Bob 合并]\n    E --> G[最终状态收敛]\n    F --> G\n```\n\n## 文本编辑为什么比计数器难得多\n\n计数器的“加 1”没有位置问题，文本却有。Alice 和 Bob 都在字符串 `ABC` 的 `B` 前面插入字符，如果更新只记录“在位置 1 插入”，网络乱序时两个操作就没有稳定身份。\n\n文本 CRDT 通常会给每个字符或元素分配唯一标识，并记录它与其他元素的关系。插入操作不再是“插到第几个格子”，而是“插到某个已知元素附近，并带有唯一 ID”。当两个元素竞争同一个位置时，系统使用确定的排序规则，让所有副本看到同样的顺序。\n\n删除也不是简单地把字符从数组中抹掉。某个副本可能还没收到插入操作，另一个副本已经删除它；为了让迟到的更新仍然能被识别，很多实现会保留删除标记或墓碑。这样做换来了正确合并，却带来元数据膨胀、压缩和垃圾回收问题。\n\n因此，“CRDT 不需要冲突解决”这句话不够准确。它把一部分冲突处理从人工步骤提前搬到了数据结构设计中。设计者仍然要回答：并发插入如何排序？删除和编辑同时发生怎么办？撤销操作如何定义？格式化和权限是否也要合并？\n\n## CvRDT 与 CmRDT：两种合并风格\n\n### 基于状态的 CvRDT\n\n每个副本可以直接发送自己的状态，接收方通过合并函数计算新状态。优点是实现直观、重复发送通常安全；缺点是状态可能很大，频繁传输浪费带宽。\n\n### 基于操作的 CmRDT\n\n副本发送“发生了什么操作”，接收方重放这些操作。优点是更新小，适合增量同步；缺点是操作需要唯一 ID、去重机制和一定的因果处理，不能把任意乱序消息直接当作安全输入。\n\n现实项目常常把两者结合：首次加入房间时发送压缩后的状态，之后发送操作更新；离线时间较长时，再通过状态向量或快照补齐缺口。\n\n## CRDT 不会替你解决的三类问题\n\n第一，语义冲突仍然存在。两个人同时把商品库存从 1 改成 0 和 10，数学上可以收敛，但业务上哪个结果合法，需要库存规则决定。\n\n第二，元数据有成本。为了处理并发、因果和删除，CRDT 往往比最终文本多保存不少信息。移动端、长文档和大量历史记录尤其需要压缩策略。\n\n第三，权限不能靠“最终一致”解决。一个没有权限的客户端如果可以产生任意更新，CRDT 只会忠实地把恶意操作合并到所有副本。权限校验、签名、撤销和服务端策略仍然要单独设计。\n\n## 什么时候值得选 CRDT\n\nCRDT 特别适合离线优先、多人实时协作、节点经常断线、希望本地操作立即响应的场景，例如共享笔记、白板、表单草稿和部分游戏状态。如果业务必须每一步都经过中心服务器批准，或者数据量极小、单主写入已经足够，CRDT 的复杂度可能不值得。\n\n落地时可以按这个顺序思考：先确定需要合并的抽象数据类型，再定义并发操作的语义，然后验证交换、结合和幂等性质，最后才选择具体库。不要因为“协同编辑”四个字就直接把一个文本 CRDT 塞进所有数据模型。\n\n## 一句话带走\n\nCRDT 的魔法不是让冲突不存在，而是把数据表示成“可以安全合并的事实”。它让每个副本先行动、网络随后同步成为可能，但代价是更多元数据、更复杂的撤销与权限设计，以及对业务语义的更严格建模。\n\n## 延伸阅读\n\n- [A comprehensive study of CRDTs](https:\u002F\u002Fdsf.berkeley.edu\u002Fcs286\u002Fpapers\u002Fcrdt-tr2011.pdf)\n- [CRDT 论文索引](https:\u002F\u002Fcrdt.tech\u002Fpapers.html)\n- [CRDTs: Consistency without concurrency control](https:\u002F\u002Farxiv.org\u002Fabs\u002F0907.0929)","\u002Fuploads\u002F2026-08-06\u002F50641649-08d8-4cac-94f0-647eabe9fc5f.jpg",[],[],"Foundit","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27],{"id":24,"name":25,"slug":26},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":28,"name":29,"slug":30},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse","资料来源",null,"published",false,62,0,"2026-08-06T00:00:00.000Z","2026-08-06T05:55:11.614Z","2026-08-06T05:38:23.928Z",[41,49,56],{"id":42,"type":6,"title":43,"slug":44,"summary":45,"coverUrl":46,"authorName":14,"sno":47,"publishedAt":37,"createdAt":48},"d6ff32b0-f499-453a-b1e3-c31a4a7a5b16","布隆过滤器：系统如何快速判断一个东西肯定不存在","bloom-filter-probabilistic-membership-explained","布隆过滤器用位数组和多个哈希函数，以极少内存快速筛掉肯定不存在的对象。本文讲清误报与漏报、误报率、删除难题，以及它在缓存、数据库和去重系统中的正确用法。","\u002Fuploads\u002F2026-08-06\u002F234915f6-2ef2-4820-8523-1a98544732f9.jpg",61,"2026-08-06T05:38:25.381Z",{"id":50,"type":6,"title":51,"slug":52,"summary":53,"coverUrl":54,"authorName":14,"sno":35,"publishedAt":37,"createdAt":55},"a04c10a4-3fe8-4537-81bd-354103c1078f","WebGPU：浏览器为什么能跑 3D、滤镜和部分 AI","webgpu-browser-gpu-computing-explained","WebGPU 让网页能更直接地使用 GPU 做渲染与通用计算。本文从 CPU 与 GPU 的分工讲起，拆解适配器、设备、缓冲区、WGSL 着色器和命令队列，并说明什么时候并行计算真的值得。","\u002Fuploads\u002F2026-08-06\u002F05f26f06-cda2-4ed3-b1f2-5f26c82c2a37.jpg","2026-08-06T05:38:22.834Z",{"id":57,"type":6,"title":58,"slug":59,"summary":60,"coverUrl":61,"authorName":14,"sno":62,"publishedAt":37,"createdAt":63},"86b63309-e12a-41b4-9d9e-db95480617b0","令牌桶限流：API 如何把流量洪峰变成可控波浪","token-bucket-rate-limiting-explained","令牌桶把长期平均速率和短时突发容量分开，是 API 限流中最常见的模型之一。本文从令牌补充、请求成本和 429 响应讲起，比较固定窗口、滑动窗口与漏桶，并拆解分布式限流的真实难点。","\u002Fuploads\u002F2026-08-06\u002F952eedca-0e08-4c7c-a8a6-d07f1acd5291.jpg",63,"2026-08-06T05:38:26.532Z"]