簡體   English   中英

作業隊列為 SQL 表,具有多個消費者 (PostgreSQL)

[英]Job queue as SQL table with multiple consumers (PostgreSQL)

我有一個典型的生產者消費者問題:

多個生產者應用程序將作業請求寫入 PostgreSQL 數據庫上的作業表。

作業請求有一個 state 字段,該字段在創建時包含 QUEUED。

當生產者插入新記錄時,規則會通知多個消費者應用程序:

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

他們將嘗試通過將其 state 設置為 RESERVED 來保留新記錄。 當然,只有一個消費者應該成功。 所有其他消費者不應該能夠保留相同的記錄。 他們應該使用 state=QUEUED 保留其他記錄。

示例:一些生產者在表jobrecord中添加了以下記錄:

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

現在,兩個消費者AB想要處理它們。 它們同時開始運行。 一個應該保留 id 1,另一個應該保留 id 2,然后第一個完成的人應該保留 id 3,依此類推..

在純粹的多線程世界中,我會使用互斥鎖來控制對作業隊列的訪問,但消費者是可能在不同機器上運行的不同進程。 它們只訪問同一個數據庫,因此所有同步都必須通過數據庫進行。

I read a lot of documentation about concurrent access and locking in PostgreSQL, eg http://www.postgresql.org/docs/9.0/interactive/explicit-locking.html Select unlocked row in Postgresql PostgreSQL and locking

從這些主題中,我了解到,以下 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 語句,讓多個消費者不會保留同一個記錄?

我也將 postgres 用於 FIFO 隊列。 我最初使用 ACCESS EXCLUSIVE,它在高並發下產生正確的結果,但不幸的是與 pg_dump 互斥,它在執行期間獲取了一個 ACCESS SHARE 鎖。 這會導致我的 next() function 鎖定很長時間(pg_dump 的持續時間)。 這是不可接受的,因為我們是一家 24x7 的商店,客戶不喜歡半夜排隊等候的時間。

我認為必須有一個限制較少的鎖,它仍然是並發安全的,並且在 pg_dump 運行時不會鎖定。 我的搜索使我找到了這個 SO 帖子。

然后我做了一些研究。

對於 FIFO 隊列 NEXT() function 來說,以下模式就足夠了,它將把作業的狀態從排隊更新為運行,沒有任何並發失敗,也不會阻塞 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)

這是一個 shell 腳本,它在高並發 (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 更改為風險最小的。

我對我的測試台和答案背后的一般想法感到很自在。 我希望分享這個有助於為其他人解決這個問題。

無需為此執行整個表鎖定:\。

使用for update創建的行鎖可以正常工作。

請參閱https://gist.github.com/mackross/a49b72ad8d24f7cefc32了解我對 apinstein 的答案所做的更改並驗證它仍然有效。

最終代碼是

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;

select 怎么樣?

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

在這里閱讀我的帖子:

https://stackoverflow.com/a/6500830/32688

如果您使用事務和 LOCK TABLE,您將沒有問題。

您可能想看看 queue_classic 是如何做到的。 https://github.com/ryandotsmith/queue_classic

該代碼非常簡短且易於理解。

好的,這是對我有用的解決方案,基於 jordani 的鏈接。 由於我的一些問題與 Qt-SQL 的工作方式有關,因此我包含了 Qt 代碼:

QSqlDatabase db = GetDatabase();
db.transaction();
QSqlQuery lockQuery(db);
bool lockResult = lockQuery.exec("LOCK TABLE serverjobrecord IN ACCESS EXCLUSIVE MODE; ");
QSqlQuery query(db);
query.prepare(    
"UPDATE jobrecord "
"  SET \"owner\"= :owner, state = :reserved "
"  WHERE id = ( "
"    SELECT id from jobrecord WHERE state = :queued ORDER BY id LIMIT 1 "
"  ) RETURNING id;"
);
query.bindValue(":owner", pid);
query.bindValue(":reserved", JobRESERVED);
query.bindValue(":queued", JobQUEUED); 
bool result = query.exec();

為了檢查,如果多個消費者處理同一個作業,我添加了一個規則和一個日志表:

CREATE TABLE serverjobrecord_log
(
  serverjobrecord_id integer,
  oldowner text,
  newowner text
) WITH ( OIDS=FALSE );


CREATE OR REPLACE RULE ownerrule AS ON UPDATE TO jobrecord
WHERE old.owner IS NOT NULL AND new.state = 1 
DO INSERT INTO jobrecord_log     (id, oldowner, newowner) 
    VALUES (new.id, old.owner, new.owner);

沒有LOCK TABLE serverjobrecord IN ACCESS EXCLUSIVE MODE; 語句,日志表偶爾會填充條目,如果一個消費者覆蓋了另一個消費者的值,但是使用 LOCK 語句,日志表仍然是空的:-)

查看PgQ而不是重新發明輪子。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM