Как предотвратить множественные экземпляры скрипта? - PullRequest
2 голосов
/ 07 декабря 2009

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

Эти сценарии будут запланированы с помощью cron (в linux) и планировщиком windows (или другим, мне сейчас наплевать) для среды 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) Я хотел бы, чтобы Mysql сразу сказал мне, что моя строка заблокирована, не затрагивая всю базу данных. Это связано с тем, что переменная innodb_lock_wait_timeout является глобальной (а не сеансовой). Существует ли другая переменная, имитирующая предложение NO_WAIT, которая доступна в Oracle?

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

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

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

Ответы [ 5 ]

10 голосов
/ 07 декабря 2009

Я решил эту проблему, используя ... сокеты. Если вы можете включить 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 по ошибке.

2 голосов
/ 11 января 2012

Почему бы не использовать старомодный семафор, он просто создан именно для этого. Я уверен, что есть также реализации для 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 и смешанном режиме. Если процесс неожиданно умирает, семафор недействителен и его можно получить снова. Семафоры реализованы «атомарно», поэтому предотвращают гонку во время блокировки.

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

1 голос
/ 29 ноября 2016

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

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

  • Проверка существования файла без слов, он же "первая попытка блокировки ребенка" новичком: просто создайте файл, а затем удалите его, когда закончите. Все остальные экземпляры проверяют, существует ли он. Это страдает от огромного риска того, что файл не будет удален, когда вы закончите (например, если питание потеряно или скрипт принудительно убит), что означает, что все будущие сценарии запускаются неудачно. Он также страдает от нескольких одновременно запущенных экземпляров, которые видят, что файл отсутствует, и все пытаются создать его исключительно. Это ужасно .
  • Запись идентификатора процесса в файл: незначительное улучшение вышеуказанного метода. Но все же чрезвычайно хакер и страдает от условий гонки. Вы бы записали идентификатор процесса текущего экземпляра PHP в текстовый файл, а затем все остальные экземпляры прочитали бы содержимое этого файла и проверили, существует ли этот идентификатор процесса еще и является ли он процессом PHP, и если это так, мы считаем процесс заблокированным. , Он подвержен условиям гонки, если два сценария начинаются очень близко друг к другу и оба читают одно и то же содержимое текстового файла и оба считают, что предыдущий идентификатор процесса PHP больше не работает и оба считают, что у них эксклюзивный замок. Этот метод следует избегать при всех затратах. Это едва приемлемо даже для базовых сценариев оболочки Bash (где другой метод недоступен), но PHP имеет гораздо более продвинутых доступных методов блокировки.
  • Сокеты: привязка к локальному порту. Если порт используется, считайте его «заблокированным». Плюсы: файл блокировки не требуется. Минусы: порт может использоваться самой системой, или конфигурация системы может не позволять процессам выполнять привязку порта, и это также намного медленнее, чем блокировка файла (так как он вызывает всю систему сокетов ОС и создает сокет ).
  • Семафоры: очень быстрые и надежные, но Только для Unix (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 (i.e.
    // when the script ends).
    $locker->unlock();
} else {
    echo time() . ": failed to get lock...\n";
}

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

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

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

Проверить наличие файла блокировки (т. Е. "Script_running.lock") псевдокод:

if file exists exit
else
create the file
run the rest of the script
unlink the file when script is done
0 голосов
/ 06 января 2017

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

  • Если файл существует, S прекратит работу.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...