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

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