Предотвратить параллельное выполнение с помощью блокировки таблицы (MySQL)
У меня есть таблица MySQL под названием cronjobs, в которой хранятся все необходимые cronjob (например, удаление старых писем, обновление возраста профиля и т. Д.). Для каждого cronjob есть определенный блок кода, который выполняется, если cronjob должен быть выполнен (я получил разные интервалы для разных cronjobs).
Для выполнения соответствующих cronjobs я получил PHP-скрипт, который выполняется crontab UNIX каждую минуту (вызывает execute_cronjobs_due.sh, который вызывает "php -f /path/to/file/execute_cronjobs_due.php").
При выполнении execute_cronjobs_due.php все cronjobs отмечаются, что они будут выполнены, так что другой вызов execute_cronjobs_due.php не вызовет параллельное выполнение того же самого cronjob, уже получая выполнение.
Теперь проблема: иногда выполнение занимает более 60 секунд, но программа crontab не вызывает execute_cronjobs_due.sh после этих 60 секунд. На самом деле происходит то, что execute_cronjobs_due.sh вызывается сразу после выполнения выполнения предыдущего crontab. И если выполнение занимает более 120 секунд, следующие два выполнения инициализируются одновременно.
График:
2015-06-15 10:00:00: выполнение execute_cronjobs_due.sh (занимает 140 секунд)
2015-06-15 10:02:20: два одновременных выполнения execute_cronjobs_due.sh
Поскольку он выполняется точно одновременно, нет смысла отмечать cronjob, что они выполняются, так как выборки (которые должны фактически исключать помеченный один раз) выполняются в одно и то же время. Так что обновление происходит сразу после того, как оба уже выбрали нужные cronjobs.
Как я могу решить эту проблему, чтобы не было одновременного выполнения cronjobs? Могу ли я использовать блокировки таблиц MySQL?
Большое спасибо за вашу помощь заранее,
Фредерик
2 ответа
Да, вы можете использовать блокировки таблиц mysql, но это может быть излишним для вашей ситуации. Во всяком случае, чтобы сделать это самым общим способом
- Убедитесь, что вы отключили автокоммит
- LOCK TABLES cronjobs;
- делай свои вещи
- РАЗБЛОКИРОВАТЬ СТОЛЫ
точный синтаксис и подробности читайте в документации, конечно, https://dev.mysql.com/doc/refman/5.0/en/lock-tables.html, я лично никогда не использовал блокировку на уровне таблицы, так что, возможно, есть некоторые зацепки, я не в курсе
Что бы я сделал, если бы вы использовали движок таблиц InnoDB с оптимистической блокировкой:
- начать транзакцию в качестве первого шага в вашем скрипте
- получить идентификатор скрипта или что-то еще, может быть, процесс PID (
getmypid()
) или комбинация хост + пид. Или просто сгенерировать guid, если вы не знаете, что будет идеально - сделать что-то вроде
UPDATE cronjobs SET executed_by = my_id WHERE executed_by is null and /* whatever condition to get jobs to run */
- затем
SELECT * FROM cronjobs where executed_by = my_pid
- делай свои вещи на что-нибудь выше, вернись
UPDATE cronjobs set executed_by = null where executed_by = my_pid
Это должно быть так просто, легче отслеживать, что происходит и масштабироваться в будущем (т.е. у вас может быть несколько экземпляров, работающих параллельно, если они выполняют разные сценарии)
При таком решении второй сценарий не даст сбой (технически), он просто запустит 0 заданий.
Минус в том, что вам придется убирать задания, которые были заявлены, но скрипт не смог пометить их как завершенные, но вам, вероятно, придется делать это в любом случае с текущим решением. Самый простой способ - это добавить столбец отметки времени, который будет отслеживать, когда заявка была запрошена в последний раз, и истечет через 15 минут или час, в зависимости от требований бизнеса (короткий псевдокод: первое обновление выполнит SET executed_by = my_id, started_at = NOW() where executed_by is null or (executed_by is not null and started_at < NOW() - 1 hour)
)
Как я могу решить эту проблему, чтобы не было одновременного выполнения cronjobs?
Есть несколько способов решить эту проблему. Они также могут быть полезны:
Мое предложение состоит в том, чтобы сделать это простым и использовать либо блокировку файла, либо проверку наличия файла.
- file_exist () + класс CronHelper на основе PID
- на основе flock: /questions/7794617/php-predotvraschenie-stolknovenij-v-cron-bezopasnaya-blokirovka-fajlov/7794626#7794626
- когда вы хотите избежать ввода-вывода, сохраните состояние блокировки в memcache
- транзакции с базой данных: см. ниже и ответ @sakfa
- заблокируйте cronjobs в распределенной системе, используя Redis в качестве центрального: https://github.com/kvz/cronlock & http://kvz.io/blog/2012/12/31/lock-your-cronjobs/
Могу ли я использовать блокировки таблиц MySQL?
Да, но это немного излишне.
Вы должны использовать "таблицу обработки cronjob" со столбцом состояния cronjob ("ToDo, Started, Complete" или "Todo, Running, Done") и столбцом PID. Затем вы выбираете вакансии и отмечаете их состояние с помощью транзакций. Это гарантирует, что "Выбор задания из Todo" и "пометка его как запущенного / запущенного" выполняется за один шаг. В конце концов, у вас все еще может быть несколько exec вашего "сценария центральной обработки cronjob", но задания НЕ выбираются несколько раз для обработки.