ふくふくHukuhuku LLC
EP.07Algorithms 10分公開: 更新:

CRDT:Notion・Figma・Linear のリアルタイム同時編集を支える分散データ構造

「複数人が同じドキュメントを同時に編集してもコンフリクトしない」を数学的に保証する CRDT を Python で実装。G-Counter・LWW-Set・RGA の仕組み。

#CRDT#分散システム#Notion#Figma
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
CO📔 Google Colab で開く(上から順にセルを実行)
シェア

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-SetUUID 付き集合 (削除も安全)Riak / Redis
RGA / Yata順序付きテキストYjs / Automerge
JSON CRDTJSON ツリー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 で実装。

よくある質問

CRDT って何の略?
**Conflict-free Replicated Data Types** の略。**「複数のレプリカが個別に更新しても、最終的にマージすれば必ず同じ状態になる」** 数学的性質を持つデータ構造。集合論の **半束 (semilattice)** という概念に基づく。
なぜコンフリクトしないと保証できる?
**マージ操作が「可換 (commutative)」「結合的 (associative)」「べき等 (idempotent)」の 3 条件を満たすから**。どの順序で merge しても、何度 merge しても、結果が一意に決まる。これが「最終的に皆同じ状態になる (eventual consistency)」の数学的根拠。
Operational Transform (OT) との違いは?
**OT は中央サーバが操作の順序を決めるが、CRDT はサーバ不要 (peer-to-peer)**。Google Docs は OT、Notion / Figma / Linear / Yjs / Automerge は CRDT。CRDT は **オフライン編集** にも強い (再接続時に自動マージ)。
CRDT の種類
**State-based (CvRDT)**: 状態全体を送ってマージ。**Operation-based (CmRDT)**: 操作だけを送る。代表例: G-Counter (増加のみ整数)、PN-Counter (増減整数)、G-Set (追加のみ集合)、LWW-Set (タイムスタンプ付き集合)、OR-Set (UUID 付き集合)、RGA (順序付きリスト、テキスト編集向け)。
本番で使われている例は?
**Notion**: ドキュメント本体は CRDT ベース。**Figma**: マルチプレイヤー編集の中核。**Linear**: オフライン操作の同期。**Yjs / Automerge**: 業界標準ライブラリ。**Riak / Redis**: KV ストアに CRDT 内蔵。**Cosmos DB / Apple Notes**: バックエンドで CRDT。
テキスト編集はどう実現する?
**RGA (Replicated Growable Array) や Yata (Yjs)、Logoot、Treedoc** などの順序付き CRDT を使う。各文字に **(client_id, timestamp) のユニーク ID** を割当て、挿入位置を ID 同士の比較で決定。これにより 2 人が同時に同じ位置に文字を挿入しても、決定論的に順序が決まる。
シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp