用PHP和MySQL实现一个简单的队列?

时间:2014-12-13 13:13:40

标签: php mysql sql pdo queue

我有一个PHP脚本,它从数据库中检索行,然后根据内容执行工作。这项工作可能很耗时(但不一定计算成本很高),因此我需要允许多个脚本并行运行。

数据库中的行看起来像这样:

+---------------------+---------------+------+-----+---------------------+----------------+
| Field               | Type          | Null | Key | Default             | Extra          |
+---------------------+---------------+------+-----+---------------------+----------------+
| id                  | bigint(11)    | NO   | PRI | NULL                | auto_increment |
.....
| date_update_started | datetime      | NO   |     | 0000-00-00 00:00:00 |                |
| date_last_updated   | datetime      | NO   |     | 0000-00-00 00:00:00 |                |
+---------------------+---------------+------+-----+---------------------+----------------+

我的脚本目前选择date_last_updated中具有最早日期的行(在工作完成后更新)并且不使用date_update_started

如果我现在要并行运行多个脚本实例,他们会选择相同的行(至少在某些时候),并且会完成重复的工作。

我正在考虑的是使用事务来选择行,更新date_update_started列,然后向SQL语句添加WHERE条件,选择行以仅选择行date_update_started大于某个值(以确保其他脚本无法正常工作)。 E.g。

$sth = $dbh->prepare('
    START TRANSACTION;
    SELECT * FROM table WHERE date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000;
    UPDATE table DAY SET date_update_started = UTC_TIMESTAMP() WHERE id IN (SELECT id FROM table WHERE date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000;);
    COMMIT;
');
$sth->execute(); // in real code some values will be bound
$rows = $sth->fetchAll(PDO::FETCH_ASSOC);

从我读过的内容来看,这本质上是一个队列实现,似乎在MySQL中不受欢迎。同样,我需要找到一种允许多个脚本并行运行的方法,经过研究我已经完成了这个就是我想出来的。

这种方法会起作用吗?还有更好的方法吗?

6 个答案:

答案 0 :(得分:6)

我认为您的方法可以正常工作,只要您还为您选择的当前处理的行添加某种标识符,它可能是@JuniusRendel建议的,我甚至会想到使用另一个字符串键(随机或实例ID)用于脚本导致错误并且没有正常完成的情况,因为在工作之后更新行后,您将不得不清理这些字段。

这种方法存在问题,因为我认为可以选择在同一点运行2个脚本,并在签名为锁定之前选择相同的行。在这里,我可以看到它,它真的取决于你在行上做了什么样的工作,如果这两个脚本的最终结果是相同的,我认为你唯一的问题是浪费时间和服务器内存(这这不是小问题,但我暂时把它们放在一边......)。如果你的工作会在两个脚本上产生不同的更新,你的问题就是你可能会在TB结尾处进行错误的更新。

@Jean已经提到了你可以采用的第二种方法,涉及使用MySql锁。我不是这个主题的专家,但它似乎是一个很好的方法,使用' Select .... FOR UPDATE'声明可以给你你正在寻找的东西,因为你可以在同一个电话上选择&更新 - 这将比2个单独的查询更快,并且可以降低其他实例选择这些行的风险,因为它们将被锁定。

' SELECT .... FOR UPDATE' 允许您运行select语句并锁定这些特定行以进行更新,因此您的语句可能如下所示:

START TRANSACTION;
   SELECT * FROM tb where field='value' LIMIT 1000 FOR UPDATE;
   UPDATE tb SET lock_field='1' WHERE field='value' LIMIT 1000;
COMMIT;

锁是强大的,但要小心它不会影响你在不同部分的应用程序。检查当前为更新锁定的所选行是否在应用程序的其他位置(可能是最终用户)请求,以及在这种情况下会发生什么。

此外,表必须是InnoDB,并且建议您检查where子句的字段具有Mysql索引,如果不是,您可能会锁定整个表或遇到' Gap Lock&# 39;

还有可能锁定过程,特别是在运行并行脚本时,CPU和CPU的运行量很大。记忆。

这是关于这个主题的另一个读物:http://www.percona.com/blog/2006/08/06/select-lock-in-share-mode-and-for-update/

希望这会有所帮助,并希望了解你的进步情况。

答案 1 :(得分:4)

我们在生产中实现了类似的东西。

为了避免重复,我们像这样进行MySQL更新(我修改了查询以使其类似于您的表):

UPDATE queue SET id = LAST_INSERT_ID(id), date_update_started = ... 
WHERE date_update_started IS NULL AND ...
LIMIT 1;

我们在单个事务中执行此更新,并且我们利用LAST_INSERT_ID函数。当像这样使用时,使用参数,它在事务会话中写入参数,在这种情况下,它是已更新的单个(LIMIT 1)队列的ID(如果有的话) )。

在那之后,我们做:

SELECT LAST_INSERT_ID();

当不带参数使用时,它会检索先前存储的值,获取必须执行的队列项ID。

答案 2 :(得分:1)

修改:抱歉,我完全误解了您的问题

您应该在表上放置一个“锁定”列,将值放在您的脚本正在使用的条目上,并在完成时将其置为false。

在我的情况下,我已经放了3个其他时间戳(整数)列:target_ts,start_ts,done_ts。 你

UPDATE table SET locked = TRUE WHERE target_ts<=UNIX_TIMESTAMP() AND ISNULL(done_ts) AND ISNULL(start_ts);

然后

SELECT * FROM table WHERE target_ts<=UNIX_TIMESTAMP() AND ISNULL(start_ts) AND locked=TRUE;

执行您的工作并逐个更新每个条目(以避免数据不一致)将done_ts属性设置为当前时间戳(您现在也可以解锁它们)。您可以将target_ts更新为您希望的下一个更新,或者您可以忽略此列,只需使用done_ts进行​​选择

答案 3 :(得分:1)

每次脚本运行时,我都会让脚本生成uniqid。

$sctiptInstance = uniqid();

我会添加一个脚本实例列来将此值保存为varchar并在其上放置索引。当脚本运行时,我会在事务内部使用select for update来根据任何逻辑选择行,不包括带有脚本实例的行,然后使用脚本实例更新这些行。类似的东西:

START TRANSACTION;
SELECT * FROM table WHERE script_instance = '' AND date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000 FOR UPDATE;
UPDATE table SET date_update_started = UTC_TIMESTAMP(), script_instance = '{$scriptInstance}' WHERE script_instance = '' AND date_update_started > 1 DAY ORDER BY date_last_updated LIMIT 1000;
COMMIT;

现在,这些行将从脚本的其他实例中排除。您是否工作,然后更新行以将脚本实例设置回null或空白,并更新您的日期上次更新列。

您还可以使用脚本实例写入另一个名为“当前实例”的表或类似的表,并让脚本检查该表以获取运行脚本的计数以控制并发脚本的数量。我也会将脚本的PID添加到表中。然后,您可以使用该信息创建一个内务处理脚本,以便定期从cron运行以检查长时间运行或恶意进程并终止它们等。

答案 4 :(得分:1)

我有一个系统在生产中完全像这样工作。我们每分钟运行一个脚本来进行一些处理,有时候运行可能需要一分多钟。

我们有一个状态表列,对于NOT RUN YET为0,对于FINISHED为1,其他值为正在进行中。

脚本所做的第一件事就是更新表,设置一行或多行,其中的值意味着我们正在处理该行。我们使用getmypid()来更新我们想要处理的行,并且这些行仍然是未处理的。

当我们完成处理时,脚本会更新具有相同进程ID的行,并将它们标记为已完成(状态1)。

这样我们就可以避免每个脚本尝试处理已经处理的行,它就像魅力一样。这并不意味着没有更好的方法,但这确实可以完成工作。

答案 5 :(得分:1)

我过去曾以非常类似的原因使用过存储过程。我们使用FOR UPDATE读锁来锁定表,同时更新选定的标志以从任何将来的选择中删除该条目。它看起来像这样:

CREATE PROCEDURE `select_and_lock`()
 BEGIN
  START TRANSACTION;
  SELECT your_fields FROM a_table WHERE some_stuff=something 
   AND selected = 0 FOR UPDATE;
  UPDATE a_table SET selected = 1;
  COMMIT;
 END$$

现在我没有理由在存储过程中完成它。