diff options
| author | muqiuhan <[email protected]> | 2025-05-08 10:32:53 +0000 |
|---|---|---|
| committer | muqiuhan <[email protected]> | 2025-05-08 10:32:53 +0000 |
| commit | 6b6b4052f9081f8050cd4ff4d4b370c4307a1b5c (patch) | |
| tree | 218b96a92314a4b8d364d2b91cc8716c231fbfec /2025/04 | |
| parent | 82652597b81d959f864ac9d6e19407022961f6ba (diff) | |
| download | blog-6b6b4052f9081f8050cd4ff4d4b370c4307a1b5c.tar.gz | |
deploy: c8a911bb506ee39dd9d9cc7a7bf5f480eabd273b
Diffstat (limited to '2025/04')
| -rw-r--r-- | 2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html | 377 | ||||
| -rw-r--r-- | 2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html | 2 | ||||
| -rw-r--r-- | 2025/04/13/uuidv7-rdbms/index.html | 2 |
3 files changed, 2 insertions, 379 deletions
diff --git a/2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html b/2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html deleted file mode 100644 index dd3a6926..00000000 --- a/2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html +++ /dev/null @@ -1,377 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8"> -<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> -<meta http-equiv="X-UA-Compatible" content="ie=edge"> - - <meta name="author" content="韩暮秋"> - - - <meta name="subtitle" content="暮秋小屋"> - - - <meta name="description" content="这里是暮秋小屋,思念和灵感的寄存处"> - - - <meta name="keywords" content="韩暮秋,MuqiuHan,'Muqiu Han', 'muqiu han', muqiuhan"> - - - - - <title> - - 多人协作系统中的实现策略 (CRDT,锁等实现方案) | - 暮秋小屋 - </title> - - - - <link rel="icon" href="/favicon.ico"> - - - <style> - @font-face { - font-family: CarroisSong; - src: url('/fonts/CarroisSong.ttf'); - } - </style> - - - - <!-- stylesheets list from _config.yml --> - - <link rel="stylesheet" href="/css/style.css"> - - - - - - <!-- scripts list from _config.yml --> - - <script - src="/js/menu.js"></script> - - - - - - <script - src="https://polyfill.alicdn.com/polyfill.js?features=es6"></script> - <script - id="MathJax-script" - async - src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/mathjax/3.2.0/es5/tex-mml-chtml.js"></script> - - - - - <meta name="generator" content="Hexo 6.3.0"></head> - <body> - <div class="mask-border"> - </div> - - <div class="wrapper"> - - <div class="header"> - <div class="flex-container"> - <div class="header-inner"> - <div class="site-brand-container"> - <a href="/"> - - 暮秋小屋 - - </a> - </div> - <div id="menu-btn" class="menu-btn" onclick="toggleMenu()"> - Menu - </div> - <nav class="site-nav"> - <ul class="menu-list"> - - - <li class="menu-item"> - <a href="/"> - 主页 - </a> - </li> - - - - <li class="menu-item"> - <a href="/categories/gallery/"> - 日记本 - </a> - </li> - - - - <li class="menu-item"> - <a href="/tags/Medicine/"> - 医学 - </a> - </li> - - - - <li class="menu-item"> - <a href="/tags/Technique/"> - 计算机/互联网 - </a> - </li> - - - - <li class="menu-item"> - <a href="/tags/Life/"> - 生活 - </a> - </li> - - - - <li class="menu-item"> - <a href="/archives"> - 全部 - </a> - </li> - - - - <li class="menu-item"> - <a href="/about"> - 关于 - </a> - </li> - - - - <li class="menu-item search-btn"> - <a href="#">Search</a> - </li> - - </ul> - </nav> - </div> - </div> -</div> - - <div class="main"> - <div class="flex-container"> - <article id="post"> - - - <div class="post-head"> - <div class="post-info"> - <div class="tag-list"> - - - <span class="post-tag"> - <a href="/tags/Technique/"> - Technique - </a> - </span> - - - </div> - <div class="post-title"> - - - 多人协作系统中的实现策略 (CRDT,锁等实现方案) - - - </div> - <span class="post-date"> - Apr 2, 2025 - </span> - </div> - <div class="post-img"> - - <div class="h-line-primary"></div> - - </div> -</div> - <div class="post-content"> - <p>最近碰到一块业务:在系统中可以存在多个用户同时对某个项目信息进行编辑,这种多人协作的场景挺有意思的,不过在我们的业务中,并不需要实时协作,只需要保证不会出错就行,话虽如此,但也可以探索一下实时协作的实现方案,防止老年痴呆。</p> -<p>先来看看第一个方案 —— CRDT(Conflict-free Replicated Data Type,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线/并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。 </p> -<p>核心思想是:所有并发操作都是<strong>幂等</strong>(idempotent)、<strong>可交换</strong>(commutative)的。</p> -<p>常见类型有:<br>一、G-Counter(只能增计数器)<br>二、PN-Counter(可增可减计数器)<br>三、LWW-Register(最后写入胜出)<br>四、结合 JSON 的树型 CRDT(如 <a target="_blank" rel="noopener" href="https://github.com/automerge/automerge">Automerge</a> / <a target="_blank" rel="noopener" href="https://yjs.dev/">Yjs</a>) </p> -<p>更多原理可参考 Decipad 博客“Collaborative and Offline Editing Using CRDTs”[^1]。</p> -<blockquote> -<p>有一个挺有趣的 Rust 项目 <a target="_blank" rel="noopener" href="https://github.com/loro-dev/loro">Loro: Make your JSON data collaborative and version-controlled with CRDTs</a></p> -</blockquote> -<p>假设我的项目信息编辑页面允许多人实时/离线修改某个研究项目的“名称”、“描述”字段,前端用 SvelteKit + GraphQL 获取和提交变更:</p> -<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">┌── 用户 A 离线修改了 “description” 的若干段文本 </span><br><span class="line">└── 用户 B 同时在线修改了同一字段的其他段落 </span><br></pre></td></tr></table></figure> - -<p>如果后端使用 CRDT(比如把 <code>description</code> 用 JSON-CRDT 存储),两次修改只要在任意顺序合并都能得到完整的内容:</p> -<p>首先,A 客户端本地 apply 操作并缓存,恢复网络后推给服务器;<br>然后,服务器用 CRDT merge(A.delta, B.delta),得到一致文档<br>最后,服务器广播新文档到所有客户端,A/B 均得到相同结果 </p> -<hr> -<p>好了说点实际符合业务场景的方案,首先想到的是悲观锁(Pessimistic Locking) ,思路是:用户打开编辑界面时,向后端申请“锁” → 其它用户尝试编辑时被拒绝 → 编辑完成后释放锁/超时自动释放。 </p> -<p>假如有一个这样的锁表:</p> -<pre><code class="sql">CREATE TABLE project_lock ( - project_id UUID PRIMARY KEY, - locked_by UUID NOT NULL, - expires_at TIMESTAMPTZ NOT NULL -); -</code></pre> -<p>可以在事务内申请它:</p> -<pre><code class="ts">const now = new Date(); -const expires = new Date(now.getTime() + 5*60*1000); // 5 分钟后过期 -await prisma.$transaction(async tx => { - const existing = await tx.project_lock.findUnique({ where:{ project_id } }); - - if (existing && existing.expires_at > now) { - throw new Error('项目正被人编辑'); - } - - await tx.project_lock.upsert({ - where: { project_id }, - update: { locked_by: userId, expires_at: expires }, - create: { project_id, locked_by: userId, expires_at: expires } - }); -}); -</code></pre> -<p>释放锁就直接从锁表里删掉对应的数据即可:</p> -<pre><code class="ts">await prisma.project_lock.delete({ where:{ project_id } }); -</code></pre> -<p>前端的话,大概就是:</p> -<p>在进入编辑前请求一下 <code>/api/project/:id/lock</code> 之类的 API,失败则提示“被占用”;<br>在 <code>onbeforeunload</code> 时执行 <code>/unlock</code>;<br>超时后后端自动允许新锁。 </p> -<hr> -<p>第二个方案是乐观并发控制(Optimistic Concurrency) :记录资源的<strong>版本号</strong>或<strong>时间戳</strong>;客户端提交更新时带上自己的版本号,后端检查版本是否一致,不一致则认为冲突,返回 409,由客户端告知用户“数据已过期,请刷新后合并”。 </p> -<p>具体实现中,可以尝试在 <code>project</code> 表加上 <code>version INT NOT NULL DEFAULT 1, updated_at TIMESTAMPTZ</code> ,然后更新项目时:</p> -<pre><code class="ts">async updateProject(parent, { id, version, input }, ctx) { - const result = await prisma.$executeRaw` - UPDATE project - SET name = ${input.name}, - description = ${input.description}, - version = version + 1, - updated_at = now() - WHERE id = ${id} AND version = ${version} - `; - if (result === 0) { - throw new ConflictException('版本冲突,请刷新后重试'); - } - return prisma.project.findUnique({ where:{ id } }); -} -</code></pre> -<p>前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并/重新加载?” 之类的玩意儿。</p> -<hr> -<p>第三个方案是:操作转化(Operational Transformation,OT) </p> -<p>也就是记录用户每次的“操作”(insert/delete at position),服务器根据历史操作序列对并发操作做<strong>转化</strong>(transform),确保先到达的操作调整后再应用后到达的。 </p> -<p>有一些实现案例:<br>一、ShareDB(Node.js)<br>二、Google Docs 中的同步算法 </p> -<p>具体实现的话,可能要现在前端逐字符/块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。</p> -<hr> -<p>最后可能还可以用事件溯源(Event Sourcing)+ 场景命令模式来实现:</p> -<p>不直接存状态,而是存所有“命令 / 事件”(Event),回放事件得到当前状态。冲突通过合并策略或补偿事件(Compensating Events)解决。 </p> -<p>例如:<br>在每次更新时推送 <code>ProjectUpdated { projectId, fieldsChanged, userId, timestamp }</code> ,<br>然后写入事件存储(如 Kafka / EventStoreDB),<br>读端 Consumer 按顺序重建最新状态或按领域聚合 ,<br>最后在并发时如果两个事件都修改了同一字段,可在写端做校验/补偿,或在读端做最后写入胜出等策略 。</p> -<hr> -<p>总结来说,</p> -<ul> -<li>CRDT 最擅长 <strong>去中心化</strong>、<strong>离线编辑</strong>、<strong>自动合并</strong>; </li> -<li>若不引入 CRDT,可根据业务侧重点选用: <ol> -<li>悲观锁 → 强制串行编辑,简单粗暴; </li> -<li>乐观并发 → 适合大多数业务场景,成本低; </li> -<li>OT → 适合富文本或实时协同场景,复杂度中等; </li> -<li>事件溯源 → 适合需要全历史审计、可回放的场景。</li> -</ol> -</li> -</ul> -<hr> -<p>[^1]: Decipad 博客 “Collaborative and Offline Editing Using CRDTs”<br> <a target="_blank" rel="noopener" href="https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts">https://www.decipad.com/blog/decipads-innovative-method-collaborative-and-offline-editing-using-crdts</a></p> -<p>[^2]: Hacker News 讨论(CRDT 相关线程)<br> <a target="_blank" rel="noopener" href="https://news.ycombinator.com/item?id=38289327">https://news.ycombinator.com/item?id=38289327</a></p> - -</div> - -<script> - window.onload = detectors(); -</script> - <div class="post-footer"> - <div class="h-line-primary"></div> - <nav class="post-nav"> - <div class="prev-item"> - - <div class="icon arrow-left"></div> - <div class="post-link"> - <a href="/2025/04/13/uuidv7-rdbms/">Prev</a> - </div> - - </div> - <div class="next-item"> - - <div class="icon arrow-right"></div> - <div class="post-link"> - <a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">Next</a> - </div> - - </div> - </nav> -</div> - - - <div class="post-comment"> - - - - - - - -</div> - - -</article> - </div> - </div> - - <div class="footer"> - <div class="flex-container"> - <div class="footer-text"> - - - 韩暮秋 | - - - 希望路过的人可以添点柴火让这里暖和点 - - </div> - </div> -</div> - - </div> - - - <div class="search-popup"> - <div class="search-popup-overlay"> - </div> - <div class="search-popup-window" > - <div class="search-header"> - <div class="search-input-container"> - <input autocomplete="off" autocapitalize="off" maxlength="80" - placeholder="Search Anything" spellcheck="false" - type="search" class="search-input"> - </div> - <div class="search-close-btn"> - <div class="icon close-btn"></div> - </div> - </div> - <div class="search-result-container"> - </div> - </div> -</div> - -<script> - const searchConfig = { - path : "/search.xml", - top_n_per_article: "1", - unescape : "false", - trigger: "auto", - preload: "false" - } -</script> -<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/search.js"></script> -<script src="/js/search.js"></script> - - - - </body> -</html> diff --git a/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html b/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html index 5d1a1631..ac98e832 100644 --- a/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html +++ b/2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html @@ -236,7 +236,7 @@ <div class="icon arrow-left"></div> <div class="post-link"> - <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">Prev</a> + <a href="/2025/04/13/uuidv7-rdbms/">Prev</a> </div> </div> diff --git a/2025/04/13/uuidv7-rdbms/index.html b/2025/04/13/uuidv7-rdbms/index.html index 2710f377..1a8b513a 100644 --- a/2025/04/13/uuidv7-rdbms/index.html +++ b/2025/04/13/uuidv7-rdbms/index.html @@ -231,7 +231,7 @@ <div class="icon arrow-right"></div> <div class="post-link"> - <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">Next</a> + <a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">Next</a> </div> </div> |
