SELECT文でデッドロックになるパターン
PostgreSQL+Springを使用しているシステムにて、いくつかの特殊な状況が重なりデッドロックが発生した。
ざっくり言うと以下の順序で処理が発生した結果、デッドロックが発生。
- トランザクションAがSELECT * FROM user_attr;
- トランザクションBがTRUNCATE user_attr;
- トランザクションAを発生させたJavaプログラムが別のトランザクションCを発生させ、SELECT * FROM user_attr;
PostgreSQLのTRUNCATE周りの仕様
PostgreSQLのTRUNCATEはOracle/MySQLと違いロールバックが可能。
ただし、TRUNCATEの実行によりAccessExclusiveLockを獲得し、TRUNCATE実行中のSELECT文をブロックする。
PostgreSQLのSELECT仕様
SELECT文を実行すると、AccessShareLockを獲得する。
これはAccessExclusiveLock(=TRUNCATE)をブロックし、ブロックされるもの。
AccessShareLockは(ほかのロックもだけど)トランザクション終了まで保持し続ける。
Springにより1リクエスト当たり複数のトランザクションを発行するケース
TransactionalアノテーションのオプションであるPropagationの設定により、1リクエストの中で複数のトランザクションを発行する場合がある。
例えば業務処理Aと、ログ記録処理Bがあった場合、Aの結果に関わらずBをコミットした場合等に利用される。
今回はPropagation.REQUIRES_NEWを使用し、ログ記録処理を行っていた。
起こったこと
TRUNCATE実行時に、TRUNCATEが完了せず待ち状態に。
上記で言うところの業務処理A実行後、ログ記録処理B実行前にTRUNCATEが差し込まれた状態となり、デッドロック。
ロックの一端をJavaが握っている関係で、PostgreSQLのdeadlock_timeoutの検知が効かず、全てのリクエストがログ記録待ちでストップ。
対策
- そもそも運用中にTRUNCATEを使わない
- Propagation.REQUIRES_NEWを濫用しない。使う場合は通常のデッドロックにも注意が必要