PHP flock () альтернатива - PullRequest
       24

PHP flock () альтернатива

12 голосов
/ 06 августа 2011

Страница документации PHP для flock() указывает, что использование IIS небезопасно.Если я не могу положиться на flock при любых обстоятельствах, есть ли другой способ, которым я мог бы безопасно достичь того же самого?

Ответы [ 6 ]

7 голосов
/ 06 августа 2011

Нет альтернативы, позволяющей безопасно достичь того же при любых мнимых возможных обстоятельствах. Это дизайн компьютерных систем, и работа не является тривиальной для кросс-платформенного кода .

Если вам нужно безопасно использовать flock(), документируйте вместо этого требования для вашего приложения.

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

Это может быть сделано путем создания файла блокировки, представляющего блокировку, но только если она не существует. К сожалению, PHP не предлагает такой функции для создания файла таким способом.

В качестве альтернативы вы можете создать каталог с помощью mkdir() и работать с результатом, потому что он вернет true при создании каталога и false, если он уже существовал.

3 голосов
/ 19 марта 2014

Вы можете реализовать шаблон блокировки файлов для операций чтения / записи на основе mkdir , поскольку это атомарное и довольно быстрое действие. Я стресс-тестировал это и в отличие от mgutt не нашел узкого места. Вы должны позаботиться о тупиковых ситуациях, хотя это, вероятно, то, что испытал mgutt. Мертвая блокировка - это когда две попытки блокировки продолжают ждать друг друга. Это может быть исправлено случайным интервалом при попытках блокировки. Вот так:

// call this always before reading or writing to your filepath in concurrent situations
function lockFile($filepath){
   clearstatcache();
   $lockname=$filepath.".lock";
   // if the lock already exists, get its age:
   $life=@filectime($lockname);
   // attempt to lock, this is the really important atomic action:
   while (!@mkdir($lockname)){
         if ($life)
            if ((time()-$life)>120){
               //release old locks
               rmdir($lockname);
               $life=false;
         }
         usleep(rand(50000,200000));//wait random time before trying again
   }
}

Затем поработайте над своим файлом в filepath, а когда закончите, позвоните:

function unlockFile($filepath){
   $unlockname= $filepath.".lock";   
   return @rmdir($unlockname);
}

Я выбрал удаление старых блокировок, после истечения максимального времени выполнения PHP в случае выхода из скрипта до его разблокировки. Лучшим способом было бы снимать блокировки всегда, когда скрипт завершился неудачей. Для этого есть хороший способ, но я забыл.

1 голос
/ 05 сентября 2018

Вот моя «альтернатива PHP flock ()» - сборка на mkdir().

Идея сделать это с помощью mkdir () возникла из здесь и здесь .

Моя версия

  • проверяет, есть ли у меня доступ к блокировке. Это также предотвращает блокировку самого себя, если я создаю и использую класс несколько раз для одного и того же basedir.name
  • проверяет, был ли создан мой файл блокировки, с помощью которого я запрашиваю блокировку доступа
  • позволяет мне получить блокировку доступа в том порядке, в котором я пришел, чтобы попросить его
  • прекращает ожидание и зацикливание, если он не может получить доступ к блокировке в указанное мной время
  • удаляет мертвые файлы блокировки (= файлы, в которых SID PID больше не существует)

Вы можете использовать PHP-класс следующим образом:

//$dir        (string) = base-directory for the lock-files (with 'files' I mean directories => mode 0644)
// 2       (float/int) = time to wait for lock-access before returning unsuccessful (default is 0 <= try once and return)
//'.my_lock'  (string) = the way you want to name your locking-dirs (default is '.fLock')
$lock = new FileLock($dir, 2, '.my_lock');

//start lock - a locking directory will be created looking like this:
//$dir/.my_lock-1536166146.4997-22796
if ($lock->lock()) {
    //open your file - modify it - write it back
} else { /* write alert-email to admin */ }

//check if I had locked before
if ($lock->is_locked) { /* do something else with your locked file */ }

//unlock - the created dir will be removed (rmdir)
$lock->unlock();

Вот рабочий класс:

//build a file-locking class
define('LOCKFILE_NONE', 0);
define('LOCKFILE_LOCKED', 1);
define('LOCKFILE_ALREADY_LOCKED', 2);
define('LOCKFILE_ALREADY_LOCKED_IN_OTHER_CLASS', 3);
define('LOCKFILE_FAILED_TO_OBTAIN_LOCK', false);
define('LOCKFILE_FAILED_TO_OBTAIN_LOCK_BY_TIMEOUT', '');


class FileLock {
    //FileLock assumes that there are no other directories or files in the
    //lock-base-directory named "$name-(float)-(int)"
    //FileLock uses mkdir() to lock. Why?
    //- mkdir() is atomic, so the lock is atomic and faster then saving files.
    //  Apparently it is faster than flock(), that requires several calls to the
    //  file system.
    //- flock() depends on the system, mkdir() works everywhere.

    private static $locked_memory = array();

    public function __construct($lockbasedir, $wait_sec=0, $name='.fLock') {
        $this->lockbasedir = (string)$lockbasedir;
        $this->wait        = (float)$wait_sec;
        $this->name        = (string)$name;

        $this->pid         = (int)getmypid();

        //if this basedir.name was locked before and is still locked don't try to lock again
        $this->is_locked   = empty(self::$locked_memory[$this->lockbasedir . $this->name]) ? LOCKFILE_NONE : LOCKFILE_ALREADY_LOCKED;
    }

    public function lock() {
        if ($this->is_locked) return $this->is_locked;

        $break_time = microtime(true);

        //create the directory as lock-file NOW
        $this->lockdir = "{$this->name}-" . number_format($break_time, 4, '.', '') . "-{$this->pid}";
        @mkdir("{$this->lockbasedir}/{$this->lockdir}", 0644);

        $break_time += $this->wait;

        //try to get locked
        while ($this->wait == 0 || microtime(true) < $break_time) {

            //get all locks with $this->name
            $files = preg_grep("/^{$this->name}-\d+\.\d+-\d+$/", scandir($this->lockbasedir));

            //since scandir() is sorted asc by default
            //$first_file is the next directory to obtain lock
            $first_file = reset($files);

            if (!$first_file) {
                //no lock-files at all
                return $this->is_locked = LOCKFILE_FAILED_TO_OBTAIN_LOCK;
            } elseif ($first_file == $this->lockdir) {
                //Its me!! I'm getting locked :)
                self::$locked_memory[$this->lockbasedir . $this->name] = 1;
                return $this->is_locked = LOCKFILE_LOCKED;
            } elseif (preg_match("/^{$this->name}-\d+\.\d+-{$this->pid}$/", $first_file)) {
                //my process-ID already locked $this->name in another class before
                rmdir("{$this->lockbasedir}/{$this->lockdir}");
                $this->lockdir = $first_file;
                self::$locked_memory[$this->lockbasedir . $this->name] = 1;
                return $this->is_locked = LOCKFILE_ALREADY_LOCKED_IN_OTHER_CLASS;
            }

            //missing lock-file for this job
            if (array_search($this->lockdir, $files) === false) return LOCKFILE_FAILED_TO_OBTAIN_LOCK;

            //run only once
            if ($this->wait == 0) break;

            //check if process at first place has died
            if (!posix_getsid(explode('-', $first_file)[2])) {
                //remove dead lock
                @rmdir("{$this->lockbasedir}/$first_file");
            } else {
                //wait and try again after 0.1 seconds
                usleep(100000);
            }
        }

        return $this->is_locked = LOCKFILE_FAILED_TO_OBTAIN_LOCK_BY_TIMEOUT;
    }

    public function unlock($force=false) {
        if ($force || $this->is_locked == 1) {
            rmdir("{$this->lockbasedir}/{$this->lockdir}");
            self::$locked_memory[$this->lockbasedir . $this->name] = $this->is_locked = LOCKFILE_NONE;
        }
    }
}
1 голос
/ 28 октября 2011

Мое предложение - использовать mkdir() вместо flock().Это реальный пример кеширования для чтения / записи, показывающий различия:

$data = false;
$cache_file = 'cache/first_last123.inc';
$lock_dir = 'cache/first_last123_lock';
// read data from cache if no writing process is running
if (!file_exists($lock_dir)) {
    // we suppress error messages as the cache file exists in 99,999% of all requests
    $data = @include $cache_file;
}
// cache file not found
if ($data === false) {
    // get data from database
    $data = mysqli_fetch_assoc(mysqli_query($link, "SELECT first, last FROM users WHERE id = 123"));
    // write data to cache if no writing process is running (race condition safe)
    // we suppress E_WARNING of mkdir() because it is possible in 0,001% of all requests that the dir already exists after calling file_exists()
    if (!file_exists($lock_dir) && @mkdir($lock_dir)) {
        file_put_contents($cache_file, '<?php return ' . var_export($data, true) . '; ?' . '>')) {
        // remove lock
        rmdir($lock_dir);
    }
}

Теперь мы пытаемся добиться того же с flock():

$data = false;
$cache_file = 'cache/first_last123.inc';
// we suppress error messages as the cache file exists in 99,999% of all requests
$fp = @fopen($cache_file, "r");
// read data from cache if no writing process is running
if ($fp !== false && flock($fp, LOCK_EX | LOCK_NB)) {
    // we suppress error messages as the cache file exists in 99,999% of all requests
    $data = @include $cache_file;
    flock($fp, LOCK_UN);
}
// cache file not found
if (!is_array($data)) {
    // get data from database
    $data = mysqli_fetch_assoc(mysqli_query($link, "SELECT first, last FROM users WHERE id = 123"));
    // write data to cache if no writing process is running (race condition safe)
    $fp = fopen($cache_file, "c");
    if (flock($fp, LOCK_EX | LOCK_NB)) {
        ftruncate($fp, 0);
        fwrite($fp, '<?php return ' . var_export($data, true) . '; ?' . '>');
        flock($fp, LOCK_UN);
    }
}

Важная частьравен LOCK_NB, чтобы избежать блокировки всех последовательных запросов:

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

Без него код создал бы огромное узкое место!

Еще одна важная часть - if (!is_array($data)) {.Это связано с тем, что $ data может содержать:

  1. array() в результате запроса БД
  2. false из-за сбоя include
  3. илипустая строка ( условие гонки )

Состояние гонки происходит, если первый посетитель выполняет эту строку:

$fp = fopen($cache_file, "c");

, а другой посетитель выполняет эту строку одну миллисекундупозже:

if ($fp !== false && flock($fp, LOCK_EX | LOCK_NB)) {

Это означает, что первый посетитель создает пустой файл, а второй посетитель создает блокировку, и поэтому include возвращает пустую строку.

Итак, вы видели много ловушек, которыеможно избежать с помощью mkdir() и его в 7 раз быстрее:

$filename = 'index.html';
$loops = 10000;
$start = microtime(true);
for ($i = 0; $i < $loops; $i++) {
    file_exists($filename);
}
echo __LINE__ . ': ' . round(microtime(true) - $start, 5) . PHP_EOL;
$start = microtime(true);
for ($i = 0; $i < $loops; $i++) {
    $fp = @fopen($filename, "r");
    flock($fp, LOCK_EX | LOCK_NB);
}
echo __LINE__ . ': ' . round(microtime(true) - $start, 5) . PHP_EOL;

результат:

file_exists: 0.00949
fopen/flock: 0.06401

PS, как вы видите, я использую file_exists() перед mkdir().Это связано с тем, что мои тесты (на немецком языке) привели к появлению узких мест при использовании только mkdir ().

0 голосов
/ 15 апреля 2019

На основе mkdir:

// call this always before reading or writing to your filepath in concurrent situations
function lockFile($filepath){
   clearstatcache();
   $lockname=$filepath.".lock";
   // if the lock already exists, get its age:
   $life=@filectime($lockname);
   // attempt to lock, this is the really important atomic action:
   while (!@mkdir($lockname)){
     if ($life)
        if ((time()-$life)>120){
           //release old locks
           rmdir($lockname);
     }else $life=@filectime($lockname);
     usleep(rand(50000,200000));//wait random time before trying again
   }
}

Во избежание тупиковой ситуации, когда один сценарий в случае выхода из сценария до его разблокировки и один (или несколько сценариев) в то же время не дают результата на $ life = @filectime ($ lockname);потому что все сценарии запускаются одновременно, а затем каталог еще не создан.Чтобы разблокировать, позвоните по номеру:

function unlockFile($filepath){
   $unlockname= $filepath.".lock";   
  return @rmdir($unlockname);
}
0 голосов
/ 23 сентября 2016

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

function my_flock ($path,$release = false){
    if ($release){
        @rmdir($path);
    } else {
        return !file_exists($path) && @mkdir($path);
    }
}
...