将数据库表用作作业队列(a.k.a批处理队列或消息队列)的最佳方法

时间:2008-11-17 23:00:31

标签: sql database concurrency locking job-queue

我有一个包含约50K行的数据库表,每行代表一个需要完成的工作。我有一个程序从数据库中提取作业,完成工作并将结果放回到数据库中。 (这个系统现在正在运行)

现在我想允许多个处理任务来完成工作,但要确保没有任务完成两次(作为性能问题而不是这会导致其他问题)。因为访问是通过sproce进行的,我当前的目的是用看起来像这样的东西替换所说的sproce

update tbl set owner=connection_id() where avalable and owner is null limit 1;
select stuff from tbl where owner = connection_id();

顺便说一句;工作人员的任务可能会在获得工作和提交结果之间失去联系。此外,我不认为DB甚至会接近瓶颈,除非我把那部分搞得一团糟(每分钟约5个工作)

这有什么问题吗?有更好的方法吗?

注意:"Database as an IPC anti-pattern"在这里只是略微适用,因为1)我没有做IPC(没有生成行的过程,它们现在都已存在)和2)为此描述的主要抱怨反模式是因为进程等待消息导致数据库上出现不必要的负载(在我的情况下,如果没有消息,一切都可以在一切都完成时关闭)

6 个答案:

答案 0 :(得分:13)

以下是我过去成功使用的内容:

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”完成。它可以删除或不删除消息,具体取决于您是否要保留审计跟踪。 State ='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

答案 1 :(得分:1)

正如可能的技术变更一样,您可以考虑使用MSMQ或类似的东西。

您的每个作业/线程都可以查询消息队列以查看是否有新作业可用。因为读取消息的行为将其从堆栈中删除,所以确保只有一个作业/线程可以获得消息。

当然,这是假设您正在使用Microsoft平台。

答案 2 :(得分:1)

在关系数据库系统中实现作业队列的最佳方法是使用SKIP LOCKED

SKIP LOCKED是一种锁获取选项,适用于读/共享(FOR SHARE)或写/独占(FOR UPDATE)锁,如今已得到广泛支持:

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

现在,考虑我们有以下post表用作作业队列:

CREATE TABLE post (
    id int8 NOT NULL,
    body varchar(255),
    status int4,
    title varchar(255),
    PRIMARY KEY (id)
)

status列用作枚举,其值为PENDING(0),APROVED(1)或SPAM(2)。

如果我们有多个并发用户试图审核post记录,我们需要一种方法来协调他们的工作,避免让两个主持人查看同一行post

因此,跳过锁定正是我们需要的。如果有两个并发用户Alice和Bob,则执行以下SELECT查询,这些查询将专门锁定帖子记录,同时还添加了SKIP LOCKED选项:

    [Alice]:
    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

[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个记录上的锁定为止。

有关“跳过锁定”的更多详细信息,请查看this article

答案 3 :(得分:0)

如果不拥有owner = null,则应将其设置为伪无人记录。搜索null不会限制索引,最终可能会进行表扫描。 (这是针对oracle,SQL服务器可能会有所不同)

答案 4 :(得分:0)

有关上下文,请参见弗拉德的回答,我只是在Oracle中添加了等效内容,因为有一些“陷阱”要注意。

The

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.

答案 5 :(得分:-1)

您正在尝试实现de“Database as IPC”反模式。仔细查看为什么要考虑正确地重新设计软件。