DBトランザクション|ACID・分離レベル・ロック・障害回復を追う
在庫を減らした直後に予約の登録が失敗したら、どちらか一方だけを残してはいけません。二人が最後の在庫を同時に見つけた場合も、成功は一人までです。トランザクションは一連の処理の成功・失敗をまとめ、並行処理と障害の下でデータの意味を守る単位です。
この記事で理解すること
ACIDの四つの性質と、業務ルール・DB制約・アプリ処理の分担を説明する。
分離レベルによる読み取りの違い、ロックの競合、更新の欠落を実行順から判断する。
デッドロック時の再実行、ログを使った障害回復、成功不明時の再送を設計する。
ここではDBに予約と在庫を保存する架空の販売サービスを扱います。SQLiteで実行する例と、一般的なロックの概念、PostgreSQLの挙動を区別します。複数スレッドの同期や優先度逆転そのものは並行処理の主担当テーマで、この記事はDB固有の整合性と回復に範囲を絞ります。
ACID:まとめる範囲を業務から決める
予約確定では、商品id501の在庫を一つ減らし、予約行を一つ登録します。この二つを同じDBトランザクションへ入れ、両方成功したときだけCOMMITします。途中で失敗したらROLLBACKし、予約なしに在庫だけ減る状態を残さないようにします。
性質 | 意味 | この事例で守ること |
|---|---|---|
A:原子性 | 全体を成功か不成功として扱う | 在庫減算と予約登録を片方だけ残さない |
C:一貫性 | 定めた整合性の条件を保つ | 在庫非負・参照整合・予約の業務規則 |
I:独立性/隔離性 | 並行実行の干渉を制御 | 同時予約で最後の在庫を二重販売しない |
D:永続性 | 確定した結果を障害後も保持 | 成功した予約を適切なログ・保存で回復 |
一貫性は、DBが未定義の業務ルールを自動で発見する意味ではありません。DBの制約、適切なSQL、アプリの判断を組み合わせて条件を守ります。CHECK(quantity >= 0)だけでは、二人とも成功と通知する処理の誤りまで解決できません。
COMMITの永続性には、ログ同期やストレージの設定等の前提があります。耐久性を緩める設定を選べば、障害時の保証も変わります。トランザクションを使うという言葉だけで、機器全損、誤削除、外部サービスの失敗まで回復できるとは考えません。
更新の欠落:読んだ値を後で書く落とし穴
在庫が10のときAとBが別々に10を読み、どちらも一個引いた9をそのまま書いたとします。両予約を受け付けていたら正しい残りは8ですが、保存された値は9です。後の書込みが前の変化を上書きする更新の欠落です。
一連の予約全体の整合性を守らない読み取りと書込みの概念例です。DBMSの実際の分離・ロックで結果は変わります。
対策の一つは、現在値に対する条件付き減算をDBで行うことです。更新した行数が1なら確保成功、0なら在庫不足等で確保失敗とします。更新の成功を確認せず予約を作ると、条件を付けても業務は壊れます。
UPDATE stock
SET quantity = quantity - 1
WHERE id = 501 AND quantity >= 1;在庫1なら、先に成功した減算で0になり、後の減算は条件を満たさなくなります。SQLiteでは同時書込みが直列化され、PostgreSQLのRead Committedでも競合更新後に条件を再評価します。別の実装・分離レベルでは中断して再試行する場合もあるため、成功・0行・例外をすべて扱います。
同一トランザクションへ、成功条件を含める
BEGIN;
-- 以下のUPDATEは掲載の条件付き減算
UPDATE stock SET quantity = quantity - 1
WHERE id = 501 AND quantity >= 1;
-- アプリは更新行数を確認する。
-- 0ならROLLBACKして予約を作らない。
-- 1なら予約INSERTへ進む。
INSERT INTO reservation(id, stock_id, request_key)
VALUES (1001, 501, 'request-a');
COMMIT;コメントにある分岐はアプリの責任であり、SQLをそのまま続けるだけでは条件を守れません。INSERTで参照・一意制約等に失敗した場合は、予約全体をROLLBACKします。DBMSによって文の失敗後にトランザクション全体が中断状態になるか、当該文だけが取り消されるかも違います。
処理を小さく保ち、トランザクションの中で利用者の入力待ちや外部HTTP通信を長時間待たないようにします。待っている間もロックや読み取り状態を保持し、他の処理に影響します。ただし短くするために、まとめるべき在庫減算と予約登録を別々に確定してはいけません。
読み取りの異常と、分離レベルの範囲
ダーティリードは他者の未確定変更を読むこと、ノンリピータブルリードは同じ行を再読して確定値が変わること、ファントムリードは同じ条件で再検索した対象行集合が変わることです。値の変化と集合の変化を区別します。
標準的な分離レベル | 許し得る現象の概略 |
|---|---|
Read Uncommitted | ダーティ・再読の変化・ファントム |
Read Committed | ダーティは禁止。再読の変化・ファントムはあり得る |
Repeatable Read | ダーティと再読の変化は禁止。標準ではファントムはあり得る |
Serializable | 直列実行と整合しない結果を防ぐ |
表はSQL標準の概念です。PostgreSQLではRead UncommittedはRead Committedとして動作し、Repeatable Readはファントムも防ぐスナップショットを使います。しかしRepeatable Readでも、複数行をまたぐ業務条件の判定が直列実行と同じ結果になるとは限りません。
Serializableは、確定した並行実行の結果が何らかの直列実行と同じになることを目指す保証です。全処理を常に一人ずつだけ動かす実装とは限らず、矛盾し得る処理を中断してアプリに再試行を要求する方式もあります。DBMS名と分離レベルを確認して判断します。
共有・専有ロックとMVCCを区別する
概念的な共有ロックSは読み取りを守り、専有ロックXは更新を守ります。同一対象ではS同士は共存、SとXおよびX同士は競合します。ただし実際のDBMSは表・行・範囲など複数の対象とロック種別を持ち、名前だけでは互換性を判断できません。
単純なS/Xモデルです。実製品の全ロック互換表を表す図ではありません。
MVCCは複数の版を使い、読み取り側へ条件に合う版を見せる方式です。PostgreSQLの通常のSELECTは行の共有ロックを取る単純なモデルとは異なり、通常の更新と並行して読み取れます。それでも更新同士や明示的なSELECT FOR UPDATEには競合・待ちがあります。
読んだ行を他者に変更されない条件で扱いたい場合は、DBMSの行ロックや適切な分離を使います。楽観的な方法なら、読み取り時のversionを更新条件へ含め、更新行数0を競合として再判断します。以下はversion3を読んで在庫9へ変更する例で、単なる再送で上書きを続けないことが大切です。
UPDATE stock
SET quantity = 9, version = version + 1
WHERE id = 501 AND version = 3;デッドロック:待ちの輪を断ち、全体をやり直す
Aは商品501をロックして502を待ち、Bは502をロックして501を待つと、互いに解放を待つ輪ができます。これがデッドロックです。単に一人が長く処理しているロック待ちとは違います。DBは検出して一方を中断するなどの方法を取ります。
商品IDの昇順など、複数対象の取得順序を統一すると輪を作りにくくなります。トランザクションを短くし、索引で不要な対象への処理も減らします。中断された場合は途中の文だけでなく、業務判断を含むトランザクション全体を上限付きで再試行します。入力の不備や制約違反まで無条件に再試行してはいけません。
障害回復:ログ・チェックポイント・バックアップ
WALは、変更対象のデータページを永続化する前に、対応する回復用ログを永続化する原則です。確定した変更がデータファイルへまだ反映されていなくても、障害後にログから再現できるようにします。チェックポイントは回復の起点を管理する仕組みです。
一般的なログ回復の説明では、必要な確定変更をREDOし、未確定変更をUNDOして整合を回復すると考えます。ただしPostgreSQLのMVCCでは、未確定版を可視にしない等の方法も使うため、全製品が同じUNDO処理を持つとは説明しません。
バックアップは復元の基盤であり、WALはその代用品ではありません。ログの保管・復元手順を組み合わせ、必要な時点へ戻せるか訓練します。論理的に確定した誤削除は通常の再起動回復でも確定変更として残るため、別の時点復元や業務上の訂正が必要です。
成功不明と再送:DBの原子性だけでは足りない
COMMITした直後に通信が切れると、DBで成功したのに利用者は結果を受け取れません。同じ予約を単純に再実行すると、在庫を二回減らす恐れがあります。利用者の一回の操作に対応するrequest_keyを一意に保存し、再送は既存の結果を返す設計を検討します。
request_keyは外部から届く操作の識別子で、内部の主キーは整数のままです。予約だけの一意制約を付けても、再送で在庫を減らしてから失敗し、その減算をROLLBACKしなければ不整合になります。認証済みの利用者、要求内容、同じキーの意味も照合します。
外部決済やメール送信は、一つのローカルDBのROLLBACKでは取り消せません。確定時に送信待ち行を同じDBトランザクションへ入れるoutbox、外部側の冪等処理、失敗時の補償などが必要になります。二重配送も考えて、DB内と外部の成功の範囲を分けて設計します。
演習1:一部だけの成功
条件:在庫減算は成功したが、同じ予約処理のINSERTが一意制約に違反した。
問い:在庫だけ減った状態を残さないため何をするか。
解答例:同じトランザクションをROLLBACKし、予約全体を失敗として扱う。
根拠と誤答の確認:文単位の失敗だけで在庫減算が必ず取り消されるとは限りません。
演習2:最後の一個
条件:在庫1へ二回の掲載の条件付き減算を行う。更新行数で成功を判定する。
問い:成功件数と残数を答える。
解答例:成功は1回、後の更新は0行、残数は0。
根拠と誤答の確認:現在値の条件をDBで判定することと、0行なら予約を作らないことが必要です。
演習3:分離レベル
条件:Read Committedで同じ行を二回読み、間で他者が更新してCOMMITした。
問い:二回目の値が違うことを何と呼ぶか。
解答例:ノンリピータブルリード。
根拠と誤答の確認:未確定値ではないのでダーティリードではありません。
演習4:取得順序
条件:Aは501→502、Bは502→501の順で専有ロックを取る。
問い:デッドロックを減らす対策を答える。
解答例:両者の取得順序を同じ商品IDの昇順等へ統一する。
根拠と誤答の確認:待ちの輪の発生条件を減らします。短縮だけで必ずなくなるとはしません。
演習5:楽観的競合
条件:version3で読んだAとBが掲載のversion条件付き更新を行い、Aが先に成功した。
問い:Bの更新行数と扱いを答える。
解答例:0行。versionが変わった競合として読み直し、業務判断をやり直す。
根拠と誤答の確認:新しい値へ無条件で上書きしてはいけません。
演習6:成功不明
条件:予約はCOMMIT済みだが応答が切れた。利用者が同じ操作を再送する。
問い:二重予約を防ぐため何を照合するか。
解答例:同じ操作のrequest_keyと利用者・要求内容を照合し、確定済みの結果を返す。
根拠と誤答の確認:通信失敗はDBのROLLBACKが済んだという意味ではありません。
参照資料とこの記事の範囲
事例・数値・図・演習は独自に作成した教材です。用語の範囲はIPAシラバス、仕組みは以下の一次資料で確認しました。特定年度の問題を読んでいなくても学べます。製品固有の動作と一般的な原理は本文で区別します。
関連テーマを続けて学ぶ
正規化とデータモデル変更|関数従属・サブタイプ・制約・DDLをつなぐ
応用SQLと索引|ウィンドウ関数・再帰問合せ・実行効率を読む
この記事についてAIに深掘り質問する
ChatGPT、Claude、Perplexityにこの記事を参照させ、要点の確認や疑問点を自由に質問できます。
次におすすめの学習
編集・検証について
編集・検証:IT資格ラボ編集部
IPAが公開する試験要綱・シラバス・過去問題と、各技術の公式資料を優先して内容を確認しています。制度変更や誤りを確認した場合は、記事を見直して更新します。
編集方針・情報源・訂正方針を見る