結果整合性(Eventual Consistency)は、複製されたデータが更新直後には異なる値でも、新しい更新が止まり、通信と同期処理が続けば、最終的に同じ状態へ収束するという一貫性モデルです。
「数秒後には必ず最新になる」という意味ではありません。収束時間の上限、障害時の動作、読み取り保証は、採用するシステムと設定で別に確認します。
更新直後に古い値が見える理由
三つのレプリカへデータを複製しているとします。アプリがレプリカAへの書き込み成功後すぐ応答し、BとCへの伝播を非同期で行う場合、別の読み取りがBへ届くと旧値を返す可能性があります。
t0: A, B, C は version 1
t1: A が version 2 を受け入れる
t2: 書き込み成功を利用者へ返す
t3: Bを読むと version 1 が見える場合がある
t4: B, Cへ version 2 が伝播し、収束する

この「古い値を読む」状態をstale readと呼びます。結果整合性はデータ消失を許す言葉ではなく、どの版を返し、複数の版をどう収束させるかを設計するモデルです。
結果整合性だけではUXを決められない
利用者がプロフィール名を変更した直後に旧名へ戻って見えると、保存に失敗したと感じます。そこで、基本モデルより強いセッション保証を組み合わせることがあります。
| 保証 | 利用者から見える性質 |
|---|---|
| Read-your-writes | 自分が完了した書き込みより古い値を、その後の自分の読み取りで見ない |
| Monotonic reads | 一度見た版より古い版へ戻らない |
| Monotonic writes | 同じ利用者の書き込みを発行順に適用する |
実現方法には、同じレプリカへ一定時間ルーティングする、書き込み版を示すトークンを読み取りへ渡す、同期済みのレプリカまで待つ、画面で送信中の状態を保持する、などがあります。どれも遅延、可用性、状態管理との交換です。
収束を支える処理
非同期レプリケーションは、一度送って終わりとは限りません。障害中に送れなかった更新を再送する仕組み、レプリカ間の差分を検出して修復するanti-entropy、競合する版を記録する仕組みなどが収束を支えます。
再送では同じ更新が複数回届く可能性があります。処理IDを保存して重複を無視する、同じ操作を複数回適用しても結果が変わらないidempotentな更新にするなど、at-least-once deliveryを前提とした設計が必要です。
「メッセージ送信成功」と「すべての複製への反映完了」は別の状態です。同期キューから消えた後も、反映先で拒否や競合が起きていないかを確認します。
同時更新には競合解決が必要
ネットワーク分断中に二つのレプリカが同じキーを更新すると、通信回復後に複数の正当な版が残ります。単に伝播を待つだけでは一つに決まりません。
代表的な方法は次の通りです。
- Last Write Wins(LWW):時刻などで一つを選ぶ。単純ですが、時計のずれや同時更新で一方を失います。
- アプリケーションで統合:買い物かごの項目を併合するなど、業務ルールで解決します。
- バージョン情報を保持:vector clockなどで前後関係と並行更新を区別します。
- CRDT:定められた演算で、順序が違っても同じ状態へ収束させます。
どの方法でもすべてのデータに安全なわけではありません。残高をLWWで決めれば、別の場所で行った更新を失う可能性があります。データ型と業務上の不変条件に合わせて選びます。
適する場面と慎重に扱う場面
結果整合性を検討しやすい例は、少し古くても回復できる表示です。
- 閲覧数や「いいね」数
- 検索インデックスへの反映
- キャッシュ、分析用の集計
- 複数リージョンへ配るプロフィール表示
一方、次は「後で合えばよい」と簡単には扱えません。
- 残高、決済、在庫の最終確定
- 一回だけ実行すべき処理
- アクセス権の取り消し
- 同じ識別子の重複禁止
強い一貫性が必要な操作と、結果整合性でよい表示を同じサービス内で分けることもできます。システム全体へ一つのラベルを付けるより、操作ごとの保証を定義します。
ACIDとの比較で誤解しない
ACIDは主にトランザクションの性質、結果整合性は複製された読み取りの見え方を説明する用語です。「ACIDか結果整合性か」という一つの軸だけで製品を分類できません。単一リージョンのトランザクションはACIDで、別リージョンへの複製は結果整合性という構成もあります。
BASEという説明は設計思想の対比には使えますが、各文字だけでは具体的な保証を定義できません。APIでは、書き込み成功条件、読み取り元、競合時の動作、収束の目標時間を個別に示します。
quorumは万能な強整合性ではない
複製数をN、読み取りに必要な応答数をR、書き込みに必要な応答数をWとし、R + W > Nにすると読み書き集合が重なる構成があります。しかし、重なることだけで常に線形化可能になるとは限りません。
同時書き込みの版管理、失敗ノードの代替先、read repair、時計、読み取り時にどの版を選ぶかなどが影響します。Dynamo型の仕組みを、単純な式だけで「強整合」と断定しません。
quorumを大きくすれば、多くのノードを待つため遅延と失敗率に影響します。操作ごとにRとWを変えられる製品でも、現在の公式仕様と実測を確認します。
同じ製品でも、primary keyの読み取り、secondary index、cache、複数regionの複製で一貫性が異なる場合があります。「このDBは結果整合性」とまとめず、実際に使う読み取り経路ごとに文書化します。 書き込み経路についても同様に成功条件を分けます。
因果関係を守りたい場合
結果整合性より強く、すべての操作を直列化するほどではない保証として、causal consistencyがあります。「投稿を作成した後に、その投稿へ返信する」のような因果関係を、すべての利用者が同じ順に見ることを目指します。
単純な時刻順では因果関係を表せないことがあります。version、セッション情報、論理時計などを使います。採用するデータストアが提供しない保証を、画面側の並べ替えだけで実現したと考えないようにします。
APIと画面で扱う
更新APIが202 Acceptedを返すなら、「受付済みで反映中」であることを画面に示し、確認用の処理IDを返せます。200 OKを返しても、他の読み取りモデルまで即時反映を保証するとは限りません。API契約に反映範囲を書きます。
{
"status": "pending",
"operationId": "op_123",
"submittedVersion": 42
}
画面では保存直後の入力をローカルに保持するoptimistic UIも選択肢ですが、サーバーが拒否した時の差し戻しと、競合を利用者へどう伝えるかが必要です。
versionをAPIへ含める
クライアントが最後に見たversionを更新要求へ含めると、古い状態を前提にした上書きを検出できます。
PATCH /profiles/42
If-Match: "version-7"
サーバー側がすでにversion 8なら、412 Precondition Failedなどで競合を返し、最新値との統合を促せます。これは複製の結果整合性そのものを強くするのではなく、利用者によるlost updateをAPI境界で防ぐ方法です。
イベント駆動の反映では、operation IDと元イベントのversionをログに残すと、どこまで伝播したかを追いやすくなります。
観測する指標
- 更新から各レプリカへ反映されるまでの遅延
- 未同期の変更数、同期キューの最古時刻
- 競合の発生数と解決方法
- read-your-writes違反や古い版の読み取り
- 再試行、重複処理、同期失敗
「通常はすぐ反映される」だけでは運用基準になりません。どのpercentileを何秒以内にするか、上限を超えた時に警告するかをサービス要件として決めます。
障害シナリオを試す
正常時の更新だけでは、結果整合性の問題は見えません。検証環境で次を試します。
- 一つのレプリカへの伝播を一時停止する
- 同じキーを二つの場所で更新する
- 通信を戻し、どの版へ収束するか確認する
- 同じイベントを重複配信する
- 長時間停止したレプリカを復帰させる
期待値は「最終的に何となく同じ」ではなく、競合が検出されるか、データを失わないか、収束時間を観測できるかで定めます。
よくある誤解
- いつか必ず収束する:更新停止、通信回復、同期処理の継続などの前提が必要です。
- 整合性が弱いほど常に高速:構成やアクセス経路によるため、実測が必要です。
- LWWなら最新が残る:分散環境の時計と業務上の「最新」は一致しない場合があります。
- 結果整合性の製品は全操作が同じ:読み取りオプション、リージョン、索引などで保証が異なります。
まとめ
結果整合性は、更新が止まり同期が続けば複製が収束するモデルです。更新直後のstale read、同時更新の競合、収束までのUXは別途設計します。データベース名で判断せず、操作ごとに必要な保証、許容時間、競合解決と観測方法を決めます。