VOLUME 03 / CHAPTER 02
二つの正しい処理が、間違った結果を作る
同時実行で崩れる判断と、障害後も残すべき確定結果を分ける。隔離レベル、ロック、MVCC、WALを、それぞれの保証から理解する。
出典照合日: · 独自解説
架空事例:青葉機材の機材状態と予約確定を扱う。二台の更新順序は同時実行の説明用モデルである。PostgreSQLの具体例は18の文書に照合し、一般的なSQLの概念と区別する。
どちらの担当者も、規則を確認した
倉庫には、点検に使う予備の測定器が二台ある。青葉機材では、少なくとも一台を使用可能な状態に残し、もう一台だけを調整作業へ回す規則を採用していた。
担当者甲は、二台とも使用可能であることを画面で確認し、Aを調整中へ変えた。同じころ担当者乙も、二台が使用可能と確認し、Bを調整中へ変えた。どちらも「もう一台が使えるから、自分の一台を外してよい」と判断したのである。
結果として、使用可能な測定器はゼロになった。
一つずつ順番に行えば、二人目は残り一台しかないと分かり、変更しなかったはずである。個々の処理が単独では正しくても、同時実行した結果が、そのどちらの順番にも対応しないことがある。
原子性は、他の処理を無関係にする性質ではない
原子性は、一連の更新をまとまりとして成功または失敗させるための性質である。Aの状態更新が半分だけ残ることを防いでも、別のトランザクションがBを変える判断まで自動的に調整するわけではない。
一貫性は、定めた制約や不変条件を守ることに関係する。だが、DBMSが「予備機を一台残す」という業務の事情を、記述なしに理解するわけではない。アプリケーションの判断と、データベースの制約、適切な同時実行制御を結び付ける必要がある。ACIDの四つを一括して「正しく動く」と覚えると、どの保証が不足したかを説明できない。1
今回のモデルでは、二つのトランザクションが別々の行を書き換えている。同じ行へ二人が古い値を書き戻す更新喪失とは、問題の形が違う。更新した行が衝突しなければ安全という判断では、複数行を読んだ結果に依存する業務規則を守れない。
読み直して同じなら、正しいとは限らない
複数版のデータを利用するMVCCは、同時実行中の読み取りと更新の干渉を減らす仕組みである。スナップショットとして一貫した見え方を得られる構成もある。しかし、安定した過去の状態を読むことと、複数の判断を直列実行と同じ結果へまとめることは異なる。1
予備機の例で、甲と乙がそれぞれ開始時の二台を見続け、最後に別の行を更新できるモデルを考える。甲が何度読み直しても、乙の更新が見えなければ、「Bは使える」という前提は崩れない。乙にも同じことが起きる。読み取りがぶれないこと自体が、業務規則の保証になるわけではない。
このような、共通に読んだ状態に基づいて異なる対象を書き換え、組合せで制約を破る現象はwrite skewを考える典型的な形になる。本章の二台の例では、両方が確定した結果は、甲の後に乙、乙の後に甲といういずれの逐次実行でも得られない。
PostgreSQL 18のRepeatable Readはスナップショット分離に基づき、ファントムリードを防ぐ一方、直列化異常まで禁止するものではない。SQLの隔離レベルが許容する現象の表と、製品が実際に許す現象を同じものとして暗記しない。PostgreSQLではRead Uncommittedを指定しても、Read Committedと同じ動作になる。2
異常の名前を、読んだ対象から区別する
まだ確定していない他の処理の値を読むのがダーティリードである。他の処理が後で取り消したなら、読み取った値は確定した事実にならない。
同じ行を読み直したとき、他の確定更新によって値が変わるのが非反復読取りである。一方、同じ条件で検索し直したとき、条件に合う行の集合が変わるのがファントムリードになる。いずれも、最初に何を読んだのかを具体的に置けば、名前を取り違えにくい。1
これらを防いだことと、あらゆる直列化異常を防いだことも同じではない。予備機の例のように、読み取りが安定したまま複数の更新結果が矛盾する場合がある。Serializableは、確定した並行処理の結果を、ある順番の直列実行と同等にする保証として理解する。すべての処理を物理的に一つずつ動かす実装しか認めないという意味ではない。2
また、ここでいう順番は、利用者がボタンを押した現実時間の順番を必ず保存するという保証とは別である。どの順序や時点の整合を要求しているかまで、同じ「一貫性」という語へ押し込めない。
強い隔離を選ぶと、失敗を扱う責任が生じる
PostgreSQL 18のSerializableでは、直列化と矛盾する依存を検出した場合にトランザクションを失敗させることがあり、アプリケーションは再試行を扱う必要がある。失敗した最後のUPDATEだけでなく、判断のための読み取りを含むトランザクション全体をやり直す。2
予備機の例なら、最初の読み取りを使ったまま書込みだけ再実行すると、「もう一台使える」という古い前提を保存してしまう。やり直した処理は、新しい状態を確認し、必要なら調整の依頼を不成立にする。
このとき、トランザクションの途中でメールや外部の確定操作を実行していたら、それらが再試行で重複しないかも考える必要がある。データベースのロールバックが、外部へ送信済みのメールを回収するわけではない。DB内の処理のまとまりと、外部副作用の境界を分ける。
分離を強くすれば常に一定割合遅くなる、という一般式もない。競合の頻度、処理の長さ、アクセスする範囲、再試行の費用によって結果は変わる。必要な保証を満たす候補を選び、その条件で性能を測る。
ロックする対象と、判断する対象を揃える
別の案として、予備機の管理単位に対応する共通の記録をロックし、その範囲の状態確認と変更を順に行うことを考える。甲が判断している間に乙が同じ管理単位を変更できなければ、二人とも古い「二台」を前提に確定することを防ぐ方向になる。
一方、更新する自分の一行だけをロックしても、相手が別の行を更新するなら、今回の判断は調整されない。ロックの存在ではなく、守りたい条件に関係する処理が同じ制御へ従っているかを見る。
悲観的に先に制御する方法に対し、版番号などを用いて更新時に競合を検出する方法もある。ただし、版を確認した後に無条件で書き込むだけでは、間に別の更新が入る可能性がある。条件の確認と更新が一体として成立する仕組みへつなげなければならない。1
MVCCを使うDBMSでも、ロックがなくなるわけではない。PostgreSQLの行ロックは競合する更新やロック要求を待たせるが、通常の読み取りまで同じように止めるとは限らない。共有ロック、排他ロックという一般的な説明を、製品のすべての読取り動作へそのまま当てはめない。3
お互いが、相手の終了を待つ
甲の処理がAを確保してBを待ち、乙の処理がBを確保してAを待つと、どちらも先へ進めない。待ちの向きを描けば、循環が現れる。これがデッドロックの基本形である。
すべての関係する処理が同じ順序で対象をロックするようにすれば、このような逆順取得による循環を避けられる。ただし、一部の経路だけ順序を揃え、別の更新が逆順のままなら、保証にはならない。途中でロックの強さを変える場合なども含めて考える。3
トランザクションを短くすることは、資源を待つ時間や競合の機会を減らす助けになる。しかし、短ければ理論上デッドロックが起きないわけではない。発生後に一方を取り消して回復することと、発生する条件を事前に除くことも区別する。
利用者が画面を眺めている間、更新ロックを保持し続ける設計には注意が必要である。判断に必要な短い区間だけをDBのトランザクションにし、利用者の長い操作については版や業務状態を使って扱うなど、異なる時間幅を分けて考える。
COMMITの成功を、何が支えているか
予約を確定した直後に電源が落ちた。メモリ上の更新がデータファイルへ全部書かれていなくても、確定済みの結果を回復できるようにするため、WALを利用する。
WALの基本は、変更されたデータページを永続化するより先に、それを回復するためのログを永続化するという順序である。PostgreSQLの説明では、データページへ未反映の変更をログからREDOできるため、毎回のコミットで変更した全データページを書き切る必要がなくなる。4
これは、アプリケーションが「予約を保存しました」とログへ一行書くだけの仕組みではない。DBMSとストレージが、回復に必要な記録と書込み順序を守るための仕組みである。一般的な教科書で扱うUNDOとREDOの説明を、どのDBMSにも同じ内部実装があるという意味で読み替えない。
さらに、永続性の保証は設定によって変わる。PostgreSQL 18で非同期コミットを選ぶと、WALの永続化を待たずに成功を返し、直後のクラッシュで最近の確定結果を失う可能性がある。これは単なる応答時間の調整ではなく、成功の意味を変える選択である。5
青葉機材では、営業が成功応答を受けた後に顧客へ機材を確約する。したがって、その成功に依存してどんな外部行為が続くかを考え、許せるデータ損失と整合した設定を選ぶ。速さだけを測って設定を変えると、利用者の理解と障害後の記録が食い違う。
知識を答案へ戻す
午前Ⅱ。 ACIDの四要素、三つの読取り異常、直列化可能性、MVCC、ロック、WALの保証対象を分ける。隔離レベルの標準上の許容と特定DBMSの実装は同じとは限らない。COMMITの成功と、永続化を待つ設定を関連付けて読む。
午後Ⅰ。 単独では正しい処理が並行したとき、どの古い前提を共有するかを追う。同じ行の更新衝突だけでなく、複数行を読んで別の行を変える判断にも着目する。問題文が示していないDBMSの固有動作を勝手に前提にしない。
午後Ⅱ。 強い隔離を選んだなら、どの業務規則を守り、失敗時の再試行や外部副作用をどう扱ったかまで説明する。ロックを採用したという名称だけで、整合性を証明したことにはならない。
立ち止まって考える:別々の行しか更新しないなら、同時に確定してよいか
複数行を読んだ結果に依存する業務規則があると、別々の行への更新でも組合せで違反する。予備機の例では両者がもう一台を当てにしている。読む対象、判断の条件、書く対象を一緒に追い、必要な隔離や共通の制御を選ぶ。
出典と照合範囲
[1] コンプリートテキスト第7・13・18章、IPA「SAシラバス Ver.5.2」5-5、10-3、11-3:ACID、読取り異常、MVCC、ロック、設計の保証対象を基礎とした。二台の測定器の同時更新は独自のモデルである。公式シラバス
[2] PostgreSQL 18 Documentation, 13.2 Transaction Isolation:Read Uncommittedの扱い、Repeatable Readとスナップショット、Serializableの保証と全体再試行を照合した。SQL標準については同公式解説に基づく比較であり、標準規格の全文を参照したとはしていない。公式文書
[3] PostgreSQL 18 Documentation, 13.3 Explicit Locking:行ロックの対象、デッドロック、取得順序、長時間の保持に関する説明を参照した。公式文書
[4] PostgreSQL 18 Documentation, 28.3 Write-Ahead Logging:ログとデータページの永続化順序、REDOによる回復を照合した。公式文書
[5] PostgreSQL 18 Documentation, 28.4 Asynchronous Commit:成功応答とWAL永続化の間の損失リスクを参照した。本文は設定変更の推奨ではない。公式文書