web-dev-qa-db-ja.com

複数のコンシューマーを備えたSQLテーブルとしてのジョブキュー(PostgreSQL)

私は典型的な生産者と消費者の問題を抱えています:

複数のプロデューサーアプリケーションが、PostgreSQLデータベースのジョブテーブルにジョブリクエストを書き込みます。

ジョブ要求には、作成時にQUEUEDを含む状態フィールドが始まります。

multipleコンシューマーアプリケーションがあり、プロデューサーが新しいレコードを挿入したときにルールによって通知されます。

CREATE OR REPLACE RULE "jobrecord.added" AS
  ON INSERT TO jobrecord DO 
  NOTIFY "jobrecordAdded";

状態をRESERVEDに設定して、新しいレコードを予約しようとします。もちろん、消費者だけが成功するべきです。他のすべてのコンシューマは同じレコードを予約できないはずです。代わりに、state = QUEUEDで他のレコードを予約する必要があります。

例:一部のプロデューサーが次のレコードをテーブルjobrecordに追加しました:

id state  owner  payload
------------------------
1 QUEUED null   <data>
2 QUEUED null   <data>
3 QUEUED null   <data>
4 QUEUED null   <data>

現在、2つのコンシューマ[〜#〜] a [〜#〜][〜#〜] b [〜#〜]それらを処理したい。彼らは同時に走り始めます。 1つはID 1を予約し、もう1つはID 2を予約する必要があります。次に、最初に終了する人がID 3を予約する、というように続きます。

純粋なマルチスレッドの世界では、mutexを使用してジョブキューへのアクセスを制御しますが、コンシューマーは異なるマシンで実行される異なるプロセスです。それらは同じデータベースにのみアクセスするため、すべての同期はデータベースを介して行われる必要があります。

PostgreSQLでの同時アクセスとロックに関するドキュメントをたくさん読んだ。 http://www.postgresql.org/docs/9.0/interactive/explicit-locking.htmlPostgresqlでロックされていない行を選択PostgreSQLおよびロック

これらのトピックから、次のSQLステートメントで必要なことを実行できることがわかりました。

UPDATE jobrecord
  SET owner= :owner, state = :reserved 
  WHERE id = ( 
     SELECT id from jobrecord WHERE state = :queued 
        ORDER BY id  LIMIT 1 
     ) 
  RETURNING id;  // will only return an id when they reserved it successfully

残念ながら、これを複数のコンシューマープロセスで実行すると、約50%の時間で、同じレコードが予約され、処理と、一方が他方の変更を上書きします。

何が欠けていますか?複数のコンシューマーが同じレコードを予約しないようにするには、SQLステートメントをどのように記述する必要がありますか?

35
code_talker

ここで私の投稿を読んでください:

Postgresqlでのロックと更新の選択による一貫性

トランザクションとLOCK TABLEを使用する場合は問題ありません。

5
jordani

FIFOキューにもpostgresを使用します。元々はACCESS EXCLUSIVEを使用しましたが、高い同時実行性で正しい結果が得られますが、pg_dumpと相互に排他的であり、ACCESS SHAREを取得するという残念な影響があります。実行中にロックします。これにより、next()関数が非常に長い時間(pg_dumpの期間)ロックされます。これは、24時間365日のショップであり、顧客がキューのデッドタイムを気に入らなかったため、受け入れられませんでした真夜中。

私は、pg_dumpの実行中にロックされず、並行して安全である、より制限の少ないロックが必要であると考えました。私の検索により、このSO投稿につながりました。

その後、調査を行いました。

queuedからジョブのステータスを更新するFIFO queue NEXT()関数には次のモードで十分ですtorunning同時実行に失敗せず、pg_dumpに対してブロックしません:

SHARE UPDATE EXCLUSIVE
SHARE ROW EXCLUSIVE
EXCLUSIVE

クエリ:

begin;
lock table tx_test_queue in exclusive mode;
update 
    tx_test_queue
set 
    status='running'
where
    job_id in (
        select
            job_id
        from
            tx_test_queue
        where
            status='queued'
        order by 
            job_id asc
        limit 1
    )
returning job_id;
commit;

結果は次のようになります。

UPDATE 1
 job_id
--------
     98
(1 row)

これは、高い同時実行性(30)でさまざまなロックモードをすべてテストするシェルスクリプトです。

#!/bin/bash
# RESULTS, feel free to repro yourself
#
# noLock                    FAIL
# accessShare               FAIL
# rowShare                  FAIL
# rowExclusive              FAIL
# shareUpdateExclusive      SUCCESS
# share                     FAIL+DEADLOCKS
# shareRowExclusive         SUCCESS
# exclusive                 SUCCESS
# accessExclusive           SUCCESS, but LOCKS against pg_dump

#config
strategy="exclusive"

db=postgres
dbuser=postgres
queuecount=100
concurrency=30

# code
psql84 -t -U $dbuser $db -c "create table tx_test_queue (job_id serial, status text);"
# empty queue
psql84 -t -U $dbuser $db -c "truncate tx_test_queue;";
echo "Simulating 10 second pg_dump with ACCESS SHARE"
psql84 -t -U $dbuser $db -c "lock table tx_test_queue in ACCESS SHARE mode; select pg_sleep(10); select 'pg_dump finished...'" &

echo "Starting workers..."
# queue $queuecount items
seq $queuecount | xargs -n 1 -P $concurrency -I {} psql84 -q -U $dbuser $db -c "insert into tx_test_queue (status) values ('queued');"
#psql84 -t -U $dbuser $db -c "select * from tx_test_queue order by job_id;"
# process $queuecount w/concurrency of $concurrency
case $strategy in
    "noLock")               strategySql="update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "accessShare")          strategySql="lock table tx_test_queue in ACCESS SHARE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "rowShare")             strategySql="lock table tx_test_queue in ROW SHARE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "rowExclusive")         strategySql="lock table tx_test_queue in ROW EXCLUSIVE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "shareUpdateExclusive") strategySql="lock table tx_test_queue in SHARE UPDATE EXCLUSIVE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "share")                strategySql="lock table tx_test_queue in SHARE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "shareRowExclusive")    strategySql="lock table tx_test_queue in SHARE ROW EXCLUSIVE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "exclusive")            strategySql="lock table tx_test_queue in EXCLUSIVE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    "accessExclusive")      strategySql="lock table tx_test_queue in ACCESS EXCLUSIVE mode; update tx_test_queue set status='running{}' where job_id in (select job_id from tx_test_queue where status='queued' order by job_id asc limit 1);";;
    *) echo "Unknown strategy $strategy";;
esac
echo $strategySql
seq $queuecount | xargs -n 1 -P $concurrency -I {} psql84 -U $dbuser $db -c "$strategySql"
#psql84 -t -U $dbuser $db -c "select * from tx_test_queue order by job_id;"
psql84 -U $dbuser $db -c "select count(distinct(status)) as should_output_100 from tx_test_queue;"
psql84 -t -U $dbuser $db -c "drop table tx_test_queue;";

編集したい場合は、コードもここにあります: https://Gist.github.com/1083936

EXCLUSIVEモードを使用するようにアプリケーションを更新しています。これは、a)が正しく、b)pg_dumpと競合しない最も制限的なモードであるためです。 postgresロックの専門家でなくても、アプリをACCESS EXCLUSIVEから変更するという点ではリスクが最も低いと思われるので、最も制限の厳しいものを選択しました。

私のテストリグと、答えの背後にある一般的なアイデアにかなり満足しています。これを共有することで、この問題を他の人が解決できるようになれば幸いです。

34
apinstein

これのためにテーブル全体をロックする必要はありません:\。

for updateで作成された行ロックは問題なく機能します。

私がapinsteinの回答に加えた変更と、それがまだ機能することを確認した変更については、 https://Gist.github.com/mackross/a49b72ad8d24f7cefc32 を参照してください。

最終的なコードは

update 
    tx_test_queue
set 
    status='running'
where
    job_id in (
        select
            job_id
        from
            tx_test_queue
        where
            status='queued'
        order by 
            job_id asc
        limit 1 for update
    )
returning job_id;
16
mackross

ただ選択するのはどうですか?

SELECT * FROM table WHERE status = 'QUEUED' LIMIT 10 FOR UPDATE SKIP LOCKED;

https://www.postgresql.org/docs/9.5/static/sql-select.html#SQL-FOR-UPDATE-SHARE

Queue_classicがどのように実行するかを確認したい場合があります。 https://github.com/ryandotsmith/queue_classic

コードはかなり短く、理解しやすいです。

2
Joe Van Dyk