Notion でドキュメントを 5 人で同時編集する。Figma で複数のデザイナーが同じファイルを操作する。Linear でオフライン中に書いたチケットが再接続後に同期される。これらすべての中核技術が CRDT (Conflict-free Replicated Data Types) ── 数学的に「コンフリクトしない」ことが保証された分散データ構造。
1. これで何が動いているか
- Notion: ドキュメント / データベースの編集
- Figma: マルチプレイヤー (リアルタイム同時編集)
- Linear: オフライン操作の同期
- Yjs: WebRTC ベースの CRDT ライブラリ (Notion 等で採用)
- Automerge: Ink & Switch の CRDT ライブラリ
- Riak / Redis: KV ストアの CRDT 型 (CRDT counter / set)
- Apple Notes / Cosmos DB: バックエンド分散同期
2. 仕組みのざっくり
- 各レプリカ (ユーザの端末) はローカルに状態を持つ
- 更新は他レプリカに非同期で伝播する (オフラインも OK)
- マージ操作は「可換・結合・べき等」の 3 条件を満たす
- 結果: いつ、どの順序でマージしても、最終的に全レプリカが同じ状態になる (Strong Eventual Consistency)
3. G-Counter (増加のみカウンタ) の実装
最も簡単な CRDT: 各ノードのカウンタを別々に持ち、合計を取る
Python
class GCounter: '''各ノードのカウンタを別々に保持し、合計を value() とする''' def __init__(self, node_id): self.node = node_id self.counts = {} # {node_id: count}
def inc(self, n=1): self.counts[self.node] = self.counts.get(self.node, 0) + n
def value(self): return sum(self.counts.values())
def merge(self, other): '''各ノードの最大値を取る (べき等・可換・結合)''' for k, v in other.counts.items(): self.counts[k] = max(self.counts.get(k, 0), v)
# 3 ノードでバラバラに +1, +2, +3a = GCounter('A'); a.inc(3)b = GCounter('B'); b.inc(2)c = GCounter('C'); c.inc(1)
# 任意の順序でマージa.merge(b); a.merge(c)b.merge(a)c.merge(a)
print(f'A={a.value()}, B={b.value()}, C={c.value()}')# 全て 6 (= 3+2+1)4. LWW-Element-Set (タイムスタンプベース集合) の実装
Last-Write-Wins: 同じ要素の add/remove はタイムスタンプで決着
Python
import time
class LWWSet: '''Last-Write-Wins Element Set''' def __init__(self): self.adds = {} # {element: timestamp} self.removes = {} # {element: timestamp}
def add(self, e, ts=None): ts = ts or time.time() self.adds[e] = max(self.adds.get(e, 0), ts)
def remove(self, e, ts=None): ts = ts or time.time() self.removes[e] = max(self.removes.get(e, 0), ts)
def __contains__(self, e): a = self.adds.get(e, 0) r = self.removes.get(e, 0) return a > r # add が新しければ含まれる
def merge(self, other): for e, t in other.adds.items(): self.adds[e] = max(self.adds.get(e, 0), t) for e, t in other.removes.items(): self.removes[e] = max(self.removes.get(e, 0), t)
# シナリオ: 2 人が同じ要素を異なるタイミングで add/removes1 = LWWSet(); s2 = LWWSet()s1.add('apple', ts=100)s2.add('apple', ts=100)s2.remove('apple', ts=200) # s2 だけ削除
s1.merge(s2)print(f'apple in s1: {"apple" in s1}') # False (削除が新しい)5. CRDT の種類と用途
| CRDT | 用途 | 代表ライブラリ |
|---|---|---|
| G-Counter | 増加のみ整数 (PV カウント) | Riak |
| PN-Counter | 増減整数 (いいね数) | Riak / Redis |
| G-Set | 追加のみ集合 | Riak |
| LWW-Set | タイムスタンプ付き集合 | Cassandra |
| OR-Set | UUID 付き集合 (削除も安全) | Riak / Redis |
| RGA / Yata | 順序付きテキスト | Yjs / Automerge |
| JSON CRDT | JSON ツリー | Automerge |
6. 実用ライブラリ
y-py (Yjs の Python バインディング)
Python
# pip install y-py# ※ 本番では JS の Yjs を WebSocket で接続するのが一般的
import y_py as Y
doc = Y.YDoc()text = doc.get_text('content')with doc.begin_transaction() as txn: text.insert(txn, 0, 'Hello, ') text.insert(txn, 7, 'World!')
print(str(text)) # 'Hello, World!'# Yjs の update バイナリは差分同期に使える (WebRTC / WebSocket で配信)update = Y.encode_state_as_update(doc)7. メリットとデメリット
- メリット: 中央サーバ不要、オフライン編集可、自動マージ
- メリット: 数学的に「最終整合性」が保証される
- デメリット: 全操作の履歴を持つ場合、メモリ・帯域が膨らむ (GC が必要)
- デメリット: トランザクション (atomicity) はサポートしない
- デメリット: 「最後の書き込みが勝つ」等のセマンティクスは用途次第で適切でないことも
8. 関連ツール
- Yjs (JS): 業界標準、WebRTC / WebSocket / IndexedDB バインディング
- Automerge (JS / Rust): JSON CRDT 中心
- Riak: KV ストアの CRDT 型 (Distributed Counter / Set / Map)
- redis-stack: Redis CRDT モジュール
- Loro: Rust 製の最新 CRDT
9. 次の話
EP.08 では Raft 合意アルゴリズム を扱います。etcd / Consul / TiDB / CockroachDB の中核、リーダー選出とログ複製の仕組みを Python で実装。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。