56

私はある種のキューイングメカニズムを構築しています。処理が必要なデータの行とステータスフラグがあります。私はupdate .. returningそれを管理するために句を使用しています:

UPDATE stuff
SET computed = 'working'
WHERE id = (SELECT id from STUFF WHERE computed IS NULL LIMIT 1)
RETURNING * 

ネストされた選択部分は更新と同じロックですか、それともここに競合状態がありますか?もしそうなら、内側の選択は?である必要がありselect for updateますか?

4

2 に答える 2

43

Erwinの提案は、正しい動作を取得するための最も簡単な方法である可能性がありますが(40001の例外が発生した場合にトランザクションを再試行する限りSQLSTATE)、アプリケーションのキューイングは、その性質上、リクエストがブロックされて順番を変える機会がない場合にうまく機能する傾向があります。トランザクションのPostgreSQL実装よりもキューSERIALIZABLE。これにより、より高い同時実行性が可能になり、衝突の可能性についていくらか「楽観的」になります。

問題のクエリ例は、現状では、デフォルトのREAD COMMITTEDトランザクション分離レベルで、2つ(またはそれ以上)の同時接続がキューから同じ行を「要求」することを許可します。何が起こるかはこれです:

  • T1が開始し、UPDATEフェーズで行をロックするところまで到達します。
  • T2は実行時にT1とオーバーラップし、その行を更新しようとします。T1のCOMMITまたはの保留中をブロックします。ROLLBACK
  • T1はコミットし、行を正常に「要求」します。
  • T2は行を更新しようとし、T1がすでに持っていることを検出し、新しいバージョンの行を探し、それがまだ選択基準を満たしていることを検出し(これはまさにid一致します)、行を「要求」します。

正しく機能するように変更できます(FOR UPDATEサブクエリで句を許可するバージョンのPostgreSQLを使用している場合)。FOR UPDATEIDを選択するサブクエリの最後に追加するだけで、次のようになります。

  • T1が起動し、IDを選択する前に行をロックします。
  • COMMITT2は実行時間でT1とオーバーラップし、T1のまたはが保留中のIDを選択しようとしているときにブロックしますROLLBACK
  • T1はコミットし、行を正常に「要求」します。
  • T2が行を読み取ってIDを確認できるようになるまでに、T2はそれが要求されていることを確認するため、次に使用可能なIDを見つけます。

REPEATABLE READまたはトランザクション分離レベルではSERIALIZABLE、書き込みの競合によりエラーがスローされます。これをキャッチして、SQLSTATEに基づくシリアル化の失敗であると判断し、再試行してください。

一般にSERIALIZABLEトランザクションが必要であるが、キューイング領域での再試行を回避したい場合は、アドバイザリロックを使用してそれを実行できる可能性があります。

于 2012-07-19T20:35:34.573 に答える
28

あなたが唯一のユーザーである場合、クエリは問題ないはずです。特に、クエリ自体の内部(外部クエリとサブクエリの間)に競合状態やデッドロックはありません。ここでマニュアルを引用します:

ただし、トランザクションがそれ自体と競合することはありません。

同時使用の場合、問題はより複雑になる可能性があります。SERIALIZABLEあなたはトランザクションモードで安全な側にいるでしょう:

BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE stuff
SET    computed = 'working'
WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
RETURNING * 
COMMIT;

このような場合は、シリアル化の失敗に備えてクエリを再試行する必要があります。

しかし、これがやり過ぎではないかどうかは完全にはわかりません。@kgrittnに立ち寄ってもらいます..彼は並行性とシリアル化可能なトランザクションのエキスパートです..

そして彼はそうしました。:)


両方の長所

デフォルトのトランザクションモードでクエリを実行しますREAD COMMITTED

Postgres 9.5以降の場合は、を使用しますFOR UPDATE SKIP LOCKED。見る:

古いバージョンの場合computed IS NULLは、外側の条件を明示的に再確認してUPDATEください。

UPDATE stuff
SET    computed = 'working'
WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
AND   computed IS NULL;

@kgrittnが彼の回答へのコメントでアドバイスしたように、このクエリは、(ありそうもない)同時トランザクションと絡み合った場合に、何もしなくても空になる可能性があります。

したがって、トランザクションモードの最初のバリアントとほぼ同じように機能しSERIALIZABLE、パフォーマンスを低下させることなく再試行する必要があります。

唯一の問題:機会のウィンドウが非常に小さいため、競合が発生する可能性は非常に低いですが、負荷が高い場合に発生する可能性があります。最終的に行が残っていないかどうかはわかりません。

それが問題ではない場合(あなたの場合のように)、ここで完了です。
もしそうなら、絶対に確実に、空の結果を得た後、明示的なロックでもう1つのクエリを開始します。これが空になったら、完了です。そうでない場合は、続行します。plpgsql
では次のようになります。

LOOP
   UPDATE stuff
   SET    computed = 'working'
   WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL
                LIMIT 1 FOR UPDATE SKIP LOCKED);  -- pg 9.5+
   -- WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL LIMIT 1)
   -- AND    computed IS NULL; -- pg 9.4-

   CONTINUE WHEN FOUND;  -- continue outside loop, may be a nested loop

   UPDATE stuff
   SET    computed = 'working'
   WHERE  id = (SELECT id FROM stuff WHERE computed IS NULL
                LIMIT 1 FOR UPDATE);

   EXIT WHEN NOT FOUND;  -- exit function (end)
END LOOP;

これにより、パフォーマンスと信頼性の両方の長所が得られるはずです。

于 2012-07-18T02:49:47.310 に答える