簡體   English   中英

將數據庫表用作作業隊列(又名批處理隊列或消息隊列)的最佳方法

[英]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 反模式”在這里只是稍微合適,因為

  1. 我不是在做 IPC(沒有生成行的過程,它們現在都已經存在)和
  2. 針對該反模式描述的主要抱怨是,當進程等待消息時,它會導致數據庫上不必要的負載(在我的情況下,如果沒有消息,一切都可以在一切完成后關閉)

在關系數據庫系統中實現作業隊列的最佳方法是使用SKIP LOCKED

SKIP LOCKED是一個鎖獲取選項,適用於讀/共享 ( FOR SHARE ) 或寫/排他 ( FOR UPDATE ) 鎖,現在得到廣泛支持:

  • Oracle 10g 及更高版本
  • PostgreSQL 9.5 及更高版本
  • SQL Server 2005 及更高版本
  • MySQL 8.0 及更高版本

現在,考慮我們有以下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.

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