[英]SQL, Postgresql: optimal way to query multiple cells from one table
[英]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>
現在,兩個消費者A , B想要處理它們。 它們同時開始運行。 一個應該保留 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
您可能想看看 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.