本記事について
当サイトを閲覧いただきありがとうございます。 本記事はシリーズ『生成AI時代のアーキテクチャ超入門』の「ソフトウェアアーキテクチャ」カテゴリ第6弾として、トランザクション設計について解説する記事です。
「全部成功するか、全部なかったことにする」を保証する仕組みで、銀行振込のようなお金が消えたり増えたりする事故を防ぐ設計の核です。本記事ではACID特性・分離レベル・分散トランザクション・結果整合性・Saga/Outboxパターン・CAP定理・冪等性まで解説し、業務ごとに整合性レベルを仕分ける判断軸を示します。
このカテゴリの他の記事
「お金が消えない」を支える仕組み
トランザクション設計は、システム全体の中で「どのデータがどの粒度の整合性を必要とするか」を決める判断です。銀行口座のように厳格な整合性が必要なデータもあれば、SNSの「いいね数」のように多少ずれても構わないデータもあります。全てに最強の整合性を適用するとシステムが重くなりすぎるため、業務ごとに求める精度を分けるのが設計の肝です。
全体構造(特にマイクロサービス)とセットで検討する必要があります。1DBで済むならACIDだけで十分で、分散トランザクションを避けられる構成に倒せるかどうかが、そのまま運用コストに直結します。
全体構造とセットで検討し、1DBで済むならACIDだけで十分です。
ACID特性
RDBMSが昔から提供してきた古典的なトランザクション保証がACIDです。頭文字を取った4つの性質が揃うことで、データの整合性を強く保証できます。
ACIDが重要視されるのは、アプリの最大の損失が「お金・在庫・注文のデータ不整合」で発生するからです。一度データがズレると原因追跡・補正・顧客対応のコストが膨大になり、事業全体の信頼を揺るがします。
ACIDは1970年代以降のRDBの発展とともに確立され、「1つのDBの中でなら整合性を強く保証できる」という前提で設計されています。そのためサーバー1台・DB1台の時代には完璧に機能しました。しかしクラウド時代の分散環境では、複数のDBやサービスを横断した処理でACIDを維持するには全ノードをロックする必要があり、ネットワーク障害で全体が止まるため現実的に運用できません。これが後述する結果整合性や Saga が登場した理由です。
| 特性 | 内容 |
|---|---|
| Atomicity(原子性) | 全部成功か全部失敗か。中途半端な状態は許さない |
| Consistency(一貫性) | 制約違反時は必ず失敗させる |
| Isolation(独立性) | 並行実行しても互いに干渉しない |
| Durability(持続性) | コミット後のデータは障害でも消えない |
単一DBの範囲ならACIDで十分手堅く、金融・会計・在庫等の強整合が必要な業務で定番です。
分離レベル
ACIDの中で現実的に性能とのトレードオフになるのが「I」(独立性)です。どこまで厳密に他のトランザクションから隔離するかを段階で選びます。厳しくするほど整合性は上がるが、並行性能は下がる。
| レベル | 許容する現象 | 性能 |
|---|---|---|
| READ UNCOMMITTED | 他のトランザクションの未コミット値が見える | 最速 |
| READ COMMITTED | 同じクエリで結果が変わる(ノンリピータブルリード) | 速い |
| REPEATABLE READ | 新しい行が途中で増える(ファントムリード) | 中 |
| SERIALIZABLE | すべて防止(最も厳格) | 遅い |
PostgreSQLのデフォルトは READ COMMITTED、MySQL(InnoDB)は REPEATABLE READ です。高分離は性能を犠牲にするので、必要な箇所だけ個別に強化します。
分散トランザクションの困難さ
マイクロサービスや複数DB構成では、複数のDBを横断したトランザクションが必要になる場面が出てきます。従来は 2相コミット(2PC、Two-Phase Commit、全DBに「準備OK?」と聞いてから一斉コミットさせる方式)で解決されていましたが、この方式はクラウド時代には致命的な欠点があります。
2PCは「全ノードが準備完了 → 全ノードがコミット」という2段階のやり取りを強制するため、どこか1ノードが止まると全体がブロックされます。クラウドではネットワーク障害が日常的に起きるため、2PCを本番運用するとサービス全体が頻繁に止まってしまい、「実質機能しない」のが実情です。
クラウドネイティブ時代の原則は2PCを避けること。代わりに結果整合性で設計します。
結果整合性(Eventual Consistency)
結果整合性は、「即座には整合しないが、最終的には整合する」という緩い保証を許容する設計です。書き込みは即座に成功させ、他のレプリカや他サービスへの伝播は非同期で行います。そのため、ユーザーの画面には一時的に古いデータが見える可能性があります。
銀行口座なら一瞬でもズレては困りますが、ECサイトの「いいね数」「フォロワー数」「商品レビュー数」などは、1秒古くても業務は成立します。このような「多少ズレても問題ないデータを結果整合性で扱う」ことで、システム全体の可用性とスケーラビリティを確保できます。
例:Amazonの在庫数 / Twitterのフォロワー数 / Instagramのいいね数 / YouTubeの再生回数
データの種類ごとに、強整合が必要か結果整合で十分かを業務要件から判断します。
Sagaパターン
Saga(サガ)は、分散トランザクションを「小さなローカルトランザクションの連鎖 + 補償処理」で実現する設計パターンです。ホテル予約のフローで考えると、「予約作成」「決済」「在庫確保」「通知送信」の各ステップは別々のサービスで実行されますが、途中で失敗した場合はそれまでの処理を取り消す補償処理を実行します。
sequenceDiagram
participant U as ユーザー
participant R as 予約サービス
participant P as 決済サービス
participant I as 在庫サービス
participant N as 通知サービス
U->>R: ホテル予約
R->>P: 決済要求
P-->>R: 決済成功
R->>I: 在庫確保
I-->>R: 在庫確保 NG
Note over R,I: 失敗 → 補償処理開始
R->>P: 補償: 返金
P-->>R: 返金完了
R-->>U: 予約失敗(在庫切れ)
2PCのように全サービスをロックせず、各ステップはローカルで完結するため、クラウド環境でも現実的に運用できます。ただし、補償処理の設計が不完全だと中途半端な状態が残るリスクがあります。
オーケストレーション型のSagaを擬似コードで書くと下記のような構造になります。各ステップが自前のローカルTXで完結し、失敗時に逆順で補償処理を呼ぶのがポイントです。
async function bookHotel(input: BookingInput) {
const completed: Array<() => Promise<void>> = [];
try {
const reservation = await reservationSvc.create(input);
completed.push(() => reservationSvc.cancel(reservation.id));
const payment = await paymentSvc.charge(input.amount);
completed.push(() => paymentSvc.refund(payment.id));
await inventorySvc.reserve(input.roomId);
completed.push(() => inventorySvc.release(input.roomId));
await notificationSvc.notify(input.userId);
return { ok: true, reservationId: reservation.id };
} catch (err) {
// 逆順に補償処理を実行(冪等性が前提)
for (const compensate of completed.reverse()) {
await compensate().catch(logCompensationFailure);
}
throw err;
}
}
補償処理は必ず冪等に作ります。リトライで二重に走っても結果が変わらない構造でなければ、サガ自体が新たな整合性事故の原因になります。実運用では Outbox パターンと組み合わせ、メッセージ送信と DB 更新を確実に同期させるのが定番です。
Sagaの2形態
Sagaには実装方法が2種類あり、処理の流れをどこで制御するかで分かれます。
| 方式 | 特徴 | 長所 | 短所 |
|---|---|---|---|
| Orchestration | 中央のオーケストレータが順序制御 | 流れが見えやすい・デバッグ容易 | 中央集約のリスク |
| Choreography | イベント駆動で各サービスが自律的に動く | 疎結合・スケールしやすい | 全体像が把握しにくい |
業務が複雑で処理順序が重要な場合は Orchestration、シンプルで各サービスが独立している場合は Choreography が使われます。
既定はまずOrchestrationから入ります。全体像が見えないシステムは運用が地獄になります。
Outboxパターン
Outbox(アウトボックス)パターンは、「DBへの書き込み」と「メッセージキューへの送信」を同一DBトランザクションで整合させる手法です。マイクロサービスで極めて頻繁に発生する「DBは成功したがKafka送信に失敗」(Kafka:分散型メッセージキュー基盤)という典型的なトラブルを防ぐための定番パターンです。
- ビジネスデータと一緒に
outboxテーブルに送信すべきメッセージを記録する - 別プロセス(Relay)が
outboxを読んでメッセージキューに送信する - 送信済みフラグを更新する
これにより、DBコミットと送信のどちらかだけが成功するという事故を防げます。マイクロサービスでイベント駆動アーキテクチャを組むなら、ほぼ必須のパターンです。
SagaとOutboxを組み合わせるのが定番で、マイクロサービスの必須パターンです。
CAP定理
分散システムアーキテクチャの根幹にある理論が CAP定理 です。Consistency(整合性)・Availability(可用性)・Partition tolerance(分断耐性)の3つを同時には満たせない、という制約を示しています。
ネットワーク分断は現実に起きるため「P」は事実上必須です。そのため実際の選択は「CP(整合性を優先)」か「AP(可用性を優先)」かの2択になります。業務要件によって「エラーを出してでも正確にしたい」か「古い値を見せてでも動き続けたい」かが変わります。
| 選択 | 代表システム | 業務例 |
|---|---|---|
| CP(整合性重視) | 従来のRDB・MongoDB(設定次第) | 銀行・決済・在庫管理 |
| AP(可用性重視) | DynamoDB・Cassandra・Redis | SNS・ECの閲覧・分析 |
選択は業務要件から逆算します。CAPを意識せずに分散DBを選ぶと、期待と違う挙動に悩まされます。
冪等性(Idempotency)
分散システムでは、ネットワーク障害やタイムアウトで「同じリクエストが複数回届く」ことが日常的に起きます。この時に何度実行されても結果が同じであることを保証する性質を冪等性と呼びます。
「決済API」が冪等でないと、再送時に二重課金が発生します。冪等性はクライアントがリクエストIDを付与し、サーバーがそのIDで重複排除することで実現するのが定番です。HTTPメソッドでは GET / PUT / DELETE は仕様上冪等、POSTは非冪等なので注意が必要です。
マイクロサービス・イベント駆動・リトライ処理では、冪等性の確保が必須で、後から入れるのは難しいため、最初から設計します。
判断基準:整合性レベルの決め方
トランザクション設計の核心は、業務ごとに必要な整合性レベルを見極めることです。全データに強整合を求めると性能が崩壊し、全てを結果整合にするとビジネスが壊れます。
判断の軸は「そのデータが一瞬でもズレたら金銭的・法的・信用上の被害が出るか」です。お金が動く処理・在庫の引き当て・税務や監査の対象になる記録は、1円・1個のズレも許されないため強整合が必須で、この部分に妥協すると事業そのものが揺らぎます。
一方、いいね数・閲覧数・レコメンド・ログ集計のような「多少ズレても業務が成立する」データは結果整合で十分で、むしろ強整合を適用すると性能とスケーラビリティが犠牲になります。業務担当者に「このデータが数秒ズレるとどう困るか」を具体的に聞いて逆算するのが最も確実です。
| 業務 | 必要な整合性 | 理由 |
|---|---|---|
| 銀行振込・決済 | 強整合(ACID) | ズレが金銭的被害につながる |
| 在庫管理 | 強整合 or Saga | 売り越しは返金・信頼失墜 |
| 注文履歴 | 強整合 | 税務・監査対応 |
| いいね数・閲覧数 | 結果整合 | 多少ズレても業務影響なし |
| レコメンド | 結果整合 | 古くても動けばOK |
| ログ・分析 | 結果整合 | リアルタイム性より規模優先 |
強整合はコストが高い要件で、本当に必要な業務だけに限定します。
データ種別×整合性レベルの実務段階表
※ 2026年4月時点の業界相場値です。テクノロジー・人材市場の変化で陳腐化するため、定期的にアップデートが必要です。
「全部強整合」が過剰で「全部結果整合」が危険なので、データの種類ごとに必要な整合性レベルを仕分けるのが実務の核心です。以下が業界の典型的な仕分けです。
| データ種別 | 整合性レベル | 理由 | 実装方式 |
|---|---|---|---|
| 銀行口座残高・決済 | 強整合(ACID・SERIALIZABLE) | 1円のズレが金銭被害 | 1DB内ACID |
| 注文・在庫引当 | 強整合 | 売り越しで返金・信用失墜 | 1DB内ACID or Saga |
| 会員登録・認証情報 | 強整合 | セキュリティ事故に直結 | 1DB内ACID |
| 税務・監査対象記録 | 強整合 + 改竄防止 | 法的要件 | ACID + WORM |
| カート情報 | 中間(READ COMMITTED) | 一時的ズレは許容 | 1DB内TX |
| いいね・閲覧数 | 結果整合 | 数秒ズレても業務成立 | 非同期集計 |
| レコメンド | 結果整合 | 古くても動けばOK | バッチ再計算 |
| ログ・分析データ | 結果整合 | リアルタイム性不要 | Kafka + DWH |
分離レベルの数値Gateは、PostgreSQL のデフォルト READ COMMITTED を基本に、「在庫引当・金融取引だけ SERIALIZABLE へ個別昇格」するのが定石です。SERIALIZABLE を全テーブルに適用すると並行性能が数倍遅くなるので、必要な箇所だけに絞ります。
強整合はコストが高い要件で、本当に必要な業務だけに限定します。
分散トランザクションの鬼門・禁じ手
マイクロサービス横断・複数DB横断のトランザクションで事故る典型を整理します。どれも本番でデータ不整合を生みます。
| 禁じ手 | なぜダメか |
|---|---|
| 2PC(Two-Phase Commit)をクラウドで本番運用 | ネットワーク障害で全サービスがブロック。実質的に機能しない |
| 冪等性キーなしでリトライ実装 | ネットワーク障害で二重決済・在庫二重減算・通知の複数送信が発生 |
| Saga の補償処理を設計しない | 中途半端な状態(決済OK・在庫NG)が残り、手作業での補正が必要に |
| DB書き込みとメッセージキュー送信を別TXで実行 | Outboxパターンなしだと「DB成功・Kafka送信失敗」で整合性破綻 |
全データに SERIALIZABLE 分離レベル | 並行性能が数倍遅くなる。必要な箇所だけに限定 |
| 楽観ロックなしで同時更新許容 | ロストアップデート(書き戻し事故)で1つの更新が消える |
| タイムアウト = 失敗と決めつけてリトライ | サーバー側では成功している可能性あり。冪等性キー必須 |
| DBレプリカからの読み取り直後に書き込み | レプリケーション遅延(数ms〜数秒)で古い値を上書き |
| 分散TXを自前実装 | Saga / Outboxを知らずに組むと必ず破綻。ライブラリ(Temporal等)に任せる |
Knight Capital 2012年事件は厳密には分散TXの事故ではありませんが、「古いコードが残った1台 + リトライ暴走」で45分・4.4億ドル損失・会社消滅というシナリオは、冪等性と整合性の設計を怠った時の恐ろしさを示しています(詳細は付録「重大インシデント事例集」)。
「分散 + リトライ + 冪等性なし」=二重処理の地雷三点セットです。
AI時代の視点
AI駆動開発が前提になっても、分散トランザクションの本質的な難しさはAIでも解決できません。むしろ、AIが短時間で大量のコードを生成できるほど、「ちゃんとトランザクションが張れているか」を人間が注意深く見る必要性が高まります。
AIは表面的に「正しく見えるコード」を生成しがちで、DB境界を跨ぐ整合性ミスを見逃すと、本番で初めてデータ不整合が発覚します。
| AI時代に有利 | AI時代に不利 |
|---|---|
| 1DB内ACIDトランザクションで完結 | 複数サービス跨ぎの手書きSaga |
| Outbox Pattern等の定番パターン | 独自実装の結果整合性 |
| DBベンダー機能に任せる(Aurora Global等) | アプリ層で分散TXを書く |
| スキーマと制約をコードで表現 | DBトリガーや隠れた整合性ルール |
AI時代の鉄則は「シンプルな設計に寄せ、AIが書く・読む範囲を超えない」こと。マイクロサービスで分散TXを組むより、モジュラーモノリス+1DBでACIDを徹底する方が、AI時代には圧倒的に運用しやすくなります。
AI時代は「分散TXを避ける設計」が賢い選択で、1DB完結のシンプルさが価値を持ちます。
よくある勘違い
- 「ACIDが効いてれば安全」 → ACIDは1DBの範囲でしか効きません。複数サービス跨ぎでは別の設計(Saga + Outbox)が必要
- 「SERIALIZABLEが一番安全」 → 厳しすぎる分離レベルは並行性能を壊滅させる。必要な箇所だけ個別に強化する
- 「結果整合性はエンジニアの甘え」 → 業務特性に合わせた選択で、甘えではありません。いいね数に強整合を適用する方が、むしろ設計の未熟
- 「リトライすれば何とかなる」 → 冪等性なしのリトライは二重処理の直接原因。リトライを入れる前に冪等性キーを設計する
「タイムアウト=失敗」と決めつけた日(業界事例)
決済APIのタイムアウト対策として、クライアント側に3回リトライを入れたプロジェクトで、結果的に少数のユーザーに同じ金額が重複請求される事故が起きた、という事例があります。サーバー側の処理は成功していたのに応答が詰まっただけで、リトライが全部通ってしまった、という話が聞かれます。
似た失敗を横目で見たことがある開発者は少なくないはずです。リトライの実装は楽観的に入りがちで、「たぶん失敗してるから再送しとこう」という発想が、本番の二重請求につながる典型です。
冪等性キーを入れていない限り、「タイムアウト=失敗」と決めつけて良い場面はほぼありません。ネットワーク越しの「成功したかどうか分からない」状態は、分散システムでは日常的に発生します。リトライを入れる前に冪等性キーを先に設計するのが鉄則です。
分散 + リトライ + 冪等性なし=二重処理の地雷。三点セットで初めて安全です。
決めるべきこと — あなたのプロジェクトでの答えは?
以下の項目について、あなたのプロジェクトの答えを1〜2文で言語化してみてください。曖昧なまま着手すると、必ず後から「なぜそう決めたんだっけ」が問われます。
- データごとの整合性要件(強整合 / 結果整合)
- 分離レベル(業務が許容する最低ライン)
- 分散トランザクションの扱い(Saga / Outbox / そもそも避ける)
- リトライ方針と冪等性の確保
- ロック戦略(楽観ロック / 悲観ロック)
- CAPのどちら(CP / AP)を優先するか
よくある失敗
- 全データに強整合を求めて性能崩壊 ― 「とりあえず安全側で」とSERIALIZABLEをかけると、並行性能が壊滅する
- 分散トランザクションを2PCで解こうとする ― クラウドでは2PCは実質機能しない。Saga + Outboxにする
- 補償処理が不完全で中途半端な状態が残る ― Sagaでは補償処理の設計が命。成功パスと同じくらい真剣に設計する
- リトライで冪等性を担保せず多重実行 ― ネットワーク障害で二重決済、在庫二重減算、通知の複数送信が起きる
「整合性はコストの高い要件」と認識します。本当に必要な部分だけ強整合にします。
最終的な判断の仕方
トランザクション設計は「業務がどれだけズレを許容するか」から逆算するのが核心で、技術的な好みで決めてはいけません。銀行振込のように1円のズレも許されない業務と、SNSの「いいね数」のように1秒ズレても誰も困らない業務を同じ強度で扱うと、システム全体が重くなりすぎて破綻します。
選定の核は「データごとに整合性レベルを仕分けること」に尽きる。強整合はコストが高い要件で、本当に必要な業務だけに限定します。ここを見誤ると、性能崩壊か業務破壊のどちらかが待っています。
現代の定石は「分散トランザクションを避ける設計」です。クラウド環境では2PCは実質機能せず、マイクロサービス跨ぎのトランザクションはSaga + Outboxで組むしかありませんが、その設計コストは非常に高い。
AI駆動開発では「表面上正しく見えるが整合性が壊れているコード」を生成しやすく、分散TXほど事故リスクが増えます。1DB内ACIDで完結させられるならそれが最強で、モジュラーモノリス+1DBの構成がAI時代には圧倒的に運用しやすくなる。分散が避けられない時だけ、Saga + Outbox + 冪等性の三点セットを正しく組みます。
選定の優先順位をまとめると次の通りです。
- 業務要件から逆算する(強整合が本当に必要な部分だけに限定)
- 1DB完結を最優先にする(分散TXはコストが高い)
- 分散が必要ならSaga + Outbox(2PCは避ける)
- 冪等性を必須条件にする(後付けは困難)
「整合性はコストの高い要件」必要な部分に集中させ、他は結果整合で軽く保ちます。
まとめ
本記事はトランザクション設計について、ACID・分離レベル・Saga・Outbox・CAP定理・冪等性まで含めて解説しました。如何だったでしょうか。
データごとに整合性レベルを仕分け、1DB完結を最優先、分散が必要ならSaga + Outbox + 冪等性の三点セット。これがAI時代も含めた2026年の現実解です。
次回はソフトウェアアーキテクチャカテゴリの最終記事、「認証・セッション」(サーバセッション/JWT/OAuth)について解説します。
シリーズ目次に戻る → 『生成AI時代のアーキテクチャ超入門』の歩き方
それでは次の記事も閲覧いただけると幸いです。
📚 シリーズ:生成AI時代のアーキテクチャ超入門(23/89)