У меня есть сценарий 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
столбец, а затем добавить WHERE
условие для оператора SQL выбора строк, чтобы выбрать только строки с date_update_started
больше некоторого значения (чтобы другой скрипт не работал над ним). Например.
$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, похоже, ее осуждают. Тем не менее, мне нужно найти способ, позволяющий нескольким сценариям работать параллельно, и после проведенного исследования я пришёл к этому.
Будет ли работать этот тип подхода? Есть ли способ лучше?
Я думаю, что ваш подход может сработать, если вы также добавите какой-то идентификатор к выбранным вами строкам, над которыми они в данный момент работают, это может быть так, как предложил @JuniusRendel, и я бы даже подумал об использовании другого ключа строки (случайного идентификатор экземпляра) для случаев, когда скрипт приводил к ошибкам и не завершался корректно, так как вам придется очищать эти поля после обновления строк после работы.
Проблема с этим подходом, как я вижу, заключается в том, что будет 2 сценария, которые выполняются в одной точке и будут выбирать одинаковые строки до того, как они будут подписаны как заблокированные. здесь, как я вижу, это действительно зависит от того, какую работу вы выполняете со строками, если конечный результат в обоих сценариях будет одинаковым, я думаю, что единственная проблема, которую вы испытываете, это потраченное время и память сервера (что не маленькие проблемы, но я отложу их пока …). если ваша работа приведет к различным обновлениям обоих сценариев, ваша проблема будет заключаться в том, что у вас может быть неправильное обновление в конце TB.
@Jean упомянул второй подход, который вы можете использовать, который включает использование блокировок MySql. я не эксперт в этом вопросе, но мне кажется, что это хороший подход и использованиеВыберите …. ДЛЯ ОБНОВЛЕНИЯ‘заявление может дать вам то, что вы ищете, как вы могли бы сделать на тот же вызов, выберите & обновление — которое будет быстрее, чем 2 отдельных запроса и может снизить риск для других экземпляров выбирать эти строки, поскольку они будут заблокированы.
«ВЫБЕРИТЕ …. ДЛЯ ОБНОВЛЕНИЯ» позволяет вам выполнить оператор выбора и заблокировать эти конкретные строки для их обновления, чтобы ваш оператор мог выглядеть так:
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/
Надеюсь, это поможет, и хотел бы услышать, как вы продвинулись.
У нас что-то подобное реализовано в производстве.
Чтобы избежать дубликатов, мы делаем ОБНОВЛЕНИЕ 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
) очередь, которая была обновлена (если есть).
Сразу после этого мы делаем:
SELECT LAST_INSERT_ID();
При использовании без параметра он извлекает ранее сохраненное значение, получая идентификатор элемента очереди, который должен быть выполнен.
редактировать: Извините, я совершенно неправильно понял ваш вопрос
Вы должны просто поместить «заблокированный» столбец в вашу таблицу, установить значение true для записей, с которыми работает ваш скрипт, а когда это будет сделано, установить значение 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 для вашего выбора
Каждый раз, когда запускается скрипт, я должен был генерировать скрипт uniqid.
$sctiptInstance = uniqid();
Я бы добавил столбец экземпляра скрипта для хранения этого значения как varchar и поместил бы индекс в него. При запуске сценария я использовал бы команду select для обновления внутри транзакции, чтобы выбрать строки на основе любой логики, исключая строки с экземпляром сценария, а затем обновить эти строки с помощью экземпляра сценария. Что-то вроде:
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 для проверки на длительные или мошеннические процессы, их уничтожения и т. Д.
У меня есть система, работающая точно так же, как это в производстве. Мы запускаем скрипт каждую минуту, чтобы выполнить некоторую обработку, и иногда этот запуск может занять больше минуты.
У нас есть столбец таблицы для статуса, который равен 0 для NOT RUN YET, 1 для FINISHED и другого значения для выполнения.
Первое, что делает скрипт, это обновляет таблицу, устанавливая строку или несколько строк со значением, означающим, что мы работаем над этой строкой. Мы используем getmypid () обновить строки, над которыми мы хотим работать, и которые еще не обработаны.
Когда мы заканчиваем обработку, скрипт обновляет строки с одинаковым идентификатором процесса, помечая их как завершенные (статус 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$$
Нет причин делать это в хранимой процедуре, хотя сейчас я думаю об этом.