summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html377
-rw-r--r--2025/04/02/N-1-selects-problem-与-Prisma-ORM/index.html2
-rw-r--r--2025/04/13/uuidv7-rdbms/index.html2
-rw-r--r--archives/2025/04/index.html18
-rw-r--r--archives/2025/index.html34
-rw-r--r--archives/2025/page/2/index.html38
-rw-r--r--archives/2025/page/3/index.html250
-rw-r--r--archives/index.html32
-rw-r--r--archives/page/10/index.html36
-rw-r--r--archives/page/11/index.html36
-rw-r--r--archives/page/12/index.html36
-rw-r--r--archives/page/13/index.html20
-rw-r--r--archives/page/2/index.html36
-rw-r--r--archives/page/3/index.html38
-rw-r--r--archives/page/4/index.html36
-rw-r--r--archives/page/5/index.html36
-rw-r--r--archives/page/6/index.html36
-rw-r--r--archives/page/7/index.html36
-rw-r--r--archives/page/8/index.html36
-rw-r--r--archives/page/9/index.html36
-rw-r--r--search.xml95
-rw-r--r--tags/Technique/index.html36
-rw-r--r--tags/Technique/page/2/index.html36
-rw-r--r--tags/Technique/page/3/index.html36
-rw-r--r--tags/Technique/page/4/index.html36
-rw-r--r--tags/Technique/page/5/index.html18
26 files changed, 1104 insertions, 324 deletions
diff --git a/2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html b/2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html
new file mode 100644
index 00000000..dd3a6926
--- /dev/null
+++ b/2025/04/02/Multiplayer-Collaborative-Systems-tips/index.html
@@ -0,0 +1,377 @@
+<!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,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线&#x2F;并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。 </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> &#x2F; <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>假设我的项目信息编辑页面允许多人实时&#x2F;离线修改某个研究项目的“名称”、“描述”字段,前端用 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&#x2F;B 均得到相同结果 </p>
+<hr>
+<p>好了说点实际符合业务场景的方案,首先想到的是悲观锁(Pessimistic Locking) ,思路是:用户打开编辑界面时,向后端申请“锁” → 其它用户尝试编辑时被拒绝 → 编辑完成后释放锁&#x2F;超时自动释放。 </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 =&gt; &#123;
+ const existing = await tx.project_lock.findUnique(&#123; where:&#123; project_id &#125; &#125;);
+
+ if (existing &amp;&amp; existing.expires_at &gt; now) &#123;
+ throw new Error(&#39;项目正被人编辑&#39;);
+ &#125;
+
+ await tx.project_lock.upsert(&#123;
+ where: &#123; project_id &#125;,
+ update: &#123; locked_by: userId, expires_at: expires &#125;,
+ create: &#123; project_id, locked_by: userId, expires_at: expires &#125;
+ &#125;);
+&#125;);
+</code></pre>
+<p>释放锁就直接从锁表里删掉对应的数据即可:</p>
+<pre><code class="ts">await prisma.project_lock.delete(&#123; where:&#123; project_id &#125; &#125;);
+</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, &#123; id, version, input &#125;, ctx) &#123;
+ const result = await prisma.$executeRaw`
+ UPDATE project
+ SET name = $&#123;input.name&#125;,
+ description = $&#123;input.description&#125;,
+ version = version + 1,
+ updated_at = now()
+ WHERE id = $&#123;id&#125; AND version = $&#123;version&#125;
+ `;
+ if (result === 0) &#123;
+ throw new ConflictException(&#39;版本冲突,请刷新后重试&#39;);
+ &#125;
+ return prisma.project.findUnique(&#123; where:&#123; id &#125; &#125;);
+&#125;
+</code></pre>
+<p>前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并&#x2F;重新加载?” 之类的玩意儿。</p>
+<hr>
+<p>第三个方案是:操作转化(Operational Transformation,OT) </p>
+<p>也就是记录用户每次的“操作”(insert&#x2F;delete at position),服务器根据历史操作序列对并发操作做<strong>转化</strong>(transform),确保先到达的操作调整后再应用后到达的。 </p>
+<p>有一些实现案例:<br>一、ShareDB(Node.js)<br>二、Google Docs 中的同步算法 </p>
+<p>具体实现的话,可能要现在前端逐字符&#x2F;块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。</p>
+<hr>
+<p>最后可能还可以用事件溯源(Event Sourcing)+ 场景命令模式来实现:</p>
+<p>不直接存状态,而是存所有“命令 &#x2F; 事件”(Event),回放事件得到当前状态。冲突通过合并策略或补偿事件(Compensating Events)解决。 </p>
+<p>例如:<br>在每次更新时推送 <code>ProjectUpdated &#123; projectId, fieldsChanged, userId, timestamp &#125;</code> ,<br>然后写入事件存储(如 Kafka &#x2F; EventStoreDB),<br>读端 Consumer 按顺序重建最新状态或按领域聚合 ,<br>最后在并发时如果两个事件都修改了同一字段,可在写端做校验&#x2F;补偿,或在读端做最后写入胜出等策略 。</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 ac98e832..5d1a1631 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/13/uuidv7-rdbms/">Prev</a>
+ <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">Prev</a>
</div>
</div>
diff --git a/2025/04/13/uuidv7-rdbms/index.html b/2025/04/13/uuidv7-rdbms/index.html
index 1a8b513a..2710f377 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/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">Next</a>
+ <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">Next</a>
</div>
</div>
diff --git a/archives/2025/04/index.html b/archives/2025/04/index.html
index 293893ea..7541b842 100644
--- a/archives/2025/04/index.html
+++ b/archives/2025/04/index.html
@@ -206,6 +206,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">
+
+ 多人协作系统中的实现策略 (CRDT,锁等实现方案)
+
+ </a>
+ </div>
+
+ <span class="post-date">Apr 2, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">
N+1 selects problem 与 Prisma ORM
diff --git a/archives/2025/index.html b/archives/2025/index.html
index e2773b2c..cab180d0 100644
--- a/archives/2025/index.html
+++ b/archives/2025/index.html
@@ -242,9 +242,9 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">
+ <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">
- N+1 selects problem 与 Prisma ORM
+ 多人协作系统中的实现策略 (CRDT,锁等实现方案)
</a>
</div>
@@ -260,14 +260,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/31/%E5%9C%A8F-%E4%B8%AD%E5%A4%84%E7%90%86%E5%A4%8D%E6%9D%82%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97/">
+ <a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">
- 在F#中处理复杂依赖注入的实践指南
+ N+1 selects problem 与 Prisma ORM
</a>
</div>
- <span class="post-date">Mar 31, 2025</span>
+ <span class="post-date">Apr 2, 2025</span>
</div>
@@ -278,14 +278,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/30/F-%E9%87%8D%E8%BD%BD%E8%A7%A3%E6%9E%90%E9%97%AE%E9%A2%98/">
+ <a href="/2025/03/31/%E5%9C%A8F-%E4%B8%AD%E5%A4%84%E7%90%86%E5%A4%8D%E6%9D%82%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97/">
- F# 重载解析问题
+ 在F#中处理复杂依赖注入的实践指南
</a>
</div>
- <span class="post-date">Mar 30, 2025</span>
+ <span class="post-date">Mar 31, 2025</span>
</div>
@@ -296,14 +296,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/25/Scala-3-Capture-Checking/">
+ <a href="/2025/03/30/F-%E9%87%8D%E8%BD%BD%E8%A7%A3%E6%9E%90%E9%97%AE%E9%A2%98/">
- Scala 3 (Capture Checking)
+ F# 重载解析问题
</a>
</div>
- <span class="post-date">Mar 25, 2025</span>
+ <span class="post-date">Mar 30, 2025</span>
</div>
@@ -314,14 +314,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/20/TDD-%E5%92%8C-DDD-%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B0%8F%E6%83%B3%E6%B3%95/">
+ <a href="/2025/03/25/Scala-3-Capture-Checking/">
- TDD 和 DDD 的一些小想法
+ Scala 3 (Capture Checking)
</a>
</div>
- <span class="post-date">Mar 20, 2025</span>
+ <span class="post-date">Mar 25, 2025</span>
</div>
@@ -332,9 +332,9 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/20/%E8%82%BA%E5%8A%9F%E8%83%BD%E6%A3%80%E6%9F%A5%E6%95%B0%E5%80%BC/">
+ <a href="/2025/03/20/TDD-%E5%92%8C-DDD-%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B0%8F%E6%83%B3%E6%B3%95/">
- 肺功能检查数值
+ TDD 和 DDD 的一些小想法
</a>
</div>
@@ -348,7 +348,7 @@
<div class=paginator>
<div class="h-line-primary"></div>
<nav class="pagination">
- <span class="page-number current">1</span><a class="page-number" href="/archives/2025/page/2/">2</a>
+ <span class="page-number current">1</span><a class="page-number" href="/archives/2025/page/2/">2</a><a class="page-number" href="/archives/2025/page/3/">3</a>
</nav>
</div>
diff --git a/archives/2025/page/2/index.html b/archives/2025/page/2/index.html
index 05e87a0b..a7e992a6 100644
--- a/archives/2025/page/2/index.html
+++ b/archives/2025/page/2/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/03/20/%E8%82%BA%E5%8A%9F%E8%83%BD%E6%A3%80%E6%9F%A5%E6%95%B0%E5%80%BC/">
+
+ 肺功能检查数值
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 20, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/03/19/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E8%81%9A%E5%90%88%E6%A0%B9%E6%8C%81%E4%B9%85%E5%8C%96%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%8F%91%E5%B8%83%E5%8F%AF%E8%83%BD%E5%AF%BC%E8%87%B4%E6%95%B0%E6%8D%AE%E4%B8%8D%E4%B8%80%E8%87%B4%E9%97%AE%E9%A2%98/">
领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题
@@ -325,30 +343,12 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2025/01/19/%E8%BA%AB%E4%BD%93%E5%BE%80%E5%89%8D%E5%80%BE%EF%BC%8C%E7%81%B5%E9%AD%82%E5%90%91%E5%90%8E%E6%91%87/">
-
- 身体往前倾,灵魂向后摇
-
- </a>
- </div>
-
- <span class="post-date">Jan 19, 2025</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
<div class="h-line-primary"></div>
<nav class="pagination">
- <a class="page-number" href="/archives/2025/">1</a><span class="page-number current">2</span>
+ <a class="page-number" href="/archives/2025/">1</a><span class="page-number current">2</span><a class="page-number" href="/archives/2025/page/3/">3</a>
</nav>
</div>
diff --git a/archives/2025/page/3/index.html b/archives/2025/page/3/index.html
new file mode 100644
index 00000000..f937e820
--- /dev/null
+++ b/archives/2025/page/3/index.html
@@ -0,0 +1,250 @@
+<!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>
+
+ Archive: 2025 |
+ 暮秋小屋
+ </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">
+ <div class="post-list">
+
+
+
+
+
+
+ <div class="year-title">2025</div>
+
+
+ <div class="post-list-item">
+ <div class="post-title">
+ <a href="/2025/01/19/%E8%BA%AB%E4%BD%93%E5%BE%80%E5%89%8D%E5%80%BE%EF%BC%8C%E7%81%B5%E9%AD%82%E5%90%91%E5%90%8E%E6%91%87/">
+
+ 身体往前倾,灵魂向后摇
+
+ </a>
+ </div>
+
+ <span class="post-date">Jan 19, 2025</span>
+ </div>
+
+
+ <div id="paginator">
+
+ <div class=paginator>
+ <div class="h-line-primary"></div>
+ <nav class="pagination">
+ <a class="page-number" href="/archives/2025/">1</a><a class="page-number" href="/archives/2025/page/2/">2</a><span class="page-number current">3</span>
+ </nav>
+ </div>
+
+ </div>
+</div>
+
+ </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/archives/index.html b/archives/index.html
index e8524151..bffc10cc 100644
--- a/archives/index.html
+++ b/archives/index.html
@@ -242,9 +242,9 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">
+ <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">
- N+1 selects problem 与 Prisma ORM
+ 多人协作系统中的实现策略 (CRDT,锁等实现方案)
</a>
</div>
@@ -260,14 +260,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/31/%E5%9C%A8F-%E4%B8%AD%E5%A4%84%E7%90%86%E5%A4%8D%E6%9D%82%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97/">
+ <a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">
- 在F#中处理复杂依赖注入的实践指南
+ N+1 selects problem 与 Prisma ORM
</a>
</div>
- <span class="post-date">Mar 31, 2025</span>
+ <span class="post-date">Apr 2, 2025</span>
</div>
@@ -278,14 +278,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/30/F-%E9%87%8D%E8%BD%BD%E8%A7%A3%E6%9E%90%E9%97%AE%E9%A2%98/">
+ <a href="/2025/03/31/%E5%9C%A8F-%E4%B8%AD%E5%A4%84%E7%90%86%E5%A4%8D%E6%9D%82%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97/">
- F# 重载解析问题
+ 在F#中处理复杂依赖注入的实践指南
</a>
</div>
- <span class="post-date">Mar 30, 2025</span>
+ <span class="post-date">Mar 31, 2025</span>
</div>
@@ -296,14 +296,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/25/Scala-3-Capture-Checking/">
+ <a href="/2025/03/30/F-%E9%87%8D%E8%BD%BD%E8%A7%A3%E6%9E%90%E9%97%AE%E9%A2%98/">
- Scala 3 (Capture Checking)
+ F# 重载解析问题
</a>
</div>
- <span class="post-date">Mar 25, 2025</span>
+ <span class="post-date">Mar 30, 2025</span>
</div>
@@ -314,14 +314,14 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/20/TDD-%E5%92%8C-DDD-%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B0%8F%E6%83%B3%E6%B3%95/">
+ <a href="/2025/03/25/Scala-3-Capture-Checking/">
- TDD 和 DDD 的一些小想法
+ Scala 3 (Capture Checking)
</a>
</div>
- <span class="post-date">Mar 20, 2025</span>
+ <span class="post-date">Mar 25, 2025</span>
</div>
@@ -332,9 +332,9 @@
<div class="post-list-item">
<div class="post-title">
- <a href="/2025/03/20/%E8%82%BA%E5%8A%9F%E8%83%BD%E6%A3%80%E6%9F%A5%E6%95%B0%E5%80%BC/">
+ <a href="/2025/03/20/TDD-%E5%92%8C-DDD-%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B0%8F%E6%83%B3%E6%B3%95/">
- 肺功能检查数值
+ TDD 和 DDD 的一些小想法
</a>
</div>
diff --git a/archives/page/10/index.html b/archives/page/10/index.html
index 957b6e8b..8dbe9ef1 100644
--- a/archives/page/10/index.html
+++ b/archives/page/10/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/06/22/%E7%81%AF%E7%AC%BC%E8%8D%89/">
+
+ 灯笼草
+
+ </a>
+ </div>
+
+ <span class="post-date">Jun 22, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/06/17/%E5%8F%91%E7%83%AD%E5%8F%8A%E8%B6%85%E9%AB%98%E7%83%AD%E5%8D%B1%E8%B1%A1/">
发热及超高热危象
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/03/14/%E8%9E%BA%E5%86%85%E9%85%AF%E7%89%87/">
-
- 螺内酯片
-
- </a>
- </div>
-
- <span class="post-date">Mar 14, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/11/index.html b/archives/page/11/index.html
index 518de872..8853d410 100644
--- a/archives/page/11/index.html
+++ b/archives/page/11/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/03/14/%E8%9E%BA%E5%86%85%E9%85%AF%E7%89%87/">
+
+ 螺内酯片
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 14, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/03/14/%E5%91%8B%E5%A1%9E%E7%B1%B3/">
呋塞米
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/02/22/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E4%BA%8C%E6%9C%88%E4%BA%8C%E5%8D%81%E4%BA%8C%E6%97%A5/">
-
- 二零二三年二月二十二日
-
- </a>
- </div>
-
- <span class="post-date">Feb 22, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/12/index.html b/archives/page/12/index.html
index d1cb156e..5867d12a 100644
--- a/archives/page/12/index.html
+++ b/archives/page/12/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/02/22/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E4%BA%8C%E6%9C%88%E4%BA%8C%E5%8D%81%E4%BA%8C%E6%97%A5/">
+
+ 二零二三年二月二十二日
+
+ </a>
+ </div>
+
+ <span class="post-date">Feb 22, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/02/02/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E4%BA%8C%E6%9C%88%E4%BA%8C%E6%97%A5/">
二零二三年二月二日
@@ -327,24 +345,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2022/01/01/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%BA%8C%E5%B9%B4%E4%B8%80%E6%9C%88%E4%B8%80%E6%97%A5/">
-
- 二零二二年一月一日
-
- </a>
- </div>
-
- <span class="post-date">Jan 1, 2022</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/13/index.html b/archives/page/13/index.html
index 2ad6e3ed..58ac64c7 100644
--- a/archives/page/13/index.html
+++ b/archives/page/13/index.html
@@ -165,6 +165,26 @@
+ <div class="year-title">2022</div>
+
+
+ <div class="post-list-item">
+ <div class="post-title">
+ <a href="/2022/01/01/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%BA%8C%E5%B9%B4%E4%B8%80%E6%9C%88%E4%B8%80%E6%97%A5/">
+
+ 二零二二年一月一日
+
+ </a>
+ </div>
+
+ <span class="post-date">Jan 1, 2022</span>
+ </div>
+
+
+
+
+
+
<div class="year-title">2021</div>
diff --git a/archives/page/2/index.html b/archives/page/2/index.html
index c551b531..3f74857a 100644
--- a/archives/page/2/index.html
+++ b/archives/page/2/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/03/20/%E8%82%BA%E5%8A%9F%E8%83%BD%E6%A3%80%E6%9F%A5%E6%95%B0%E5%80%BC/">
+
+ 肺功能检查数值
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 20, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/03/19/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E8%81%9A%E5%90%88%E6%A0%B9%E6%8C%81%E4%B9%85%E5%8C%96%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%8F%91%E5%B8%83%E5%8F%AF%E8%83%BD%E5%AF%BC%E8%87%B4%E6%95%B0%E6%8D%AE%E4%B8%8D%E4%B8%80%E8%87%B4%E9%97%AE%E9%A2%98/">
领域驱动设计中聚合根持久化和事件发布可能导致数据不一致问题
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2025/01/19/%E8%BA%AB%E4%BD%93%E5%BE%80%E5%89%8D%E5%80%BE%EF%BC%8C%E7%81%B5%E9%AD%82%E5%90%91%E5%90%8E%E6%91%87/">
-
- 身体往前倾,灵魂向后摇
-
- </a>
- </div>
-
- <span class="post-date">Jan 19, 2025</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/3/index.html b/archives/page/3/index.html
index cb8d1350..cf56c445 100644
--- a/archives/page/3/index.html
+++ b/archives/page/3/index.html
@@ -165,6 +165,26 @@
+ <div class="year-title">2025</div>
+
+
+ <div class="post-list-item">
+ <div class="post-title">
+ <a href="/2025/01/19/%E8%BA%AB%E4%BD%93%E5%BE%80%E5%89%8D%E5%80%BE%EF%BC%8C%E7%81%B5%E9%AD%82%E5%90%91%E5%90%8E%E6%91%87/">
+
+ 身体往前倾,灵魂向后摇
+
+ </a>
+ </div>
+
+ <span class="post-date">Jan 19, 2025</span>
+ </div>
+
+
+
+
+
+
<div class="year-title">2024</div>
@@ -325,24 +345,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/09/27/Turborepo-%E7%AE%80%E8%BF%B0/">
-
- Turborepo 简述
-
- </a>
- </div>
-
- <span class="post-date">Sep 27, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/4/index.html b/archives/page/4/index.html
index c713d295..5a767a86 100644
--- a/archives/page/4/index.html
+++ b/archives/page/4/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/09/27/Turborepo-%E7%AE%80%E8%BF%B0/">
+
+ Turborepo 简述
+
+ </a>
+ </div>
+
+ <span class="post-date">Sep 27, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/09/15/%E8%82%A9%E5%91%A8%E7%82%8E%E5%92%8C%E8%82%A9%E8%A2%96%E6%8D%9F%E4%BC%A4%E7%9A%84%E5%8C%BA%E5%88%AB/">
肩周炎和肩袖损伤的区别
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/08/02/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%85%AB%E6%9C%88%E4%B8%80%E6%97%A5/">
-
- 二〇二四年八月一日
-
- </a>
- </div>
-
- <span class="post-date">Aug 2, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/5/index.html b/archives/page/5/index.html
index 87330e25..c391f953 100644
--- a/archives/page/5/index.html
+++ b/archives/page/5/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/08/02/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%85%AB%E6%9C%88%E4%B8%80%E6%97%A5/">
+
+ 二〇二四年八月一日
+
+ </a>
+ </div>
+
+ <span class="post-date">Aug 2, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/07/23/OCaml-News-2024-6/">
OCaml News 2024-6
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/05/07/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%83%E6%97%A5/">
-
- 二〇二四年五月七日
-
- </a>
- </div>
-
- <span class="post-date">May 7, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/6/index.html b/archives/page/6/index.html
index dc59824d..4e3f7c0d 100644
--- a/archives/page/6/index.html
+++ b/archives/page/6/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/05/07/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E4%BA%94%E6%9C%88%E4%B8%83%E6%97%A5/">
+
+ 二〇二四年五月七日
+
+ </a>
+ </div>
+
+ <span class="post-date">May 7, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/04/08/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%9B%9B%E5%B9%B4%E5%9B%9B%E6%9C%88%E5%85%AB%E6%97%A5/">
二〇二四年四月八日
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/01/22/Rescript-React-Native/">
-
- Rescript and React Native
-
- </a>
- </div>
-
- <span class="post-date">Jan 22, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/7/index.html b/archives/page/7/index.html
index 02fe542c..9f93ea90 100644
--- a/archives/page/7/index.html
+++ b/archives/page/7/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/01/22/Rescript-React-Native/">
+
+ Rescript and React Native
+
+ </a>
+ </div>
+
+ <span class="post-date">Jan 22, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/01/18/OCaml-News-2024-2/">
OCaml News 2024-2
@@ -327,24 +345,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/10/25/Radiographic-Absence-of-the-Left-Humeral-Head/">
-
- Radiographic Absence of the Left Humeral Head
-
- </a>
- </div>
-
- <span class="post-date">Oct 25, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/8/index.html b/archives/page/8/index.html
index 1fc94df2..15b34903 100644
--- a/archives/page/8/index.html
+++ b/archives/page/8/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/10/25/Radiographic-Absence-of-the-Left-Humeral-Head/">
+
+ Radiographic Absence of the Left Humeral Head
+
+ </a>
+ </div>
+
+ <span class="post-date">Oct 25, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/10/25/%E8%82%BA%E7%82%8E%E6%94%AF%E5%8E%9F%E4%BD%93%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9/">
肺炎支原体注意事项
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/08/27/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%85%AB%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%83%E6%97%A5/">
-
- 二零二三年八月二十七日
-
- </a>
- </div>
-
- <span class="post-date">Aug 27, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/archives/page/9/index.html b/archives/page/9/index.html
index 47d45afc..7001bfe0 100644
--- a/archives/page/9/index.html
+++ b/archives/page/9/index.html
@@ -170,6 +170,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/08/27/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%85%AB%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%83%E6%97%A5/">
+
+ 二零二三年八月二十七日
+
+ </a>
+ </div>
+
+ <span class="post-date">Aug 27, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/08/21/%E4%BA%8C%E9%9B%B6%E4%BA%8C%E4%B8%89%E5%B9%B4%E5%85%AB%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%80%E6%97%A5/">
二零二三年八月二十一日
@@ -325,24 +343,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/06/22/%E7%81%AF%E7%AC%BC%E8%8D%89/">
-
- 灯笼草
-
- </a>
- </div>
-
- <span class="post-date">Jun 22, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/search.xml b/search.xml
index b1597b59..fa55fdd4 100644
--- a/search.xml
+++ b/search.xml
@@ -824,6 +824,101 @@
</tags>
</entry>
<entry>
+ <title>多人协作系统中的实现策略 (CRDT,锁等实现方案)</title>
+ <url>/2025/04/02/Multiplayer-Collaborative-Systems-tips/</url>
+ <content><![CDATA[<p>最近碰到一块业务:在系统中可以存在多个用户同时对某个项目信息进行编辑,这种多人协作的场景挺有意思的,不过在我们的业务中,并不需要实时协作,只需要保证不会出错就行,话虽如此,但也可以探索一下实时协作的实现方案,防止老年痴呆。</p>
+<p>先来看看第一个方案 —— CRDT(Conflict-free Replicated Data Type,无冲突可复制数据类型)是一类数据结构,它保证了在分布式节点(或多客户端)上进行离线&#x2F;并发更新后,无需中心协调、也无需人工干预,通过“合并策略”就能得到一致的最终状态。 </p>
+<p>核心思想是:所有并发操作都是<strong>幂等</strong>(idempotent)、<strong>可交换</strong>(commutative)的。</p>
+<p>常见类型有:<br>一、G-Counter(只能增计数器)<br>二、PN-Counter(可增可减计数器)<br>三、LWW-Register(最后写入胜出)<br>四、结合 JSON 的树型 CRDT(如 <a href="https://github.com/automerge/automerge">Automerge</a> &#x2F; <a href="https://yjs.dev/">Yjs</a>) </p>
+<p>更多原理可参考 Decipad 博客“Collaborative and Offline Editing Using CRDTs”[^1]。</p>
+<blockquote>
+<p>有一个挺有趣的 Rust 项目 <a href="https://github.com/loro-dev/loro">Loro: Make your JSON data collaborative and version-controlled with CRDTs</a></p>
+</blockquote>
+<p>假设我的项目信息编辑页面允许多人实时&#x2F;离线修改某个研究项目的“名称”、“描述”字段,前端用 SvelteKit + GraphQL 获取和提交变更:</p>
+<figure class="highlight plaintext"><table><tr><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&#x2F;B 均得到相同结果 </p>
+<hr>
+<p>好了说点实际符合业务场景的方案,首先想到的是悲观锁(Pessimistic Locking) ,思路是:用户打开编辑界面时,向后端申请“锁” → 其它用户尝试编辑时被拒绝 → 编辑完成后释放锁&#x2F;超时自动释放。 </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 =&gt; &#123;
+ const existing = await tx.project_lock.findUnique(&#123; where:&#123; project_id &#125; &#125;);
+
+ if (existing &amp;&amp; existing.expires_at &gt; now) &#123;
+ throw new Error(&#39;项目正被人编辑&#39;);
+ &#125;
+
+ await tx.project_lock.upsert(&#123;
+ where: &#123; project_id &#125;,
+ update: &#123; locked_by: userId, expires_at: expires &#125;,
+ create: &#123; project_id, locked_by: userId, expires_at: expires &#125;
+ &#125;);
+&#125;);
+</code></pre>
+<p>释放锁就直接从锁表里删掉对应的数据即可:</p>
+<pre><code class="ts">await prisma.project_lock.delete(&#123; where:&#123; project_id &#125; &#125;);
+</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, &#123; id, version, input &#125;, ctx) &#123;
+ const result = await prisma.$executeRaw`
+ UPDATE project
+ SET name = $&#123;input.name&#125;,
+ description = $&#123;input.description&#125;,
+ version = version + 1,
+ updated_at = now()
+ WHERE id = $&#123;id&#125; AND version = $&#123;version&#125;
+ `;
+ if (result === 0) &#123;
+ throw new ConflictException(&#39;版本冲突,请刷新后重试&#39;);
+ &#125;
+ return prisma.project.findUnique(&#123; where:&#123; id &#125; &#125;);
+&#125;
+</code></pre>
+<p>前端捕获到冲突错误可以用一个弹窗提示“另有用户已更新此项目,是否合并&#x2F;重新加载?” 之类的玩意儿。</p>
+<hr>
+<p>第三个方案是:操作转化(Operational Transformation,OT) </p>
+<p>也就是记录用户每次的“操作”(insert&#x2F;delete at position),服务器根据历史操作序列对并发操作做<strong>转化</strong>(transform),确保先到达的操作调整后再应用后到达的。 </p>
+<p>有一些实现案例:<br>一、ShareDB(Node.js)<br>二、Google Docs 中的同步算法 </p>
+<p>具体实现的话,可能要现在前端逐字符&#x2F;块地包装成操作并 WebSocket 推送,服务器再维护一个“操作历史队列”,每来一个 op 就 transform 并 broadcast,而客户端收到广播后,按顺序 replay 保证视图一致。</p>
+<hr>
+<p>最后可能还可以用事件溯源(Event Sourcing)+ 场景命令模式来实现:</p>
+<p>不直接存状态,而是存所有“命令 &#x2F; 事件”(Event),回放事件得到当前状态。冲突通过合并策略或补偿事件(Compensating Events)解决。 </p>
+<p>例如:<br>在每次更新时推送 <code>ProjectUpdated &#123; projectId, fieldsChanged, userId, timestamp &#125;</code> ,<br>然后写入事件存储(如 Kafka &#x2F; EventStoreDB),<br>读端 Consumer 按顺序重建最新状态或按领域聚合 ,<br>最后在并发时如果两个事件都修改了同一字段,可在写端做校验&#x2F;补偿,或在读端做最后写入胜出等策略 。</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 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 href="https://news.ycombinator.com/item?id=38289327">https://news.ycombinator.com/item?id=38289327</a></p>
+]]></content>
+ <tags>
+ <tag>Technique</tag>
+ </tags>
+ </entry>
+ <entry>
<title>N+1 selects problem 与 Prisma ORM</title>
<url>/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/</url>
<content><![CDATA[<p>N+1 查询问题是指在通过 ORM 查询数据时,执行了<strong>一次</strong>初始查询来获取父对象列表(这 <strong>1</strong> 次查询),然后为列表中的<strong>每一个</strong>父对象都单独执行了一次额外的查询来获取其关联的子对象(这 <strong>N</strong> 次查询)。最终导致总共执行了 <strong>1 + N</strong> 次数据库查询,其中 N 是初始查询返回的父对象的数量。</p>
diff --git a/tags/Technique/index.html b/tags/Technique/index.html
index fa6125b5..ed8431ed 100644
--- a/tags/Technique/index.html
+++ b/tags/Technique/index.html
@@ -231,6 +231,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/04/02/Multiplayer-Collaborative-Systems-tips/">
+
+ 多人协作系统中的实现策略 (CRDT,锁等实现方案)
+
+ </a>
+ </div>
+
+ <span class="post-date">Apr 2, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/04/02/N-1-selects-problem-%E4%B8%8E-Prisma-ORM/">
N+1 selects problem 与 Prisma ORM
@@ -332,24 +350,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2025/03/14/DDD-%E4%B8%AD%E7%9A%84-Ubiquitous-Languages/">
-
- DDD 中的 Ubiquitous Language
-
- </a>
- </div>
-
- <span class="post-date">Mar 14, 2025</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/2/index.html b/tags/Technique/page/2/index.html
index e0783efe..e2e87289 100644
--- a/tags/Technique/page/2/index.html
+++ b/tags/Technique/page/2/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2025/03/14/DDD-%E4%B8%AD%E7%9A%84-Ubiquitous-Languages/">
+
+ DDD 中的 Ubiquitous Language
+
+ </a>
+ </div>
+
+ <span class="post-date">Mar 14, 2025</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2025/03/13/%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%AD%E7%9A%84%E2%80%9C%E8%81%9A%E5%90%88%E6%A0%B9%E2%80%9D/">
领域驱动设计中的“聚合根”
@@ -334,24 +352,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/10/06/Obsidian-Vault-%E7%9A%84-obsidian-%E7%9B%AE%E5%BD%95%E4%B8%AD%E7%9A%84%E5%90%84%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8/">
-
- Obsidian Vault 的 .obsidian 目录中的各文件作用
-
- </a>
- </div>
-
- <span class="post-date">Oct 6, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/3/index.html b/tags/Technique/page/3/index.html
index d6d5efcd..fe0bbff3 100644
--- a/tags/Technique/page/3/index.html
+++ b/tags/Technique/page/3/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/10/06/Obsidian-Vault-%E7%9A%84-obsidian-%E7%9B%AE%E5%BD%95%E4%B8%AD%E7%9A%84%E5%90%84%E6%96%87%E4%BB%B6%E4%BD%9C%E7%94%A8/">
+
+ Obsidian Vault 的 .obsidian 目录中的各文件作用
+
+ </a>
+ </div>
+
+ <span class="post-date">Oct 6, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/09/27/Turborepo-%E7%AE%80%E8%BF%B0/">
Turborepo 简述
@@ -332,24 +350,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2024/02/26/OCaml-News-2024-3/">
-
- OCaml News 2024-3
-
- </a>
- </div>
-
- <span class="post-date">Feb 26, 2024</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/4/index.html b/tags/Technique/page/4/index.html
index 9da30b93..250074bc 100644
--- a/tags/Technique/page/4/index.html
+++ b/tags/Technique/page/4/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2024/02/26/OCaml-News-2024-3/">
+
+ OCaml News 2024-3
+
+ </a>
+ </div>
+
+ <span class="post-date">Feb 26, 2024</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2024/01/23/G-Machine/">
G-Machine
@@ -334,24 +352,6 @@
</div>
-
-
-
-
-
- <div class="post-list-item">
- <div class="post-title">
- <a href="/2023/07/21/WiscKey-Separating-Keys-from-Values-in-SSD-conscious-Storage/">
-
- WiscKey: Separating Keys from Values in SSD-conscious Storage
-
- </a>
- </div>
-
- <span class="post-date">Jul 21, 2023</span>
- </div>
-
-
<div id="paginator">
<div class=paginator>
diff --git a/tags/Technique/page/5/index.html b/tags/Technique/page/5/index.html
index 2b953c43..d025bc32 100644
--- a/tags/Technique/page/5/index.html
+++ b/tags/Technique/page/5/index.html
@@ -177,6 +177,24 @@
<div class="post-list-item">
<div class="post-title">
+ <a href="/2023/07/21/WiscKey-Separating-Keys-from-Values-in-SSD-conscious-Storage/">
+
+ WiscKey: Separating Keys from Values in SSD-conscious Storage
+
+ </a>
+ </div>
+
+ <span class="post-date">Jul 21, 2023</span>
+ </div>
+
+
+
+
+
+
+
+ <div class="post-list-item">
+ <div class="post-title">
<a href="/2023/06/28/%E9%9A%90%E8%97%8F%E4%B8%80%E4%BA%9BOCaml-Effect%E7%9A%84%E6%9C%BA%E5%88%B6%EF%BC%8C%E8%AE%A9%E5%85%B6%E8%AF%AD%E6%B3%95%E5%9C%A8%E7%B2%BE%E7%A5%9E%E4%B8%8A%E6%9B%B4%E6%8E%A5%E8%BF%91delimcc/">
隐藏一些OCaml Effect的机制,让其语法在精神上更接近delimcc