Подтвердить что ты не робот

Реализация простой очереди с PHP и MySQL?

У меня есть PHP script, который извлекает строки из базы данных и затем выполняет работу на основе содержимого. Работа может занять много времени (но не обязательно вычислительно дорого), и поэтому мне нужно разрешить параллельное выполнение нескольких сценариев.

Строки в базе данных выглядят примерно так:

+---------------------+---------------+------+-----+---------------------+----------------+
| 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 |                |
+---------------------+---------------+------+-----+---------------------+----------------+

My script в настоящее время выбирает строки с самыми старыми датами в date_last_updated (который обновляется после завершения работы) и не использует date_update_started.

Если бы я должен был запускать несколько экземпляров script параллельно параллельно, они будут выбирать одни и те же строки (по крайней мере, некоторое время), и будет выполняться дублирующаяся работа.

То, что я собираюсь сделать, это использовать транзакцию для выбора строк, обновить столбец date_update_started, а затем добавить условие WHERE в оператор SQL, выбрав строки, чтобы выбирать только строки с date_update_started больше чем какое-либо значение (чтобы еще один script не работал над ним). Например.

$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 недоверчива. Тем не менее, мне нужно найти способ позволить нескольким сценариям работать параллельно, и после исследования, которое я сделал, это то, что я придумал.

Будет ли работать этот тип подхода? Есть ли лучший способ?

4b9b3361

Ответ 1

Я думаю, что ваш подход может работать, если вы также добавляете какой-то идентификатор к выбранным вами строкам, в которые они в настоящее время работают, это может быть как @JuniusRendel, и я даже подумал бы об использовании другого строкового ключа (случайный или идентификатор экземпляра) для случаев, когда script приводил к ошибкам и не выполнялся изящно, так как вам придется очищать эти поля после обновления строк после вашей работы.

Проблема с этим подходом, как я вижу, - это вариант, который будет иметь 2 скрипта, которые работают в одной и той же точке, и будут выбирать те же строки до того, как они будут подписаны как заблокированные. здесь, как я могу это видеть, это действительно зависит от того, какую работу вы выполняете в строках, если конечный результат в этих обоих сценариях будет таким же, я думаю, что единственная проблема, с которой вы столкнулись, - это потраченное время и память сервера (что не маленькие проблемы, но я отложу их на время...). если ваша работа приведет к разным обновлениям обоих сценариев, ваша проблема будет заключаться в том, что вы можете иметь неправильное обновление в конце TB.

@Jean упомянул о втором подходе, который вы можете использовать, используя блокировки MySql. Я не эксперт в этом вопросе, но, похоже, хороший подход и использование выражения Select.... FOR UPDATE 'может дать вам то, что вы ищете как вы могли бы сделать по одному вызову select и update - который будет быстрее, чем два отдельных запроса, и может уменьшить риск для других экземпляров, чтобы выбрать эти строки, поскольку они будут заблокированы.

"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.

Также существует вероятность того, что процесс блокировки и особенно при запуске параллельных скриптов будет тяжелым для вашего процессора и памяти.

вот еще одно чтение по теме: http://www.percona.com/blog/2006/08/06/select-lock-in-share-mode-and-for-update/

Надеюсь, что это поможет, и хотелось бы услышать, как вы прогрессировали.

Ответ 2

У нас есть что-то подобное, реализованное в производстве.

Чтобы избежать дубликатов, мы делаем MySQL UPDATE, как это (я изменил запрос, чтобы он напоминал вашу таблицу):

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) очереди, которая была обновлена ​​(если таковая имеется).

Сразу после этого мы делаем:

SELECT LAST_INSERT_ID();

При использовании без параметра он извлекает ранее сохраненное значение, получая идентификатор элемента очереди, который должен быть выполнен.

Ответ 3

Изменить: Извините, я полностью не понял ваш вопрос.

Вы должны просто поместить столбец "заблокирован" в своей таблице, чтобы значение было верным для записей, с которыми работает ваш script, и когда это сделало его ложным.

В моем случае я поставил 3 других столбца timestamp (integer): 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 для своего выбора

Ответ 4

Каждый раз, когда выполняется script, я бы создал script uniqid.

$sctiptInstance = uniqid();

Я бы добавил столбец экземпляра script, чтобы сохранить это значение как varchar и поместить на него индекс. Когда выполняется script, я бы использовал select для обновления внутри транзакции, чтобы выбирать ваши строки на основе любой логики, исключая строки с экземпляром script, а затем обновлять эти строки экземпляром script. Что-то вроде:

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;

Теперь эти строки будут исключены из других экземпляров script. Работаете ли вы, а затем обновляете строки, чтобы вернуть экземпляр script к нулевому или пустому, а также обновить последний обновленный столбец даты.

Вы также можете использовать экземпляр script для записи в другую таблицу, называемую "текущие экземпляры" или что-то в этом роде, и иметь script проверить эту таблицу, чтобы получить количество запущенных скриптов для управления количеством параллельных скриптов, Я бы добавил PID из script в таблицу. Затем вы можете использовать эту информацию для создания домашнего хозяйства script для периодического запуска из cron, чтобы проверить длительные процессы или изгои и уничтожить их и т.д.

Ответ 5

У меня есть система, работающая точно так же, как в производстве. Мы выполняем script каждую минуту, чтобы выполнить некоторую обработку, и иногда этот запуск может занять более минуты.

У нас есть столбец таблицы для статуса, который равен 0 для NOT RUN YET, 1 для FINISHED и другого значения для этого.

Первое, что делает script, - это обновление таблицы, установка строки или нескольких строк со значением, означающим, что мы работаем над этой линией. Мы используем getmypid() для обновления строк, над которыми мы хотим работать, и которые по-прежнему не обрабатываются.

Когда мы закончим обработку, script обновит строки, имеющие один и тот же идентификатор процесса, помечая их как завершенные (статус 1).

Таким образом мы избегаем каждого сценария, чтобы попытаться обработать строку, которая уже обрабатывается, и она работает как шарм. Это не означает, что нет лучшего способа, но это действительно делает работу.

Ответ 6

Я использовал хранимую процедуру по очень похожим причинам в прошлом. Мы использовали блокировку чтения 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$$

Нет причин, по которым это должно быть сделано в хранимой процедуре, хотя теперь я думаю об этом.