Как предотвратить множественные экземпляры скрипта?

У меня есть сценарии php, которые я должен запускать как на Linux, так и на серверах Windows. Я хочу использовать те же сценарии без каких-либо изменений для этих двух сред.

Тезисы будут запланированы с помощью cron (on linux) и с планировщиком окон (или другим, на мой взгляд, сейчас нет) для моей среды Windows.

Однако некоторые из этих сценариев могут занять несколько минут. Я просто хочу, чтобы один и тот же сценарий не запускался планировщиком (cron или windows) до того, как он закончил последний раз, когда он был запущен.

Я не уверен, как это сделать. Я хочу быть уверенным, что «блокировка» будет выпущена, если что-то пойдет не так во время выполнения, поэтому он будет запущен снова в следующий раз без вмешательства человека.

Может быть, с пастбищем в фиктивный файл будет трюк, но я не уверен, как это сделать.

У меня также есть база данных MySQL на этих серверах. Я думал, возможно, используя блокировку на стороне базы данных.

1- Start a transaction 2- Insert script name in a table. 3- execution of the script. 4- If successful then delete the row and commit the transaction or simply rollback; 

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

Но, в транзакции, есть ли способ для других подключений, чтобы увидеть необработанные данные? если да, то как?

Я также думал, используя блокировку строки, если невозможно использовать откат.

 1- Insert script name in a table if it doesn't already exists. 2- Start a transaction. 2- Select * from Table where script_name FOR UPDATE. 3- execution of the script. 4- If successful then release the lock (rollback or commit). 

Но моя главная проблема здесь в Mysql. Выберите FOR UPDATE, пока не будет отпущена предыдущая блокировка или если истечет 50-секундный тайм-аут (innodb_lock_wait_timeout variable). Я бы хотел, чтобы Mysql сразу сказал мне, что моя строка заблокирована, не затрагивая всю базу данных. Это связано с тем, что переменная innodb_lock_wait_timeout является глобальной (а не сеансовой). Есть ли другая переменная, которая имитирует предложение NO_WAIT, доступное в Oracle?

Или я должен позволить сценарию повесить 50 секунд без каких-либо проблем?

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

Может быть, у меня есть другой вариант, который я не видел.

Related of "Как предотвратить множественные экземпляры скрипта?"

Я решил эту проблему, используя … сокеты. Это вы можете включить расширение php_sockets затем попробовать. Вот пример кода:

 $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (false === $socket) { throw new Exception("can't create socket: ".socket_last_error($socket)); } ## set $port to something like 10000 ## hide warning, because error will be checked manually if (false === @socket_bind($socket, '127.0.0.1', $port)) { ## some instanse of the script is running return false; } else { ## let's do your job return $socket; } 

Связывание сокета на определенном $port -порте – безопасная операция для выполнения сочетанного выполнения. Операционная система будет следить за тем, чтобы не было другого процесса, связанного сокетам с одним и тем же портом. Вам просто нужно проверить возвращаемое значение.

Если скрипт выйдет из строя, операционная система автоматически отвяжет порт.

Это также можно использовать на любом языке. Я тестировал его на проектах на основе perl и php. Он остановил параллельное выполнение, даже если мы дважды добавили скрипт в crontab по ошибке.

Почему бы не использовать старомодный семафор, это просто сделано именно для этого. Я уверен, что существуют также версии для Windows, или PHP просто совместим:

 if ($theSemaphore = sem_get("123456",1)) { // this "1" ensures that there is nothing parallel if (sem_acquire($theSemaphore)) { // this blocks the execution until other processes or threads are finished <put your code to serialize here> sem_release($theSemaphore); // This should be called only if sem_acquire() succeeds } } 

В среде потоковой передачи Apache это работает отлично, также в PHP-CLI и смешанном. В случае, если процесс неожиданно завершится, семафор недействителен, и фургон снова будет приобретен. Семафоры реализованы «атомарными», поэтому предотвращают условия гонки во время блокировки.

Хорошее описание, основанное на туалетах, здесь

Проверьте файл блокировки (например, «script_running.lock») псевдокод:

 if file exists exit else create the file run the rest of the script unlink the file when script is done 

Современный год 2017 Ответ:

Существует много способов реализации блокировок в PHP.

  • Проверка сумасшедшего файла, так называемая «первая попытка блокировки новорожденного»: просто создайте файл и удалите его, когда закончите. Все остальные экземпляры проверяют, существует ли он. Это страдает от огромного риска того, что файл не будет удален, когда вы закончите (например, если власть потеряна или сценарий принудительно убит), а это означает, что все будущие сценарии запускаются с ошибкой. Он также страдает от нескольких одновременно запущенных экземпляров, которые видят, что файл отсутствует, и все пытаются создать его исключительно. Это ужасно .
  • Написание идентификатора процесса в файл: небольшое улучшение описанного выше метода. Но все же чрезвычайно хаки и страдает от условий гонки. Вы должны записать текущий идентификатор процесса PHP в текстовый файл, а затем все остальные экземпляры читают содержимое этого файла и проверяют, существует ли еще этот идентификатор процесса и является процессом PHP, и если это так, мы рассматриваем процесс «заблокирован», , Он восприимчив к условиям гонки, если два сценария начинаются очень близко друг к другу, и оба читают одно и то же содержимое текстового файла, и оба считают, что предыдущий идентификатор процесса PHP больше не работает, и оба считают, что у них есть исключительная блокировка. Этот метод следует избегать любой ценой. Это едва приемлемо даже для базовых сценариев оболочки Bash (где не существует другого метода), но PHP имеет гораздо более сложные методы блокировки.
  • Сокеты: привязка к локальному порту. Если порт используется, считайте его «заблокированным». Плюсы: файл блокировки не требуется. Минусы: порт может использоваться самой системой или конфигурация системы не может позволить процессам выполнять привязку портов, а также намного медленнее блокировки файла (поскольку он вызывает всю систему сокетов ОС и создает сокет ).
  • Семафоры: очень быстрая и надежная, но Unix-only (Posix).
  • Эксклюзивные блокировки файлов: это кросс-платформенный (Unix, Linux, Mac, Windows), супер надежный и быстрый. Это то, что я реализовал чисто и качественно ниже.

locker.inc.php:

 <?php class Locker { private $_filename; private $_fh = NULL; public function __construct( string $filename ) { $this->_filename = $filename; } public function __destruct() { $this->unlock(); } /** * Attempt to acquire an exclusive lock. Always check the return value! * @param bool $block If TRUE, we'll wait for existing lock release. * @return bool TRUE if we've acquired the lock, otherwise FALSE. */ public function lock( bool $block = TRUE ) { // Create the lockfile if it doesn't exist. if( ! is_file( $this->_filename ) ) { $created = @touch( $this->_filename ); if( ! $created ) { return FALSE; // no file } } // Open a file handle if we don't have one. if( $this->_fh === NULL ) { $fh = @fopen( $this->_filename, 'r' ); if( $fh !== FALSE ) { $this->_fh = $fh; } else { return FALSE; // no handle } } // Try to acquire the lock (blocking or non-blocking). $lockOpts = ( $block ? LOCK_EX : ( LOCK_EX | LOCK_NB ) ); return flock( $this->_fh, $lockOpts ); // lock } /** * Release the lock. Also happens automatically when the Locker * object is destroyed, such as when the script ends. Also note * that all locks are released if the PHP process is force-killed. * NOTE: We DON'T delete the lockfile afterwards, to prevent * a race condition by guaranteeing that all PHP instances lock * on the exact same filesystem inode. */ public function unlock() { if( $this->_fh !== NULL ) { flock( $this->_fh, LOCK_UN ); // unlock fclose( $this->_fh ); $this->_fh = NULL; } } } 

testlock.php:

 <?php require_once( 'locker.inc.php' ); $locker = new Locker( 'test.lock' ); echo time() . ": acquiring lock...\n"; $is_locked = $locker->lock( TRUE ); // TRUE = blocking if( $is_locked ) { // ALWAYS check this return value echo time() . ": we have a lock...\n"; sleep(10); // hold the lock for 10 seconds // manually unlock again, but we don't have // to do this since it also happens when // the $locker object is destroyed (ie // when the script ends). $locker->unlock(); } else { echo time() . ": failed to get lock...\n"; } 

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

Таким образом, выбор за вами:

  • TRUE: Подождите, пока блокировка не будет доступна. Отлично, если вы хотите, чтобы все задания выполнялись, но все они должны все ждать, пока их очередь будет работать исключительно.
  • FALSE: Не ждите, если блокировка недоступна. Полезно, если вы хотите прервать другие сценарии, если его экземпляр уже запущен.

В качестве альтернативы вы можете использовать LOCK-файлы. Идея проста: если выполняется скрипт S, он сначала проверяет наличие определенного (уникального) файла, например S.lock :

  • Если файл существует, S завершается.

  • В противном случае это создаст его. Если S выйдет, файл будет удален.