第26章:衝突(コンフリクト)はどう起きる?💥😳
この章のゴール🎯✨
この章を読み終わったら…
- 「衝突って、どういう現象?」を自分の言葉で説明できる🗣️💡
- 衝突が起きる代表パターン(同時更新/オフライン/順不同…)を具体例で想像できる🧠🧩
- 一番ありがちな事故「上書き(Lost Update)」を再現して怖さを体感できる😱🧪
- 次の章(第27章)でやる「解決方法」を学ぶための前提の肌感覚ができる🌱✨
1) そもそも「衝突」って何?🤔💥
同じデータに対して、別々の場所・別々のタイミングで更新が入って、 「どっちが正しいの?」が決められなくなる(または勝手に決まって事故る)状態のことだよ😳💦
CampusCafeで超わかりやすい例☕📱
- Aさん(スマホ):「受け取り時間を 12:10 に変更」🕛
- Bさん(売店タブレット):「受け取り時間を 12:30 に変更」🕧 …が、ほぼ同時に起きたら? 最後に届いたほうが勝つとか、混ざって変な状態になるとか、いろいろ地獄が起きる😇🔥
2) 衝突が起きる“3大パターン”🧨🧠
パターンA:同時更新(並行更新)🧑🤝🧑⚡
同じデータを、2人(または2端末・2サービス)がほぼ同時に更新するやつ!
ありがちシーン🍰
-
商品「いちごケーキ」の在庫数を
- キッチン端末が減らす📉
- 管理画面が補充で増やす📈 これが同時に走ると、最後に書いた方がもう片方を消すことがある💦
パターンB:オフライン更新(あとから合流)📴➡️🌐
スマホが電波悪い・地下・学内Wi-Fi死んでる…あるあるだよね🥲📶 オフラインで編集した内容が、あとからサーバに合流するときに衝突する!
ありがちシーン🍙
- スマホがオフライン中に「アレルギー情報」を更新📝
- その間に、別端末で「通知設定」を更新🔔
- 後で同期したら、**“古い状態を丸ごと上書き”**して事故ることがある😱
パターンC:順不同(順番がズレて届く)🔀📨
イベント駆動や非同期だと、順番通りに届く保証がないのが普通だったよね(第16章でもやったやつ!)🔀😵💫 順番がズレると「結果的に矛盾」=衝突っぽい状態になる。
ありがちシーン🔔
- OrderPaid(支払い完了)💳✅
- OrderCancelled(キャンセル)❌ が順番逆で来ると、「キャンセル済みなのに支払い完了扱い」みたいな変な最終状態になりがち😇🔥
3) さらに増える:マルチリージョン/複数書き込み拠点🌍✍️
クラウドの分散DBで「複数リージョンから書き込める」構成(マルチライト)にすると、 別リージョンで同じアイテムを同時更新できちゃうので、衝突が現実に起きるよ📡💥 (Azure Cosmos DB でも「複数 write リージョンだと更新衝突が起き得る」前提で説明されてるよ)(Microsoft Learn)
4) 一番ありがちな事故:「上書き(Lost Update)」😱🧻

衝突って聞くと「DBがエラー出してくれるのかな?」って思いがちだけど… いちばん怖いのは“静かに壊れる”タイプ😇
Lost Update の典型パターン🧨
「更新」って実装によっては、こうなりがち👇
- GET でデータを取る📥
- 画面で編集する📝
- PUT で“全体”を保存する📤
このとき、別の人が先に更新してても、あとから来た PUT が全体を上書きしちゃうことがある😱 (「自分が触ってないフィールドまで一緒に上書き」←これが本当に多い💦)
5) 「衝突が見えない」こともある👻🫥
分散DBによっては、衝突した瞬間に「はい衝突ね!」って表に出ず、 内部のルールで自動的に勝者を決めて終わり、という場合もあるよ。
たとえば Cosmos DB の Last-Writer-Wins(LWW)では、比較に使うパスが _ts(タイムスタンプ)などになっていて、最大値のものが勝つ。さらにこのポリシーで解決された衝突は、conflicts feed に出てこない(=気づきにくい)と説明されてるよ😳(Microsoft Learn)
※「最後に書いた人が勝つ」は分散の世界では昔からよくあるやり方で、Amazon Dynamo の論文でも “timestamp based reconciliation(LWW)” が出てくるよ🕰️(All Things Distributed)
6) 衝突が起きやすい“設計あるある”🧩💦
あるある①:1つのキーに更新が集中する🎯
- 在庫(人気商品)
- 予約枠(昼休みの時間帯)
- 注文のステータス(みんな触る) → ホットスポットになって衝突しやすい🔥
あるある②:ドキュメントを“丸ごと”更新する📄➡️🧨
- PUT で全フィールド送って保存 → 触ってない項目まで巻き込んで上書きしがち😇
あるある③:「加算」より「代入」が多い✍️
Stock = 10(代入)←衝突で壊れやすいStock -= 1(差分)←扱いやすい場面が増える(ただし万能じゃない)
7) “衝突の匂い”を検知する方法(チラ見せ)👃⚠️
第27章で本格的にやるけど、この章でも「どうやって気づくか」だけ先に肌感覚を付けよ〜✨
代表的な考え方:楽観的同時実行制御(Optimistic Concurrency)🕊️🔒
ざっくり言うと…
- 「ぶつかったらそのとき考える」
- だから更新時に“前提条件”をつけて確認する
AWS DynamoDB のドキュメントでも、バージョン番号を使う楽観ロックで「他者の書き込みで上書きされるのを防ぐ」説明があるよ🔢🛡️(AWS ドキュメント)
HTTPでもできる:ETag と If-Match 🏷️🧷
Web API の世界だと、更新時に 「あなたが編集したのは、このバージョンのデータだよね?」 を確認するために、条件付きリクエスト(If-Match など)を使えるよ。これは HTTP の仕様(RFC 7232)で定義されてる📜(IETF Datatracker)
8) ミニ演習:プロフィール更新で“上書き事故”を再現しよう🧑💻💦
この演習は CampusCafe の「プロフィール」っぽい例でやるよ😊 (ニックネームと通知設定、みたいに“別々の人が別々の項目を編集する”が起きやすい!)
8-1. まずは“事故る版”(ETagなし)😇🔥
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var store = new Dictionary<string, Profile>
{
["u1"] = new Profile("u1", "Mika", true, 1)
};
app.MapGet("/profiles/{id}", ([FromRoute] string id) =>
{
if (!store.TryGetValue(id, out var p)) return Results.NotFound();
return Results.Json(p);
});
// ❌ 事故る:同時更新を検知しない(後勝ちで上書き)
app.MapPut("/profiles/{id}", ([FromRoute] string id, ProfileUpdate dto) =>
{
if (!store.TryGetValue(id, out var cur)) return Results.NotFound();
// ありがちな「全体更新」ノリ(dtoにない項目も、いつか増えると危険!)
var next = cur with
{
Nickname = dto.Nickname,
NotificationEnabled = dto.NotificationEnabled,
Version = cur.Version + 1
};
store[id] = next;
return Results.Json(next);
});
app.Run();
public record Profile(string Id, string Nickname, bool NotificationEnabled, int Version);
public record ProfileUpdate(string Nickname, bool NotificationEnabled);
8-2. 事故の起こし方(2人が同時に編集した想定)📱💻
- まず GET(AさんもBさんも同じものを見る)📥
- Aさん:ニックネーム変更して PUT ✍️
- Bさん:通知設定変更して PUT 🔔
でも Bさんが 古い画面のまま PUT すると、 Aさんの更新が なかったことにされる(または逆)😱🧻
PowerShell例(雰囲気でOK)🪄
# GET
Invoke-RestMethod http://localhost:5000/profiles/u1
# Aが更新(ニックネームだけ変えるつもりだった…)
Invoke-RestMethod http://localhost:5000/profiles/u1 -Method Put -ContentType "application/json" `
-Body '{"nickname":"Mika-chan","notificationEnabled":true}'
# Bが更新(通知だけ変えるつもりだった…)
Invoke-RestMethod http://localhost:5000/profiles/u1 -Method Put -ContentType "application/json" `
-Body '{"nickname":"Mika","notificationEnabled":false}'
最後の1行、Bさんの送った nickname:"Mika" が混ざって
Aさんの Mika-chan が消える…みたいな事故が起きる😇🔥
8-3. “衝突が起きた”と気づける版(ETag/If-Match)👀🛡️
次章の内容に近いけど、「気づけるってこういうこと!」をちょい体験しよ✨ (HTTPの条件付き更新のイメージね📜(IETF Datatracker))
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var store = new Dictionary<string, Profile>
{
["u1"] = new Profile("u1", "Mika", true, 1)
};
static string MakeEtag(int version) => $"\"v{version}\"";
app.MapGet("/profiles/{id}", (HttpContext http, [FromRoute] string id) =>
{
if (!store.TryGetValue(id, out var p)) return Results.NotFound();
http.Response.Headers.ETag = MakeEtag(p.Version);
return Results.Json(p);
});
// ✅ 衝突を“検知”できる:If-Match が一致しないと 412
app.MapPut("/profiles/{id}", (HttpContext http, [FromRoute] string id, ProfileUpdate dto) =>
{
if (!store.TryGetValue(id, out var cur)) return Results.NotFound();
var ifMatch = http.Request.Headers[HeaderNames.IfMatch].ToString();
var curEtag = MakeEtag(cur.Version);
if (string.IsNullOrWhiteSpace(ifMatch) || ifMatch != curEtag)
{
// 「あなたが見てた版じゃないよ!」って返す
return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
}
var next = cur with
{
Nickname = dto.Nickname,
NotificationEnabled = dto.NotificationEnabled,
Version = cur.Version + 1
};
store[id] = next;
http.Response.Headers.ETag = MakeEtag(next.Version);
return Results.Json(next);
});
app.Run();
public record Profile(string Id, string Nickname, bool NotificationEnabled, int Version);
public record ProfileUpdate(string Nickname, bool NotificationEnabled);
これで Bさんが古い画面のまま更新しようとすると、412 になって止まる✋💥 「静かに壊れる」から「衝突に気づける」へ進化する感じだよ😊✨
9) ミニ理解チェック✅🧠
次のうち「衝突が起きやすい」ものはどれ?(複数OK)👀
- 1つの注文ステータスを複数サービスが更新する🚦
- 人気商品の在庫を大量アクセスが更新する📦🔥
- GET→編集→PUT(全体上書き)で更新する📄🧨
- 1つの画面からしか更新できない、単一端末のローカルアプリ📱
👉 1〜3 は起きやすい!4 は“分散してない”なら衝突は起きにくいよ😊
10) AI活用🤖🧠✨(衝突シナリオを量産!)
「衝突の想像力」を増やすと、設計がめちゃ強くなるよ💪🔥
そのまま投げてOKプロンプト例📮
- 「CampusCafeで衝突が起きるシナリオを10個。誰がどのデータをいつ更新して、どうズレるかまで書いて」☕🧩
- 「“上書き(Lost Update)”が起きるUI/API設計の典型パターンを5個。改善の方向性も一言」😇➡️🛡️
- 「“オフライン更新→再接続”で衝突する例を、ユーザー目線のストーリーで3本」📴➡️🌐
この章のまとめ🧁✨
- 衝突は「同じデータに別々の更新がぶつかる」こと💥
- 原因はだいたい 同時更新/オフライン合流/順不同+マルチライト🌍✍️
- いちばん怖いのは「衝突してるのに気づかず上書き」😱
- 次の章(第27章)では、LWW・マージ・手動解決みたいな「どう解く?」をちゃんとやるよ🔧🧩 (Cosmos DB でも LWW やカスタム解決の仕組みが整理されてるよ)(Microsoft Learn)