ACID の各保証がデータベース設計とアプリ挙動にどう影響するかを解説。原子性・一貫性・隔離性・耐久性、トレードオフ、実例、分散システムでの扱い方まで。

食料品の支払い、航空券の予約、口座間の送金などで、結果が明確であることを期待します:成功するか、失敗するかのどちらか。データベースも同じ確実性を目指します — 多数のユーザーが同時に使い、サーバが落ち、ネットワークが不安定でもです。
トランザクション はデータベースが一つの「パッケージ」として扱う単位作業です。複数のステップ(在庫を引く、注文レコードを作る、カードに課金する、レシートを書く等)を含んでいても、一貫した一つのアクションのように振る舞うことが期待されます。
どれか一つが失敗したら、システムは半端な状態を放置するのではなく、安全なポイントまで巻き戻すべきです。
部分更新は単なる技術的不具合ではなく、カスタマーサポート案件や財務リスクになります。たとえば:
これらはデバッグが難しく、見た目は「だいたい正しい」一方で帳尻が合わなくなります。
ACID は多くのデータベースがトランザクションに対して提供できる四つの保証の略です:
特定のデータベース製品や単一のスイッチではなく、振る舞いに関する約束事だと考えてください。
強い保証は通常、データベースがより多くの作業をすることを意味します:追加の調整、ロックによる待ち、バージョンの追跡、ログへの書き込みなど。負荷下ではスループット低下やレイテンシ増加を招くことがあります。目標は常に「常に最大の ACID」ではなく、ビジネスリスクに合った保証を選ぶことです。
原子性はトランザクションが一つの単位として扱われることを意味します:完全に終わるか、まったく効果がないかのどちらか。データベース上に「半端な更新」が見えることはありません。
Alice から Bob に $50 を送るとします。内部的には少なくとも二つの変更が必要です:
原子性があれば、これら二つの変更は同時に成功するか同時に失敗します。片方だけ実行される事態(Alice からは引かれたが Bob に入らない、あるいはその逆)を防げます。
データベースはトランザクションに二つの出口を与えます:
便利な比喩は「下書き vs 公開」です。トランザクション実行中の変更は仮のもので、コミットして初めて公開されます。
原子性が重要なのは失敗が普通に起きるからです:
これらがコミット完了前に発生しても、原子性があれば DB はロールバックでき、部分的な作業が実残高に漏れるのを防ぎます。
原子性は DB 状態を守りますが、ネットワーク切断でコミットがあったか不明なときなど、アプリ側は不確実性を扱う必要があります。
実務的に有用な補助:
原子トランザクションと冪等なリトライを合わせると、部分更新や二重請求の両方を避けられます。
ACID における一貫性は「データが合理的に見える」という曖昧な意味ではありません。トランザクションはあなたが定義したルールに従って、常に有効な状態から別の有効な状態に遷移しなければなりません。
データベースは明示的な制約、トリガー、不変条件に対してのみ一貫性を守れます。ACID はこれらのルールを作らず、あくまでトランザクション実行時にそれらを守る役割を果たします。
よくある例:
order.customer_id は存在する顧客を参照していることこれらが存在すれば、データベースはそれらを破るトランザクションを拒否します。
アプリ側検証は UX を良くし複雑なビジネスルールを扱えますが、単独では不十分です。
典型的な失敗モード:アプリ内で「メールは使えるか」をチェックしてから挿入するフロー。並行実行では二つのリクエストが同時にチェックを通り、結果的に重複が発生することがあります。最終的には DB の一意制約が一つだけ成功させることでしか安全性を担保できません。
「残高は負にならない」を制約(あるいは単一トランザクション内で確実に保つロジック)としてエンコードしていれば、その不変条件を破る転送は全体が失敗します。どこにもルールを書いていないなら、ACID はそれを守れません — 守るべき定義がないからです。
一貫性は最終的に“明示化”の問題です:ルールを定義し、トランザクションにそれを守らせる。
隔離性はトランザクション同士が干渉しないようにします。あるトランザクションが進行中でも、他のトランザクションは半端な作業を見たり、上書きしたりしないべきです。目標は、他のユーザーが同時に活動していても「ひとりで実行したかのように」各トランザクションが振る舞うことです。
実システムは忙しい:顧客は注文し、サポートはプロファイルを編集し、バックグラウンドジョブは支払いを照合する——これらが同時に起きます。これらは同じ行(口座残高、在庫数、予約スロットなど)に触れることが多いです。
隔離がなければ、タイミングがビジネスロジックの一部になってしまいます。たとえば「在庫を引く」更新が別のチェックアウトと競合したり、レポートが途中の読み取りをして存在しない状態の数字を表示したりします。
「ひとりで実行したかのように振る舞う」完全隔離はコストが高くなることがあります。スループット低下、待ち(ロック)の増加、再試行の発生などです。一方で、多くのワークロードは最も厳密な保護を常に必要としません(昨日の分析結果を読むような処理は多少のズレを許容できます)。
そのため、DB は隔離レベルを提供し、どの程度の一貫性リスクを許容するかを調整できます。
隔離が弱いと以下の古典的な異常に遭遇します:
これらの失敗モードを理解すると、自分のプロダクトが要求する隔離レベルを選びやすくなります。
隔離性は他のトランザクションをどの程度「見てよいか」を決めます。弱い隔離だとユーザーにとって驚きとなる動作が現れます。
ダーティリード:他トランザクションの未コミット変更を読んでしまう現象。
例:Alex が $500 を送金し、残高が一時的に $200 になる。あなたがその $200 を読み、その後 Alex のトランザクションが失敗しロールバックされる。
ユーザーへの影響:誤った残高表示、誤検知した不正フラグ、サポート対応の間違い。
ノンリピートリード:同じ行を二度読んだとき、別トランザクションのコミットにより値が変わること。
例:注文合計を $49.00 と読み、少し後にリフレッシュしたら $54.00 になっている(割引行が削除されたため)。
ユーザーの印象:チェックアウト中に合計が変わったと感じ、不信や離脱につながる。
ファントムリード:行の集合に関するノンリピートの拡張。別トランザクションがマッチする行を挿入/削除するため、二回目のクエリで結果の行数が変わる。
例:ホテル検索で「3室空き」と表示され、予約の再確認時にゼロになる(新しい予約が入ったため)。
ユーザー影響:二重予約の試行、在庫や空席の誤表示、過販売。
ロストアップデート:二つのトランザクションが同じ値を読み、それぞれ更新して最後に保存したほうが勝つ。
例:管理者二人が同じ商品の価格を編集。どちらも $10 から始め、一方は $12、もう一方は最後に $11 を保存すると $12 が消える。
ライトスキュー:二つのトランザクションがそれぞれ個別には有効な変更を行い、結果としてルールを破ること。
例:ルール「オンコールは最低1人必要」。二人の医師が互いがまだオンコールだと確認してからオフにすると、結果的に誰もオンコールでなくなる。
強い隔離は異常を減らしますが、待ち・再試行・コストを増やします。多くのシステムは読み取りが多い分析系には弱めの隔離を選び、送金や予約など正しさが重要なフローにはより厳しい設定を使います。
隔離は他のトランザクションが実行中にあなたのトランザクションが何を見られるかに関するものです。DB はこれを 隔離レベル として公開しており、高いレベルほど驚きが少ない代わりにスループットや競合が増えます。
多くのチームはユーザー向けアプリで Read Committed を標準にしています:性能が良く、ダーティリードが防がれるため期待に合うことが多いです。
ただし、名前は標準化されていても正確な挙動は DB エンジンや設定によって異なります。自分の DB で重要な異常が発生するかを必ずテストしてください。
耐久性はトランザクションが コミットされたら その結果がクラッシュ後も残ることを意味します。アプリが顧客に「支払い成功」と伝えたら、その情報を次の障害でデータベースが忘れてはなりません。
多くのリレーショナル DB は WAL(先行書き込みログ) を使います。高レベルでは、DB はコミットと見なす前に変更の逐次的な“領収書”をディスクに書きます。クラッシュ時には起動時にログを再生してコミット済みの変更を復元します。
復旧時間を現実的にするために、DB は チェックポイント を作ります。チェックポイントでは最近の変更のかなりの部分をメインのデータファイルに書き出し、復旧時に再生すべきログの量を限定します。
耐久性は単純なオン/オフではありません。どれだけ積極的に安定したストレージへの強制(fsync 等)を行うかで変わります。
基盤となるハードウェアも影響します:SSD、RAID コントローラのライトキャッシュ、クラウドボリュームは障害時の挙動が異なります。
バックアップやレプリケーションは復旧やダウンタイム短縮に役立ちますが、耐久性そのものではありません。トランザクションがプライマリ上で耐久化されていても、レプリカにまだ到達していないことがあります。バックアップは通常スナップショットであり、コミットごとの保証とは別です。
トランザクション開始からコミットまで、DB は多くの要素を調整します:誰がどの行を読めるか、誰が更新できるか、同時に同じレコードを変更しようとする時にどうするか。
衝突への対処は大きな設計判断です:
多くのシステムはワークロードや隔離レベルに応じてこれらを組み合わせます。
現代の DB はしばしば MVCC(マルチバージョン同時実行制御) を使います。行の単一コピーではなく複数バージョンを保持します:
これにより大量の読み書きを同時にこなせることが多いですが、書き込み間の競合は依然として解決が必要です。
ロックはデッドロックを生み得ます:トランザクション A が B のロックを待ち、B が A のロックを待つと循環が発生します。
DB は通常このサイクルを検出し、一方を中止してエラーを返します("デッドロック犠牲者")。アプリはそのエラーを受けてリトライするべきです。
ACID を実現することで摩擦が生じているとき、次のような兆候がしばしば見られます:
これらはトランザクションサイズ、インデックス、隔離/ロッキング戦略の見直しが必要であることを示します。
ACID は単なる理論ではなく、API 設計、バックグラウンドジョブ、UI フローに影響します。核心は:どのステップを一緒に成功させるべきか決め、それらだけをトランザクションに入れることです。
良いトランザクショナル API は通常一つのビジネスアクションに対応します。たとえば /checkout は注文作成、在庫の確保、支払いインテントの記録を行い、これらは一つのトランザクションにまとめるべき場合が多いです。
一般的なパターン:
こうすると原子性と一貫性を保ちつつ、遅く壊れやすいトランザクションを避けられます。
トランザクション境界の置き方は「単位作業」が何かによります:
ACID は助けになりますが、アプリは依然として障害を正しく扱う必要があります:
長いトランザクション、トランザクション内での外部 API 呼び出し、ユーザーが操作を考えている間にロックを保持すること(例:「カート行をロックしてユーザー確認を待つ」)を避けてください。これらは争奪を増やし隔離の競合を招きます。
短期間でトランザクショナルなシステムを作るとき、最大のリスクは ACID を知らないことではなく、一つのビジネスアクションを複数のエンドポイントやジョブ、テーブルに散らしてしまうことです。
プラットフォーム(例:Koder.ai)は設計を加速しつつ ACID に沿った構築を支援できます:ワークフローを記述すれば React UI と Go + PostgreSQL のバックエンドを生成し、スナップショットやロールバックでスキーマやトランザクション境界を素早く試行できます。データベースが保証を守る点は変わりませんが、正しい設計から実装へ至る時間を短縮できます。
一つの DB ならトランザクション境界内で ACID を提供できますが、作業を複数のサービスや DB に広げると同じ保証を保つのは難しくコストがかかります。
厳格な一貫性は常に「最新の確定した真実」を返すことを意味し、高可用性は部品が遅れても応答し続けることを意味します。マルチサービスでは、ネットワーク障害時に全員の合意を待つ(より一貫だが可用性低下)か、短時間のズレを受け入れる(可用性優先)かを選ぶ必要があります。どちらが正しいかはビジネスが許容できるミスに依存します。
分散トランザクションは制御外の境界にまたがる調整を要します:ネットワーク遅延、リトライ、タイムアウト、サービスクラッシュ、部分障害など。たとえ各サービスが正しくても、ネットワークがあいまいさを生じさせます。たとえば決済サービスがコミットしたが注文サービスが確認を受け取れない場合など。このあいまいさを解消するには二相コミットのような調整プロトコルが必要で、遅くなり可用性が下がり運用が複雑になります。
保証を緩めるときは以下の条件が満たされる場合が多い:
リスクを管理するには不変条件(絶対に破ってはいけないルール)を定義し、操作を冪等に設計し、タイムアウトとバックオフを組み込み、ドリフト(スタックしたサガ、繰り返される補償、増え続けるアウトボックスなど)を監視することです。真に重要な不変条件(例:「口座を使い果たさない」)は可能なら単一サービスと単一 DB トランザクション内に収めてください。
ユニットテストでは正しいトランザクションが、実際のトラフィックや再起動、並行処理下では破綻することがあります。ACID 保証と本番の振る舞いを一致させるためにこのチェックリストを使ってください。
まず「常に真でなければならないこと」を書き出します。例:「口座残高は常に 0 以上」「注文合計は明細の合計と等しい」「在庫は 0 未満にならない」「支払いは正確に1つの注文に紐づく」など。これらは製品ルールとして扱い、データベースの細部ではなくプロダクトのルールとして定義してください。
次に何を 1 トランザクション内に収めるかを決めます。決定要素:
トランザクションは小さく保つ:触る行を減らし、外部 API コールを避け、すばやくコミットする。
並行性を第一級のテスト軸にしてください。
リトライをサポートするなら冪等性キーを明示し、「成功後にリクエストを繰り返した場合」をテストに含める。
保証が高コスト化したり脆弱になってきた徴候を監視してください:
トレンドにアラートを張り、単発のスパイクではなく傾向に基づいて対応し、問題の発生源となるエンドポイントやジョブに紐づける。
不変条件を保護するのに十分な最弱の隔離レベルを使い、デフォルトで最大にしないこと。お金の移動や在庫減算など小さいが重要な臨界区間があるなら、トランザクションをそこだけに狭め、他は外に出す。
ACID は障害や同時実行下でもデータベースが予測可能に振る舞うための一連のトランザクション保証です:
トランザクションはデータベースが「ひとつのパッケージ」として扱う単位作業です。複数の SQL 文を実行しても(例:注文作成、在庫減算、支払いIntentの記録など)最終的には次のいずれかの結果になります:
部分的な更新は現実の矛盾を生み、修復に大きなコストがかかります。例:
ACID(特に原子性と一貫性)はこうした「半端な」状態が真実として見えるのを防ぎます。
原子性はデータベースが“半端に終わった”トランザクションを決して公開しないことを保証します。コミット前に何かが失敗した場合(アプリのクラッシュ、ネットワーク切断、DB再起動など)は、トランザクションはロールバックされ、途中の変更が永続化されるのを防ぎます。
実務的には、二つの残高を更新するような複数ステップの変更を安全に扱えるのは原子性のおかげです。
クライアントがレスポンスを受け取れずにコミットされたか不明な場合があるため、ACID だけでは十分でない場面があります。そこで次を組み合わせます:
これにより部分更新や二重請求/二重処理を避けられます。
ACID の“一貫性”は「データが合理的に見える」という意味ではなく、トランザクションがあなたの定めたルール(制約、トリガー、不変条件)に従って有効な状態から別の有効な状態へ移ることを意味します。
そのルール自体をどこかに定義していなければ、ACID は守るべき基準を持てません。例えば「残高は負になってはいけない」というルールをどこかに定義して初めて、それを破るトランザクションは拒否されます。
アプリケーション側の検証はユーザー体験向上に有益ですが、並行実行下では不十分なことがあります(例:同時に二つのリクエストが「メールは使える」と判断する)。
データベース制約は最終防衛線です:
両方を組み合わせるのが現実的な設計です(前段でアプリ検証、最終的に DB が担保)。
隔離性はトランザクションが他と同時に実行されている間に何を“見られる”かを制御します。弱い隔離では次のような異常が発生します:
隔離レベルはこれらのリスクと性能のトレードオフを調整するための手段です。
多くの OLTP アプリでは Read Committed(コミット済み読み取り) をデフォルトとすることが現実的です。ダーティリードが防げてパフォーマンスも良い妥協点だからです。
状況に応じて上げます:
ただし、データベースエンジンごとに挙動が異なる場合があるため、必ずドキュメントで確認し、実際にテストしてください。
耐久性はトランザクションがコミットされた後、その結果がクラッシュ(電源喪失、プロセス再起動、機械の突然のリブート)に耐えて失われないことを意味します。顧客に“支払い成功”と伝えたなら、その事実を次の障害でデータベースが忘れてはいけません。
多くのリレーショナル DB は WAL(先行書き込みログ) を使い、コミット前に変更の“領収書”を順次ログに書き込みます。起動時にこのログを再生してコミット済みの変更を復元します。
ただし、耐久性は設定やストレージに依存します(同期 fsync を待つかどうか、クラウドのボリュームや RAID キャッシュの挙動等)。
トランザクションを BEGIN して COMMIT する際、DB は誰がどの行を読み書きできるか、競合が起きたらどうするか等を調整しています。
主要な方式の一つは競合制御の選択です:
現代の多くの DB は MVCC(マルチバージョン同時実行制御) を使い、読み取りは古いスナップショットを参照することで読み取りが書き込みをブロックしにくくしています。ただし書き込み同士の競合は解決が必要です。
ロックはデッドロック(待ちのループ)を生み得ます。DB はサイクルを検出して一方を中止(デッドロック犠牲者)にし、アプリはリトライすべきです。
ACID の保証はアプリ設計にも影響します。基本方針は「どのステップを一緒に成功させる必要があるか」を決め、それをトランザクションにまとめることです。
設計パターンの例:
これにより原子性と一貫性を保ちながら、長時間ロックや外部 API 待ちといった脆弱性を避けます。
また、トランザクション内で外部 API を呼ぶ、あるいはユーザーの待ち時間を含めるのはアンチパターンです。サービス境界を跨ぐ場合は一つの ACID トランザクションで覆えないため、アウトボックスやサガといった別の手法が必要になります。
単一のデータベース内では ACID を保ちやすいですが、複数サービスや複数 DB にまたがると難易度とコストが上がります。
分散では一貫性(最新の真実を常に返す)と可用性(パーツが遅くても応答し続ける)のトレードオフを現場で感じます。ネットワーク障害時に全員の合意を待つか(より一貫)、一部サービスを受け入れ差分を許容するか(より可用)を選ぶ必要があります。
分散トランザクション(例:2フェーズコミット)は調整コストが高く、可用性や運用複雑度を下げます。実務では:
重要不変条件(例:「残高を超えて支出させない」)は可能な限り単一サービスと単一 DB のトランザクション内に保つのが安全です。