在mysql中实现消息队列表的最佳方法是什么

时间:2009-01-08 03:06:10

标签: mysql job-queue

这可能是我第十次实施这样的事情,而且我从未对我提出的解决方案百分百满意。

使用mysql表而不是“正确”的消息传递系统的原因很有吸引力主要是因为大多数应用程序已经将一些关系数据库用于其他东西(对于我一直在做的大多数事情,它往往是mysql),而很少有应用程序使用消息传递系统。此外 - 关系数据库具有非常强大的ACID属性,而消息传递系统通常没有。

第一个想法是使用:

create table jobs(
  id auto_increment not null primary key,
  message text not null,
  process_id varbinary(255) null default null,
  key jobs_key(process_id) 
);

然后排队看起来像这样:

insert into jobs(message) values('blah blah');

出列看起来像这样:

begin;
select * from jobs where process_id is null order by id asc limit 1;
update jobs set process_id = ? where id = ?; -- whatever i just got
commit;
-- return (id, message) to application, cleanup after done

表和入队看起来不错,但出队有点困扰我。回滚的可能性有多大?还是被封锁了?我应该用什么键来制作O(1)-ish?

或者有什么更好的解决方案,我正在做什么?

8 个答案:

答案 0 :(得分:25)

你的出队可能更简洁。您可以在没有显式事务的情况下在一个原子语句中执行此操作,而不是依赖事务回滚:

UPDATE jobs SET process_id = ? WHERE process_id IS NULL ORDER BY ID ASC LIMIT 1;

然后你可以用(括号[]表示可选,取决于你的具体情况)拉出作业:

SELECT * FROM jobs WHERE process_id = ? [ORDER BY ID LIMIT 1];

答案 1 :(得分:7)

我已经构建了一些消息排队系统,我不确定你指的是什么类型的消息,但是在出列的情况下(是一个单词?)我做了同样的事情你做完了。您的方法看起来简单,干净,坚固。并不是说我的工作是最好的,但事实证明它对许多网站的大型监控非常有效。 (错误记录,大规模电子邮件营销活动,社交网络通知)

我的投票:不用担心!

答案 2 :(得分:6)

Brian Aker刚才谈到了queue engine。还讨论了SELECT table FROM DELETE语法。

如果您不担心吞吐量,可以始终使用SELECT GET_LOCK()作为互斥锁。例如:

SELECT GET_LOCK('READQUEUE');
SELECT * FROM jobs;
DELETE FROM JOBS WHERE ID = ?;
SELECT RELEASE_LOCK('READQUEUE');

如果你想变得非常花哨,请将其包装在存储过程中。

答案 3 :(得分:1)

我建议使用Quartz.NET

它提供SQL Server,Oracle,MySql,SQLite和Firebird。

答案 4 :(得分:1)

This thread的设计信息应该是可映射的。

引用:

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

MsgQueue表架构

MsgId身份 - NOT NULL
MsgTypeCode varchar(20) - NOT NULL
SourceCode varchar(20) - 插入消息的进程 - NULLable
State char(1) - 'N'ew如果排队,'A'(ctive)如果处理,'C'ompleted,默认'N' - NOT NULL
CreateTime datetime - 默认GETDATE() - NOT NULL
Msg varchar(255) - NULLable

您的消息类型是您所期望的 - 符合插入过程和过程(读取)之间的合同的消息,使用XML或您的其他表示形式构建(JSON在某些方面会很方便例如,案例)。

然后可以插入0到n进程,0到n进程可以读取和处理消息。每个读取进程通常处理单个消息类型。可以运行多个进程类型实例以进行负载平衡。

读取器会拉出一条消息,并在其工作时将状态更改为“A”。完成后,它将状态更改为“C”完成。它可以删除或不删除消息,具体取决于您是否要保留审计跟踪。 State ='N'的消息是以MsgType / Timestamp顺序提取的,因此在MsgType + State + CreateTime上有一个索引。

变体:
陈述“E”恐怖。
Reader进程代码列。
状态转换的时间戳。

这提供了一个很好的,可扩展的,可见的,简单的机制,用于执行您正在描述的许多事情。如果您对数据库有基本的了解,那么它非常简单且可扩展。由于原子状态转换事务,锁定回滚等问题从未出现过问题。

答案 5 :(得分:1)

这是我使用的解决方案,没有当前线程的process_id,或者锁定表。

2.2.2 :037 > award = Award.new({name: "Test award", student_id: 1})
 => #<Award id: nil, name: "Test award", year: nil, student_id: 1, created_at: nil, updated_at: nil>
2.2.2 :038 > award.valid?
  Student Load (0.1ms)  SELECT  "students".* FROM "students" WHERE "students"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
 => false
2.2.2 :039 > award.year = 1979
 => 1979
2.2.2 :040 > award.valid?
 => false
2.2.2 :041 > award.year = 1980
 => 1980
2.2.2 :042 > award.valid?
 => true

将结果输入$ row数组,然后执行:

SELECT * from jobs ORDER BY ID ASC LIMIT 0,1;

然后获取受影响的行(mysql_affected_rows)。如果存在受影响的行,请在$ row数组中处理该作业。如果有0个受影响的行,则表示某个其他进程已在处理所选作业。重复上述步骤,直到没有行。

我已经使用具有100k行的“作业”表对其进行了测试,并产生了20个并行进程来执行上述操作。没有竞争条件发生。您可以修改上述查询以使用处理标志更新行,并在实际处理后删除该行:

DELETE from jobs WHERE ID=$row['ID'];

毋庸置疑,您应该使用正确的消息队列(ActiveMQ,RabbitMQ等)进行此类工作。我们不得不求助于这个解决方案,因为我们的主机在更新软件时经常会破坏一些东西,所以越少越好。

答案 6 :(得分:0)

您可以有一个中间表来维护队列的偏移量。

create table scan(
  scan_id int primary key,
  offset_id int
);

您可能还会进行多次扫描,因此每次扫描一次偏移。在扫描开始时初始化offset_id = 0。

begin;
select * from jobs where order by id where id > (select offset_id from scan where scan_id = 0)  asc limit 1;
update scan set offset_id = ? where scan_id = ?; -- whatever i just got
commit;

您要做的只是保持最后的偏移量。这还将节省大量空间(每条记录的process_id)。希望这听起来合乎逻辑。

答案 7 :(得分:0)

在MySQL 8中,您可以使用新的NOWAIT和SKIP LOCK关键字来避免复杂的特殊锁定机制:

START TRANSACTION;
SELECT id, message FROM jobs
 WHERE process_id IS NULL
 ORDER BY id ASC LIMIT 1
 FOR UPDATE SKIP LOCKED;
UPDATE jobs
 SET process_id = ?
 WHERE id = ?;
COMMIT;

传统上,如果没有黑客和异常的特殊表或列,不可靠的解决方案或失去并发性,这是很难实现的。

“跳过锁定”可能会导致大量用户的性能问题。

但是,这仍然无法处理在事务回滚时自动将作业标记为完成。为此,您可能需要保存点。但是,这可能无法解决所有情况。您真的想设置一个操作以在事务失败时执行,但作为事务的一部分!

将来可能会出现更多功能来帮助优化情况,例如更新也可以返回匹配的行。在更改日志中随时了解新功能非常重要。