同じデータへの処理は、順番待ちできる?行ロックを確かめる
「誰がアウト?」の更新処理には行ロックがあります。独立したPostgreSQLの小さな表で、二つ目の処理が待つことと、普通の読み取りまで全部止まるわけではないことを確かめました。
CONTENTSこの記事の目次5項目
「誰がアウト?」の提出処理を読むと、グループを取得するところでPESSIMISTIC_WRITEというロックを使っていました。古い版を拒否する楽観ロックとは、何が違うのでしょうか。専用の小さなDBで試すと、同じ行をロックしようとする二つ目の処理は、先の処理が確定するまで待ちました。
一つのグループを、判断の途中で追い越さない
実装では、参加・開始・提出などの処理に@Transactionalがあり、グループをfindLockedByIdで取得します。取得メソッドにはJPAのPESSIMISTIC_WRITEが指定されています。 提出では、その後に参加資格、現在の段階、送信済みか、同じ内容の提出があるかを確かめ、保存後に全員分がそろったかを判断します。同じグループを扱う処理の途中へ、別の同種の処理が入り込まないようにする意図を読めます。
実アプリと切り離した、一行だけの実験
今回は外部へ接続しない専用のPostgreSQL 17を起動し、idが1、値が0の架空の一行を作りました。実アプリのテーブルや本番DBは使っていません。JPAを経由せず、SQLのSELECT FOR UPDATEで待機を確認するモデル試験です。 接続Aはトランザクションを始め、その行をロックして値を1へ変更します。まだ確定しないうちに、接続Bも同じ行をSELECT FOR UPDATEで読もうとしました。
| 確認したこと | 観測結果 | 読み取れる意味 |
|---|---|---|
| Aが確定する前のB | wait_event_typeはLock | ロック待ちになった |
| その間の普通のSELECT | 0を取得 | 未確定の1を読んでいない |
| AのCOMMIT後のB | 1を取得 | 確定後の値から処理を再開 |
| Bが1を足して確定した後 | 最終値は2 | 二つの処理が順に反映された |
ロックしても、普通の読み取りはできた
試験は標準のREAD COMMITTEDで行いました。Aが保持する間、別の普通のSELECTは確定済みの0を返しました。「行をロックしたら、その行を読む処理が全部止まる」という結果ではありません。 PostgreSQLの行ロックは、同じ行への競合する更新やロック取得を待たせます。分離レベルや問い合わせ方が変われば条件も変わるため、今回の表をあらゆるDB・SQLへ広げません。
楽観ロックとは、確認する時点が違う
楽観ロックは、読んだ版と保存時の版が合うかを確かめ、古くなった更新を拒否する仕組みです。今回の悲観ロックの例は、トランザクション中に対象を押さえ、競合する取得を待たせます。 どちらも利用者の意図を自動調整するわけではありません。また、DB処理が終わるまでのロックであり、利用者が編集画面を開いている間ずっと占有する話でもありません。待つ対象と時間を具体的にすると、混同しにくくなります。
待たせれば、すべて解決するわけではない
ロックの範囲外を扱う処理まで一緒に止めるわけではありません。複数の行を違う順序で押さえると、互いを待つデッドロックも起こり得ます。長いトランザクションなら、待つ側の時間も長くなります。 今回は一行・二つの更新処理で待機と再開を確認しました。実サービス全体の同時実行やデッドロック防止を証明したものではありません。ロック名を覚えるだけでなく、どの行を、どの処理の終わりまで押さえるかを追うと、設計の役割が見えてきます。
参照:PostgreSQL公式「Explicit Locking」、Spring Data JPA「Locking」。実装はOutGroupRepositoryとOutGroupServiceを確認しました。
確認日:2026年9月22日。実装・検証結果・公式資料は確認時点のものです。