Skip to content

コンフリクト解決

注: この記事は英語版 02-distributed-databases/04-conflict-resolution.md の日本語翻訳です。

TL;DR

コンフリクトは、レプリカ間で同時に更新が発生した場合に起こります。解決戦略はシンプルなもの(最終書き込み優先)から複雑なもの(カスタムマージ関数)まであります。データのセマンティクスに応じて選択してください:シンプルさを優先するならLWW(データ消失リスクあり)、自動収束にはCRDT、完全な制御にはアプリケーションレベルの解決が適しています。最良のコンフリクト対策は、そもそもコンフリクトを発生させないことです。


コンフリクトが発生するタイミング

同時更新

Replica A                    Replica B
    │                            │
 write(x, 1)                  write(x, 2)
    │                            │
    └────────────────────────────┘

           Both succeed locally
           Which value is correct?

ネットワークパーティション

┌─────────────────┐     ┌─────────────────┐
│   Partition A   │     │   Partition B   │
│                 │     │                 │
│   write(x, 1)   │  X  │   write(x, 2)   │
│                 │     │                 │
└─────────────────┘     └─────────────────┘

After partition heals:
  x = 1 or x = 2?

オフラインクライアント

Client A (online):  write(x, 1) → synced
Client B (offline): write(x, 2) → queued

Client B comes online:
  Conflict: x = 1 vs x = 2

コンフリクトの種類

Write-Writeコンフリクト

同じフィールドが異なる値に変更されます。

Replica A: user.name = "Alice"
Replica B: user.name = "Bob"

Conflict: Which name?

Read-Modify-Writeコンフリクト

両方のレプリカが同じ値を読み取り、変更して書き込みます。

Initial: counter = 10

Replica A: read 10, increment, write 11
Replica B: read 10, increment, write 11

Expected: 12
Actual: 11 (lost update)

Delete-Updateコンフリクト

一方が削除し、もう一方が更新します。

Replica A: DELETE FROM users WHERE id = 1
Replica B: UPDATE users SET name = 'Bob' WHERE id = 1

What happens to the update?

制約違反

両方のレプリカが矛盾するエントリを作成します。

Replica A: INSERT (id=1, email='x@y.com')
Replica B: INSERT (id=2, email='x@y.com')

Unique constraint on email violated

解決戦略

最終書き込み優先(LWW)

最も高いタイムスタンプが勝ちます。

Write A: {value: "Alice", timestamp: 1000}
Write B: {value: "Bob", timestamp: 1001}

Resolution: value = "Bob" (higher timestamp)

実装:

python
def resolve_lww(versions):
    return max(versions, key=lambda v: v.timestamp)

メリット:

  • 実装がシンプル
  • 決定論的
  • 自動収束

デメリット:

  • データ消失(以前の書き込みが破棄される)
  • クロックスキューにより誤った勝者が選ばれる可能性
  • セマンティクスの理解がない

使用すべきケース:

  • キャッシュエントリ
  • 最終更新タイムスタンプ
  • 「最新」が意味を持つデータ

最初の書き込み優先

最も低いタイムスタンプが勝ちます(元の値を保持)。

Write A: {value: "Alice", timestamp: 1000}
Write B: {value: "Bob", timestamp: 1001}

Resolution: value = "Alice" (lower timestamp)

使用すべきケース:

  • 不変レコード
  • 監査ログ
  • 「一度だけ作成」のセマンティクス

値のマージ

矛盾する値を結合します。

Cart at A: [item1, item2]
Cart at B: [item1, item3]

Merge: [item1, item2, item3]

実装:

python
def merge_sets(versions):
    result = set()
    for v in versions:
        result = result.union(v.items)
    return result

適用可能なケース:

  • 集合(和集合)
  • カウンター(最大値または合計)
  • 追記専用リスト

アプリケーションレベルの解決

すべてのバージョンを保存し、アプリケーションに判断させます。

Read(x) → {
    versions: [
        {value: "Alice", timestamp: 1000, source: "A"},
        {value: "Bob", timestamp: 1001, source: "B"}
    ],
    conflict: true
}

Application: Present UI for user to choose

実装:

python
def read_with_conflicts(key):
    versions = get_all_versions(key)
    if len(versions) > 1:
        return Conflict(versions)
    return versions[0]

def resolve_conflict(key, chosen_version, discarded_versions):
    write(key, chosen_version)
    for v in discarded_versions:
        mark_as_resolved(v)

CRDT(コンフリクトフリー複製データ型)

コンセプト

常にコンフリクトなしでマージできるように設計されたデータ構造です。

Property: Merge is:
  - Commutative: merge(A, B) = merge(B, A)
  - Associative: merge(merge(A, B), C) = merge(A, merge(B, C))
  - Idempotent: merge(A, A) = A

Any order of merging produces same result

G-Counter(増加専用カウンター)

Each node tracks its own increment:
  Node A: {A: 5, B: 0, C: 0}
  Node B: {A: 3, B: 7, C: 0}

Merge: component-wise max
  Result: {A: 5, B: 7, C: 0}

Total: 5 + 7 + 0 = 12

操作:

python
class GCounter:
    def __init__(self, node_id):
        self.node_id = node_id
        self.counts = {}

    def increment(self):
        self.counts[self.node_id] = self.counts.get(self.node_id, 0) + 1

    def value(self):
        return sum(self.counts.values())

    def merge(self, other):
        for node, count in other.counts.items():
            self.counts[node] = max(self.counts.get(node, 0), count)

PN-Counter(正負カウンター)

増加と減少をサポートします:

P (positive): G-Counter for increments
N (negative): G-Counter for decrements

Value = P.value() - N.value()

Merge: merge P and N separately

G-Set(増加専用集合)

要素は追加のみ可能で、削除はできません。

Set A: {1, 2, 3}
Set B: {2, 3, 4}

Merge: union = {1, 2, 3, 4}

OR-Set(観測削除集合)

「追加が勝つ」セマンティクスで追加と削除をサポートします。

Each element has unique tags:
  add(x) → x with new tag
  remove(x) → remove all current tags

Concurrent add and remove:
  add(x) creates new tag, remove sees old tags
  Result: x exists (add wins)

実装:

python
class ORSet:
    def __init__(self):
        self.elements = {}  # element → set of tags

    def add(self, element):
        tag = unique_tag()
        self.elements.setdefault(element, set()).add(tag)

    def remove(self, element):
        self.elements[element] = set()  # Remove all known tags

    def contains(self, element):
        return len(self.elements.get(element, set())) > 0

    def merge(self, other):
        for elem, tags in other.elements.items():
            self.elements[elem] = self.elements.get(elem, set()).union(tags)

LWW-Register

CRDTとしての最終書き込み優先レジスタです。

python
class LWWRegister:
    def __init__(self):
        self.value = None
        self.timestamp = 0

    def write(self, value, timestamp):
        if timestamp > self.timestamp:
            self.value = value
            self.timestamp = timestamp

    def merge(self, other):
        if other.timestamp > self.timestamp:
            self.value = other.value
            self.timestamp = other.timestamp

LWW-Map

各キーがLWW-Registerであるマップです。

Node A: {"name": ("Alice", t=100), "age": (30, t=100)}
Node B: {"name": ("Bob", t=101), "city": ("NYC", t=100)}

Merge:
  "name": ("Bob", t=101)  ← higher timestamp
  "age": (30, t=100)
  "city": ("NYC", t=100)

バージョンベクター

因果関係の追跡

Version vector: {A: 3, B: 2, C: 1}

Meaning:
  - Incorporates 3 updates from A
  - Incorporates 2 updates from B
  - Incorporates 1 update from C

バージョンの比較

python
def compare(vv1, vv2):
    less_or_equal = all(vv1.get(k, 0) <= vv2.get(k, 0) for k in set(vv1) | set(vv2))
    greater_or_equal = all(vv1.get(k, 0) >= vv2.get(k, 0) for k in set(vv1) | set(vv2))

    if less_or_equal and not greater_or_equal:
        return "vv1 < vv2"  # vv1 is ancestor
    elif greater_or_equal and not less_or_equal:
        return "vv1 > vv2"  # vv2 is ancestor
    elif less_or_equal and greater_or_equal:
        return "equal"
    else:
        return "concurrent"  # Neither is ancestor → conflict

コンフリクトの検出

Write 1: value="A", vv={A:1, B:0}
Write 2: value="B", vv={A:0, B:1}

Compare:
  {A:1, B:0} vs {A:0, B:1}
  Neither dominates → concurrent → conflict!

バージョンベクターによる解決

python
def read_repair(versions):
    # Find all versions that are not dominated by another
    non_dominated = []
    for v in versions:
        if not any(dominates(other.vv, v.vv) for other in versions if other != v):
            non_dominated.append(v)

    if len(non_dominated) == 1:
        return non_dominated[0]  # Clear winner
    else:
        return Conflict(non_dominated)  # Need resolution

セマンティックコンフリクト解決

三方向マージ

共通の祖先を使用してインテリジェントにマージします。

Original (base): "The quick brown fox"
Version A:       "The quick red fox"    (changed brown → red)
Version B:       "The fast brown fox"   (changed quick → fast)

Three-way merge:
  - "quick" → "fast" (B's change)
  - "brown" → "red" (A's change)
  Result: "The fast red fox"

実装:

python
def three_way_merge(base, a, b):
    a_changes = diff(base, a)
    b_changes = diff(base, b)

    for change in a_changes + b_changes:
        if conflicts_with(change, a_changes + b_changes):
            return Conflict(a, b)

    return apply_changes(base, a_changes + b_changes)

操作変換(OT)

操作を変換して任意の順序で適用できるようにします。

Base: "abc"
Op A: insert('X', position=1) → "aXbc"
Op B: insert('Y', position=2) → "abYc"

If A applied first:
  B needs transformation: position 2 → position 3
  "aXbc" + insert('Y', 3) = "aXbYc"

If B applied first:
  A remains: position 1
  "abYc" + insert('X', 1) = "aXbYc"

Same result regardless of order

削除の処理

トゥームストーン

削除されたアイテムを削除せずにマークします。

Before: {id: 1, name: "Alice", deleted: false}
Delete: {id: 1, name: "Alice", deleted: true, deleted_at: 1000}

Why tombstones:
  - Replicas need to know item was deleted
  - Otherwise they might resurrect it from their version

トゥームストーンのクリーンアップ

Problem: Tombstones accumulate forever

Solutions:
1. Time-based garbage collection
   Delete tombstones older than X days
   Risk: old replica comes back, resurrects data

2. Version vector garbage collection
   Delete when all replicas have seen tombstone
   Requires coordination

3. Compaction
   Periodically merge and remove tombstones

ソフトデリート vs ハードデリート

Soft delete: Keep record, mark as deleted
  + Can undelete
  + Preserves audit trail
  - Storage overhead

Hard delete: Remove record
  + No storage overhead
  - Can cause resurrection
  - Loses history

実世界のアプローチ

Amazon Dynamo(Riak)

Strategy: Return all conflicting versions to client

Read → [{value: "A", clock: {A:1}}, {value: "B", clock: {B:1}}]

Client merges, writes back with merged clock
  Write(merged, clock: {A:1, B:1, Client:1})

CouchDB

Store all revisions in conflict
  _id: "doc1"
  _conflicts: ["2-abc123", "2-def456"]

Application picks winner or merges
Losing revisions become historical

Cassandra

LWW by default per column

CREATE TABLE users (
  id uuid,
  name text,
  email text,
  PRIMARY KEY (id)
);

-- Each column resolved independently
-- Highest timestamp wins per column

Git

Three-way merge for file contents
Conflict markers for unresolvable conflicts

<<<<<<< HEAD
my changes
=======
their changes
>>>>>>> branch

User manually resolves

戦略の選択

判断マトリクス

シナリオ戦略理由
キャッシュLWW古いデータが許容される
カウンターCRDT G-Counter正確な集計
ショッピングカートOR-Set CRDT追加が勝ち、アイテムをマージ
ユーザープロフィールLWWまたはフィールドマージフィールドレベルの解決
ドキュメント編集OTまたはCRDTリアルタイムコラボレーション
金融取引コンフリクトの防止シングルリーダーを使用
ソーシャルフィードLWW古さが許容される

解決より防止

最良のコンフリクト戦略はコンフリクトを防止することです:

1. Single leader for conflicting data
2. Partition data so conflicts impossible
3. Use optimistic locking with retries
4. Serialize operations through queue

Prevention is simpler than resolution

重要なポイント

  1. コンフリクトは不可避 - マルチリーダー/リーダーレスシステムでは必ず発生します
  2. LWWはシンプルだがデータ消失がある - 一部のデータでは許容可能です
  3. CRDTは収束を保証する - 設計上コンフリクトなし
  4. バージョンベクターは同時実行を検出する - コンフリクト検出に不可欠です
  5. アプリケーションが最もよく知っている - 時にはユーザーに判断させましょう
  6. トゥームストーンは必要 - ただしクリーンアップ戦略が必要です
  7. 可能な限り防止する - 重要なデータにはシングルリーダーを使用しましょう
  8. データのセマンティクスで選択する - カウンター、集合、レジスタにはそれぞれ異なる戦略が必要です

MITライセンスの下で公開。Babushkaiコミュニティが構築。