3

次のようなタスクを含む分散タスク キューがあります。

# creates a uniquely-named file
new_path = do_work()

old_path = database.query('select old path')
unlink(old_path)

database.query('insert new path')

ここに競合状態があります: タスク キュー ソフトウェアがこれらのタスクの 2 つをまったく同時に開始した場合、それらは両方ともold_pathデータベースから同じものを取得し、競合敗者のリンク解除呼び出しは失敗します (敗者の新しいパスを将来のリンク解除から孤立させます)。 )。

このレースを回避するためにこれを構成する方法はありますか? 必要に応じて、この現在のデザインから何でも捨てることができます。具体的には、PostgreSQL、Python、Celery を使用しています。おそらくテーブル全体のロックを使用したり、psycopg2 のトランザクション レベルを SERIALIZABLE に変更したりできることは理解していますが、この競合状態を回避できるかどうかはわかりません。テーブルレベルのロックは、追加のタスクごとに新しいテーブルを導入する必要があることも意味します (それらが互いにブロックしないようにするため)。これはあまり魅力的ではありません。

4

3 に答える 3

5

PGQなど、この問題を既に解決しているツールを調べることを強くお勧めします。待ち行列は予想以上に大変です。これは、再発明したい車輪ではありません。

同時実行は難しい

Mihai の答えは表面的には問題ないように見えますが、同時操作ではやや落ちます。

2 つの同時 UPDATE で同じ行を選択できます (この例では がありますused_flag = FALSE)。そのうちの 1 つがロックを取得して続行します。もう1つは、最初の実行とコミットまで待機します。コミットが発生すると、2 回目の更新でロックが取得され、その状態が再チェックされ、一致する行が見つからなくなり、何も実行されません。したがって、一連の同時更新の 1 つを除くすべてが空のセットを返す可能性があります (実際には非常に可能性が高い)。

READ COMMITTEDモードでもまともな結果を得ることができます。これは、単一のセッションが継続的にループするのとほぼ同じですUPDATE。モードでは絶望的にSERIALIZABLE失敗します。それを試してみてください; セットアップは次のとおりです。

CREATE TABLE paths (
    used_flag boolean not null default 'f',
    when_entered timestamptz not null default current_timestamp,
    data text not null
);

INSERT INTO paths (data) VALUES
('aa'),('bb'),('cc'),('dd');

これがデモです。ステップバイステップに従って、3 つの同時セッションで試してください。READ COMMITTED で一度実行してから、プレーンの代わりにSERIALIZABLE使用してすべてのセッションでもう一度実行します。結果を比較します。BEGIN ISOLATION LEVEL SERIALIZABLEBEGIN

SESSION 1             SESSION2         SESSION 3

BEGIN;
                                       BEGIN;

UPDATE      paths
    SET     used_flag = TRUE
    WHERE   used_flag = FALSE
    RETURNING data;

                      BEGIN;

                      INSERT INTO
                      paths(data)
                      VALUES
                      ('ee'),('ff');      

                      COMMIT;               
                                       UPDATE      paths
                                           SET     used_flag = TRUE
                                           WHERE   used_flag = FALSE
                                           RETURNING data;


                      BEGIN;

                      INSERT INTO
                      paths(data)
                      VALUES
                      ('gg'),('hh');      

                      COMMIT;        

COMMIT;

最初の UPDATEはREAD COMMITTED成功し、4 つの行が生成されます。2 番目は、最初の更新の実行後に挿入およびコミットされた残りの 2 つのeeandを生成します。コミット後に実際に実行されても、2 回目の更新では返されません。これは、行が既に選択されており、挿入されるまでにロックを待機しているためです。ffgghh

単独ではSERIALIZABLE、最初の UPDATE が成功し、4 つの行が生成されます。2 番目は で失敗しERROR: could not serialize access due to concurrent updateます。この場合、SERIALIZABLE分離は役に立たず、失敗の性質を変えるだけです。

明示的なトランザクションがなければ、s が同時に実行されたときに同じことが起こりUPDATEます。明示的なトランザクションを使用すると、タイミングをいじることなく簡単にデモを行うことができます。

1 行だけを選択する場合はどうでしょうか。

上記のように、システムは問題なく動作しますが、最も古い行のみを取得したい場合はどうすればよいでしょうか? は、ロックでブロックする前に操作対象の行を選択するため、特定の一連のトランザクションで 1 つのみが結果を返すUPDATEことがわかります。UPDATE

次のようなトリックを考えます。

UPDATE      paths
    SET     used_flag = TRUE
    WHERE entry_id = (
        SELECT entry_id
        FROM paths 
        WHERE used_flag = FALSE
        ORDER BY when_entered
        LIMIT 1
    )
    AND used_flag = FALSE
    RETURNING data;

また

UPDATE      paths
    SET     used_flag = TRUE
    WHERE entry_id = (
        SELECT min(entry_id)
        FROM paths 
        WHERE used_flag = FALSE
    )
    AND used_flag = FALSE
    RETURNING data;

しかし、これらは期待どおりには機能しません。同時に実行すると、両方が同じターゲット行を選択します。1 つは続行し、1 つは最初のコミットまでロックでブロックし、次に続行して空の結果を返します。2番目がなければ、AND used_flag = FALSE重複を返すことさえできると思います! 上記のデモ テーブルentry_id SERIAL PRIMARY KEYに列を追加してから試してください。paths彼らをレースにLOCK TABLE paths参加させるには、3回目のセッションで。次のリンクにある例を参照してください。

これらの問題について別の回答で書きました。私の回答では、複数のスレッドが制約付きセットで重複した更新を引き起こす可能性があります

真剣に、PGQをチェックしてください。これはすでに解決されています。

于 2012-09-17T23:21:45.080 に答える
1

古いパスを選択する代わりに、次のようにします。

old_path = database.query('
    UPDATE      paths
        SET     used_flag = TRUE
        WHERE   used_flag = FALSE
        RETURNS data');

このRETURNS句を使用すると、更新したばかりの行 (/deleted/inserted) から値を「選択」できます。

used_flag、その行が別の Python インスタンスによって既に使用されているかどうかを指定します。ビットを使用するWHERE used_flag = FALSEと、既に使用されているものを選択していないことを確認できます。

于 2012-09-17T20:03:20.080 に答える
0

タスク キュー ソフトウェアが要求に一意の識別子を提供できる場合は、各要求の old_path を別の行に格納できる可能性があります。そうでない場合は、リクエストごとにキーを生成し、パスを保存することができます。

于 2012-09-17T20:03:33.860 に答える