【テクニカル・上級編】 トランザクション分離レベルの実装 – PostgreSQL

PostgreSQLの「分離レベル」という名の迷宮:MVCCとSSIが織りなす静かなる戦い

PostgreSQLを長く触っていると、ふと立ち止まって考えることがあります。「結局、我々が発行している `BEGIN` や `COMMIT` の裏側で、データベースは一体何を追いかけているのか?」と。

多くのエンジニアが「Read Committed」や「Serializable」という言葉を教科書的に理解していますが、PostgreSQLの内部実装、特にMVCC(多版同時実行制御)とSSI(Serializable Snapshot Isolation)の挙動まで踏み込むと、景色は一変します。今日は、この「一貫性の保証」という戦いの最前線について、少し深い話をしましょう。

1. Read Committed:スナップショットの「鮮度」を巡る戦い

PostgreSQLのデフォルトである `Read Committed`。これは一見シンプルですが、その裏では「文(Statement)ごとのスナップショット」が生成されています。

ここで注意すべきは、トランザクション全体ではなく、クエリ実行の瞬間に最新のコミット済みデータをスキャンしに行くという点です。つまり、同じトランザクション内で同じ `SELECT` を2回投げても、その間に他者が更新を確定させていれば、結果が異なる(Non-repeatable Read)ことが許容されます。

エンジニアが嵌まる罠:
長時間実行される複雑なクエリの最中に、更新対象の行が別セッションで更新された場合、PostgreSQLは「更新ロック」の解除を待機し、再評価を行います。この「待機と再評価」のコストは、高負荷時においてはデッドロックやパフォーマンス劣化の隠れたトリガーになります。

2. Repeatable Read:スナップショットの「固定」

`Repeatable Read` になると、ルールが変わります。トランザクション開始時にスナップショットが固定され、それ以降、他のトランザクションが何をしてこようと、そのスナップショットの時点で見えた世界だけが「真実」となります。

ここでのPostgreSQLの知恵は、「更新の競合」を検出する仕組みにあります。もし、あなたが読み込んだ行を、別のトランザクションが更新しようとした場合、PostgreSQLは即座にエラー(`40001: serialization_failure`)を吐き捨てます。これは「書き込みの衝突を許さない」という強い意志の表れです。

3. Serializableの真打ち:SSI(Serializable Snapshot Isolation)

さて、ここからが本題です。多くのDBがSerializableを実現するために「ロック」に頼り、パフォーマンスを犠牲にする中、PostgreSQLは SSI というエレガントな解法を採用しています。

SSIの凄みは、「ロックによる排他を最小限にしつつ、論理的な依存関係をグラフで追跡する」という点にあります。

  • SIREADロック(Predicate Lock):

PostgreSQLは「どんな条件で検索したか」を断片的なロックとして保持します。これは物理的な行ロックではなく、あくまで「読み込んだ範囲」をマークするようなものです。

  • 依存関係の追跡:

もし、「Aを読んでBを更新する」という操作がループ構造(依存関係のサイクル)を形成した場合、SSIはそれを「直列化不可能(Serializable)」と判断し、トランザクションを強制終了させます。

この仕組みの美しさは、競合が実際に起きるまではロックを待機させないことにあります。しかし、その代償として、アプリケーション側での「再試行(Retry)」の実装が必須となります。

パフォーマンストラブルシューティングの視点

実務において、これらの分離レベルを扱う際に意識すべきは、「SSIは魔法ではない」ということです。

1. Serialization Failureの頻発:
もし頻繁に `40001` エラーが出るなら、それは設計を見直すサインです。競合が多い箇所を無理やりSerializableにするのではなく、`SELECT … FOR UPDATE` で明示的にロックをかけるか、分離レベルを落とすのが定石です。
2. SSIのメモリオーバーヘッド:
`predicate locks` を保持するための領域(`max_pred_locks_per_transaction`)が不足すると、PostgreSQLはトランザクションを「ページ単位」でロックしようとします。これが引き金となり、本来競合していないトランザクションまで巻き込んでブロックが発生する「偽陽性(False Positive)」の性能劣化を招きます。

最後に:エンジニアとしての心構え

PostgreSQLの分離レベルは、単なる設定値ではなく、「データの一貫性とシステムの並列性との間の妥協点」を定義するものです。

「とりあえずSerializableにしておけば安全」というのは、SSIの実装を知る者からすれば、あまりに贅沢でリスキーな選択です。データベースが裏でどのようなグラフを描き、どの程度のコストを払って整合性を守ろうとしているのか。その想像力を持つことこそが、最高峰のエンジニアへの第一歩だと私は信じています。

皆さんのシステムで、今日もトランザクションが静かに、かつ確実に調和していますように。

コメント

タイトルとURLをコピーしました