SKIP LOCKED
SKIP LOCKED
は PostgreSQL 9.5 から入った新機能です。
これを使うと、FOR UPDATE
の際に別トランザクションによって行ロックが取得されているレコードを除外することができます。すなわち、他のトランザクションによる行ロックが解除されるのを待つ必要がなくなります。
サンプル
idというカラムを持つidsというテーブルを作成し、3レコード作成しておきます。
CREATE TABLE ids AS SELECT generate_series(1, 3) AS id;
testdb=> CREATE TABLE ids AS SELECT generate_series(1, 3) AS id; SELECT 3 testdb=> SELECT * FROM ids; id ---- 1 2 3 (3 rows)
1つめのトランザクションでは、FOR UPDATE
でid=1の行ロックを取得します。
BEGIN; SELECT * FROM ids WHERE id = 1 FOR UPDATE;
testdb=> BEGIN; BEGIN testdb=> SELECT * FROM ids WHERE id = 1 FOR UPDATE; id ---- 1 (1 row)
2つめのトランザクションでは、全行をFOR UPDATE
で取得します。id=1のレコードがロックされているので、全行取ろうとしてもロックが競合して取得できません。(競合することがわかるようにNOWAIT
で待ち合わせないようにしています)
BEGIN; SELECT * FROM ids FOR UPDATE NOWAIT;
testdb=> BEGIN; BEGIN testdb=> SELECT * FROM ids FOR UPDATE NOWAIT; ERROR: could not obtain lock on row in relation "ids"
このクエリにSKIP LOCKED
を付けると、ロックされているid=1のレコードを除外して取得できるようになります。
SELECT * FROM ids FOR UPDATE SKIP LOCKED;
testdb=> SELECT * FROM ids FOR UPDATE SKIP LOCKED; id ---- 2 3 (2 rows)
SKIP LOCKEDを利用してキューを実装
これが便利なのは、テーブルをキューとして使うときだと思います。というか、それ以外で使い道を思いついていません。
SKIP LOCKED
を使うことによって、他のトランザクションの完了を待たずにロックしていない行を取得できるので、キューとしての性能を高めることが出来ます。
ということで、キューとしての使用方法を試してみます。queuesというテーブルを作り、idの昇順で取り出すことにします。
CREATE TABLE queues ( id SERIAL, message TEXT, PRIMARY KEY(id) ); INSERT INTO queues(message) SELECT 'message' || id FROM generate_series(1, 3) AS id;
testdb=> CREATE TABLE queues ( testdb(> id SERIAL, testdb(> message TEXT, testdb(> PRIMARY KEY(id) testdb(> ); CREATE TABLE testdb=> INSERT INTO queues(message) SELECT 'message' || id FROM generate_series(1, 3) AS id; INSERT 0 3 testdb=> SELECT * FROM queues; id | message ----+---------- 1 | message1 2 | message2 3 | message3 (3 rows)
ORDER BY
とLIMIT 1
で優先度順で先頭の1件取り出し、FOR UPDATE SKIP LOCKED
を付けてあげるだけです。
1つめのトランザクションで実行します。
BEGIN; SELECT * FROM queues ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
testdb=> BEGIN; BEGIN testdb=> SELECT * FROM queues ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; id | message ----+---------- 1 | message1 (1 row)
1つめのトランザクションが完了する前に、2つめのトランザクションで同じSQLを実行します。1つめのトランザクションの完了を待たされること無く、LOCKされていない行の中から優先度が高い行を取得できます。
BEGIN; SELECT * FROM queues ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
testdb=> BEGIN; BEGIN testdb=> SELECT * FROM queues ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; id | message ----+---------- 2 | message2 (1 row)
ただ、これだとqueuesテーブルから取り出したレコードを消せていないので、取り出した行を別途DELETE
してあげる必要があります。
そのまま同一のトランザクション内で取り出したレコードに対してDELETE
するといった方法もありますが、RETURNING
を使うことによって、該当行の内容取得とDELETE
をひとつのクエリで実行できます。
DELETE FROM queues WHERE id = (SELECT id FROM queues ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED) RETURNING *;
testdb=> DELETE FROM queues testdb-> WHERE id = (SELECT id FROM queues ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED) testdb-> RETURNING *; id | message ----+---------- 2 | message2 (1 row) DELETE 1
RETURNING
は、INSERT
やUPDATE
、DELETE
で処理したレコードの情報を取得する構文です。シーケンスによって払い出された値をINSERT
の戻りで取得したいときなど、とても便利です。
ということで、とても手軽にキューが出来ました。
終わりに
似たような方法で、Advisory Locks(pg_try_advisory_lock
)を使う方法がありますが、ORDER BY
と組み合わせるとpg_try_advisory_lock
を全行評価、すなわち全行ロックしてしまうので、優先度と組み合わせた取り出しが難しいです。
SKIP LOCKED
だと、そういった制約も回避できるので、9.5以降ならばSKIP LOCKED
を使った形のほうが簡単ではと思っています。