[英]The best way to use a DB table as a job queue (a.k.a batch queue or message queue)
我有一個數據庫表,里面有大約 50K 行,每一行代表一個需要完成的工作。 我有一個程序可以從數據庫中提取作業,完成這項工作並將結果放回數據庫中。 (這個系統現在正在運行)
現在我想允許多個處理任務執行作業,但要確保沒有任務被執行兩次(作為性能問題,而不是這會導致其他問題)。 因為訪問是通過存儲過程的方式,我目前雖然是用看起來像這樣的東西替換所說的存儲過程
update tbl
set owner = connection_id()
where available and owner is null limit 1;
select stuff
from tbl
where owner = connection_id();
順便提一句; 工作人員的任務可能會降低獲得工作和提交結果之間的聯系。 另外,我不希望 DB 甚至接近瓶頸,除非我把那部分搞砸了(每分鍾約 5 個作業)
這有什么問題嗎? 有一個更好的方法嗎?
注意: “數據庫作為 IPC 反模式”在這里只是稍微合適,因為
在關系數據庫系統中實現作業隊列的最佳方法是使用SKIP LOCKED
。
SKIP LOCKED
是一個鎖獲取選項,適用於讀/共享 ( FOR SHARE
) 或寫/排他 ( FOR UPDATE
) 鎖,現在得到廣泛支持:
現在,考慮我們有以下post
表:
status
列用作Enum
,具有以下值:
PENDING
(0),APPROVED
(1),SPAM
(2)。 如果我們有多個並發用戶試圖審核post
記錄,我們需要一種方法來協調他們的工作,以避免讓兩個審核人審核同一post
行。
所以, SKIP LOCKED
正是我們所需要的。 如果兩個並發用戶 Alice 和 Bob 執行以下 SELECT 查詢,這些查詢以獨占方式鎖定發布記錄,同時還添加了SKIP LOCKED
選項:
[Alice]:
SELECT
p.id AS id1_0_,1
p.body AS body2_0_,
p.status AS status3_0_,
p.title AS title4_0_
FROM
post p
WHERE
p.status = 0
ORDER BY
p.id
LIMIT 2
FOR UPDATE OF p SKIP LOCKED
[Bob]:
SELECT
p.id AS id1_0_,
p.body AS body2_0_,
p.status AS status3_0_,
p.title AS title4_0_
FROM
post p
WHERE
p.status = 0
ORDER BY
p.id
LIMIT 2
FOR UPDATE OF p SKIP LOCKED
我們可以看到 Alice 可以選擇前兩個條目,而 Bob 選擇接下來的 2 條記錄。 如果沒有SKIP LOCKED
,Bob 鎖定獲取請求將阻塞,直到 Alice 釋放前 2 條記錄上的鎖定。
這是我過去成功使用的:
MsgQueue 表模式
MsgId identity -- NOT NULL
MsgTypeCode varchar(20) -- NOT NULL
SourceCode varchar(20) -- process inserting the message -- NULLable
State char(1) -- 'N'ew if queued, 'A'(ctive) if processing, 'C'ompleted, default 'N' -- NOT NULL
CreateTime datetime -- default GETDATE() -- NOT NULL
Msg varchar(255) -- NULLable
您的消息類型是您所期望的 - 符合流程插入和流程讀取之間的契約的消息,使用 XML 或您選擇的其他表示形式(JSON 在某些情況下會很方便,對於實例)。
然后0到n個進程可以插入,0到n個進程可以讀取和處理消息,每個讀取進程通常處理單個消息類型。 可以運行一個進程類型的多個實例以進行負載平衡。
閱讀器在處理消息時拉取一條消息並將狀態更改為“A”活動。 完成后,它會將狀態更改為“C”完成。 它可以根據您是否要保留審計跟蹤來刪除消息。 狀態 = 'N' 的消息按 MsgType/Timestamp 順序拉取,因此在 MsgType + State + CreateTime 上有一個索引。
變化:
狀態為“E”錯誤。
Reader 進程代碼列。
狀態轉換的時間戳。
這提供了一種很好的、可擴展的、可見的、簡單的機制來執行您所描述的許多事情。 如果您對數據庫有基本的了解,那么它就非常萬無一失且可擴展。
來自評論的代碼:
CREATE PROCEDURE GetMessage @MsgType VARCHAR(8) )
AS
DECLARE @MsgId INT
BEGIN TRAN
SELECT TOP 1 @MsgId = MsgId
FROM MsgQueue
WHERE MessageType = @pMessageType AND State = 'N'
ORDER BY CreateTime
IF @MsgId IS NOT NULL
BEGIN
UPDATE MsgQueue
SET State = 'A'
WHERE MsgId = @MsgId
SELECT MsgId, Msg
FROM MsgQueue
WHERE MsgId = @MsgId
END
ELSE
BEGIN
SELECT MsgId = NULL, Msg = NULL
END
COMMIT TRAN
與其在沒有所有者時設置 owner = null,不如將其設置為假的 nobody 記錄。 搜索 null 不會限制索引,您可能會以表掃描結束。 (這個是針對oracle的,SQL server可能不一樣)
正如可能的技術變化一樣,您可以考慮使用 MSMQ 或類似的東西。
您的每個作業/線程都可以查詢消息隊列以查看是否有新作業可用。 因為讀取消息的行為會將其從堆棧中刪除,所以您可以確保只有一個作業/線程會收到該消息。
當然,這是假設您使用的是 Microsoft 平台。
請參閱 Vlad 的上下文答案,我只是在 Oracle 中添加了等效項,因為有一些“陷阱”需要注意。
這
SELECT * FROM t order by x limit 2 FOR UPDATE OF t SKIP LOCKED
不會以您可能期望的方式直接轉換為 Oracle。 如果我們查看一些翻譯選項,我們可能會嘗試以下任何一種:
SQL> create table t as
2 select rownum x
3 from dual
4 connect by level <= 100;
Table created.
SQL> declare
2 rc sys_refcursor;
3 begin
4 open rc for select * from t order by x for update skip locked fetch first 2 rows only;
5 end;
6 /
open rc for select * from t order by x for update skip locked fetch first 2 rows only;
*
ERROR at line 4:
ORA-06550: line 4, column 65:
PL/SQL: ORA-00933: SQL command not properly ended
ORA-06550: line 4, column 15:
PL/SQL: SQL Statement ignored
SQL> declare
2 rc sys_refcursor;
3 begin
4 open rc for select * from t order by x fetch first 2 rows only for update skip locked ;
5 end;
6 /
declare
*
ERROR at line 1:
ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc.
ORA-06512: at line 4
或者嘗試回退到 ROWNUM 選項
SQL> declare
2 rc sys_refcursor;
3 begin
4 open rc for select * from ( select * from t order by x ) where rownum <= 10 for update skip locked;
5 end;
6 /
declare
*
ERROR at line 1:
ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc.
ORA-06512: at line 4
你不會得到任何快樂。 因此,您需要自己控制“n”行的獲取。 因此,您可以編寫如下代碼:
SQL> declare
2 rc sys_refcursor;
3 res1 sys.odcinumberlist := sys.odcinumberlist();
4 begin
5 open rc for select * from t order by x for update skip locked;
6 fetch rc bulk collect into res1 limit 10;
7 end;
8 /
PL/SQL procedure successfully completed.
您正在嘗試實施“數據庫作為 IPC”反模式。 查看它以了解為什么您應該考慮正確重新設計您的軟件。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.