我有一个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中不受欢迎。同样,我需要找到一种允许多个脚本并行运行的方法,经过研究我已经完成了这个就是我想出来的。
这种方法会起作用吗?还有更好的方法吗?
答案 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$$
现在我没有理由在存储过程中完成它。