Страница документации PHP для flock()
указывает, что она небезопасна для использования в IIS. Если я не могу полагаться на flock
при любых обстоятельствах, есть ли другой способ, которым я мог бы безопасно достичь того же?
PHP flock() альтернатива
Ответ 1
Нет альтернативы, позволяющей безопасно достичь этого при всех возможных возможных обстоятельствах. Что по дизайну компьютерных систем и задание не является тривиальным для кросс-платформенного кода.
Если вам нужно безопасно использовать flock()
, укажите документы для своего приложения.
В качестве альтернативы вы можете создать свой собственный механизм блокировки, однако вы должны обеспечить его атомарность. Это означает, что вы должны протестировать блокировку, и если она не существует, установите блокировку, пока вам нужно убедиться, что ничто другое не может получить блокировку между ними.
Это можно сделать, создав файл блокировки, представляющий блокировку, но только если он не существует. К сожалению, PHP не предлагает такую функцию для создания файла таким образом.
В качестве альтернативы вы можете создать каталог с mkdir()
и работать с результатом, потому что он вернет true
, когда каталог был создан и false
, если он уже существует.
Ответ 2
Вы можете реализовать шаблон блокировки блокировки файлов вокруг операций чтения/записи на основе mkdir, поскольку он является атомарным и довольно быстрым. Я стресс тестировал это, и в отличие от мгутт не нашел узкого места. Вы должны позаботиться о тупиковых ситуациях, хотя это, вероятно, то, что испытал 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:
[email protected]($lockname);
// attempt to lock, this is the really important atomic action:
while ([email protected]($lockname)){
if ($life)
if ((time()-$life)>120){
//release old locks
rmdir($lockname);
$life=false;
}
usleep(rand(50000,200000));//wait random time before trying again
}
}
Затем работайте над своим файлом в пути к файлу, и когда вы закончите, вызовите:
function unlockFile($filepath){
$unlockname= $filepath.".lock";
return @rmdir($unlockname);
}
Я решил удалить старые блокировки, а также после максимального времени выполнения PHP в случае выхода script до его разблокировки. Лучше всего было бы удалять блокировки всегда, когда script терпит неудачу. Для этого есть опрятный способ, но я забыл.
Ответ 3
Мое предложение - использовать 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 может содержать:
-
array()
в результате запроса db -
false
неудачногоinclude
- или пустая строка (состояние гонки)
Состояние гонки происходит, если первый посетитель выполняет эту строку:
$fp = fopen($cache_file, "c");
а другой посетитель выполнит эту строку через миллисекунду позже:
if ($fp !== false && flock($fp, LOCK_EX | LOCK_NB)) {
Это означает, что первый посетитель создает пустой файл, но второй посетитель создает блокировку и поэтому include
возвращает пустую строку.
Итак, вы видели много ошибок, которых можно избежать, используя mkdir()
и его 7x быстрее:
$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
P.S. как вы можете видеть, я использую file_exists()
перед mkdir()
. Это связано с тем, что мои тесты (немецкий) вызвали узкие места, используя только mkdir().
Ответ 4
Вот моя "альтернатива 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;
}
}
}
Ответ 5
Я понимаю, что этот вопрос несколько лет, но я, похоже, считал, что рабочий пример/замена для стаи может стоить того, чтобы строить. Я основывал это на других ответах, но для тех, кто просто ищет замену функциональности стада (вместо того, чтобы писать файл одновременно (хотя это и отражает пример рутинной рутификации PHP)). Я считаю, что следующее будет достаточным
function my_flock ($path,$release = false){
if ($release){
@rmdir($path);
} else {
return !file_exists($path) && @mkdir($path);
}
}
Ответ 6
На основании 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:
[email protected]($lockname);
// attempt to lock, this is the really important atomic action:
while ([email protected]($lockname)){
if ($life)
if ((time()-$life)>120){
//release old locks
rmdir($lockname);
}else [email protected]($lockname);
usleep(rand(50000,200000));//wait random time before trying again
}
}
Чтобы избежать взаимоблокировки, когда один сценарий в случае, если сценарий завершается до того, как он разблокирован, и один (или несколько сценариев) одновременно не дают результата в $ life = @filectime ($ lockname); потому что все сценарии запускаются одновременно, а затем каталог еще не создан. Чтобы разблокировать, затем позвоните:
function unlockFile($filepath){
$unlockname= $filepath.".lock";
return @rmdir($unlockname);
}
Ответ 7
Ни один из этих методов не является полностью атомарным.
Я провел несколько тестов, подтверждающих это.
Код для T7, использующий 7 файлов с именами по размеру в кБ:
clearstatcache();
$_DEBUG_ = false;
echo "Lock and flush tester.".time()."<br>";
$time_constant = 1570787996;
die; // Remove this line when you set time_constant
while ( time()<$time_constant )
{
usleep(500);
}
function test($n, $p, $_DEBUG_){
// $delay_multiplier = $n*2.5;
$sname = "$n"; // source
$tname = "$n.txt";// target
echo "<h4>$n at ".time()."</h4>";
for ($i = 0; $i<50; $i++ ){
$start = microtime(true);
clearstatcache(); // needed for filesize and touch
$st = stat("$sname");
$original_size = $st['size'];
if ( $_DEBUG_ )
echo "; 1) prevAccess by ".$st['mtime']." fsize ".$st['size']."; ";
$fsize = filesize($sname);
if ( $original_size <> $fsize )
die("; fsize total FAILTURE; ");
if ($fsize === 0)
echo "! <b>The fsize is 0</b>: stat(): ".$st['size']." ;";
else
{
// READ OPERATION AND LOCK FOR SHARE
$locked = false;
for ($c = 0; !$locked; $c++):
if ( $c > 400)
break;
$fp = fopen($sname, "r");
$locked = flock($fp, LOCK_SH);
if ($locked)
break;
else
{
echo "failed to get LOCK_SH;<br>";
usleep(5000);
}
endfor;
$s = fread($fp, $fsize );
$success = flock($fp, LOCK_UN);
if ( $success === false )
die("; r flock release failed; ");
$success = fclose($fp);
if ( $success === false )
die("; fclose failed; ");
// 10 - loaded data , $p - broser
if ( $success )
{
$result = touch("$sname",strlen($s),$p);
if ( $_DEBUG_ )
echo "; TOUCH: $result;";
}
else
die("fclose FAIL.");
if ( strlen($s)<60 )
echo "*$s LENGTH:".strlen($s)."<br>";
}
clearstatcache();
$st = stat("$tname");
if ( $_DEBUG_ )
echo "; 2) prevAccess by ".$st['mtime']." fsize is ".$fsize."; ";
// WRITE OPERATION WITH LOC_EX
$fp = fopen($tname, "w");
$locked = false;
/*
// TOTO NEMÁ VLIV NA ZAMKNUTÍ
for ($c = 0; !$locked; $c++ ):
$c++;
if ( $c > 400)
break;
$locked = flock($fp, LOCK_EX);
if ($locked)
break;
else
{
echo "failed to get LOCK_EX;<br>";
usleep(5000);
}
endfor;
*/
$locked = flock($fp, LOCK_EX);
if ( $locked ) { // acquire an exclusive lock
$success = fwrite($fp, $s);
if ( $success === false)
echo "; w FAILED;";
else
if ( $_DEBUG_ )
echo " $success B written; ";
$success = fflush($fp);// flush output before releasing the lock
if ( $success === false )
echo "; flush FAILED; ";
$success = flock($fp, LOCK_UN); // release the lock
if ( $success === false )
echo "; release FAILED; ";
$success = fclose($fp);
if ( $success === false )
echo "; fclose FAILED; ";
clearstatcache(); // needed for filesize and touch
$fsize = filesize($tname);
if ($original_size>$fsize)
{
echo "; <b>WRITE FAILED, restoring</b>;";
$original_fname = "$n";
$result = copy($original_fname, $tname);
if ($result == false )
die(" <b>TOTAL FAILTURE: copy failed.</b>");
else
echo " <b>RESTORED</b>;";
}
else
{
if ($fsize === 0)
echo "! THE FILE WAS NOT WRITTEN: data length: ".strlen($s)." fsize: $fsize RESOURCE: $fp<br>";
if ( $success )
touch("$tname",$fsize,$p);
}
} else {
echo "Couldn't get the lock!";
}
$time_elapsed_secs = microtime(true) - $start;
//usleep( $delay_multiplier + $n*rand(2,6) );
if ( $time_elapsed_secs === 0 )
echo " FAILED ";
echo "time: $time_elapsed_secs s<br>";
}
}
// headers to identify originator of the request
switch ( $_SERVER['HTTP_USER_AGENT'] ):
// FF 1:
case "Mozilla/5.0 (Windows NT 5.1;) Gecko":
$p = 1; break;
// Chrome:
case "Mozilla/5.0 (Windows NT 5.1) AppleWebKit Chrome Safari":
$p = 2; break;
// OPERA:
case "Mozilla/5.0 (Windows NT 5.1) AppleWebKit Chrome Safari":
$p = 3; break;
endswitch;
copy("523","523.txt");
copy("948","948.txt");
copy("1371","1371.txt");
copy("1913","1913.txt");
copy("2701","2701.txt");
copy("4495","4495.txt");
copy("6758","6758.txt");
test("523",$p,$_DEBUG_);
test("948",$p,$_DEBUG_);
test("1371",$p,$_DEBUG_);
test("1913",$p,$_DEBUG_);
test("2701",$p,$_DEBUG_);
test("4495",$p,$_DEBUG_);
test("6758",$p,$_DEBUG_);
Код для T8 (тест блокировки mkdir):
clearstatcache();
$_DEBUG_ = false;
echo "Atomicity tester.".time()."<br>";
$time_constant = 1570787996;
die; // Remove this line when you set time_constant
while ( time()<$time_constant )
{
usleep(500);
}
/*
c is counter for optimalization
first call must have c = 0;
*/
function atomicFuse($n, $c, $disableDelay = false){
$start = false;
if ( !file_exists("$n.t") )
$start = mkdir("$n.t");
if ( !$disableDelay ){
if ( $start == false )
{
$n = $n*30;
switch($c): // Delay example increase:
case 0: break; // 0,01569 total
case 1: break; // 0,03138 total
case 2: $n = $n*2; break; // 0,06276 total
case 3: $n = $n*4; break; // 0,12552 total
// case 4: You need at least *6 or *8 to get out of problems with extrem times
case 4: $n = $n*8; break; // 0,25104 t.(upper limit)
// In case of heavy traffic:
case 5: $n = $n*8; break; // 0,36087 total extrem
case 6: $n = $n*10; break; // 0,51777 total extrem
case 7: $n = $n*20; break; // 1,03554 total extrem
default: $n = $n*8; break;
endswitch;
usleep($n);
echo ($n)."<br>";
}
}
return $start;
}
function test($n, $p, $_DEBUG_){
$fp = null;
$sname = "$n"; // source
$tname = "$n.txt";// target
echo "<h4>$n at ".time()."</h4>";
for ($i = 0; $i<50; $i++ ){
$start_time = microtime(true);
{
$start = atomicFuse($n,0);
if (!$start) $start = atomicFuse($n,1);
if (!$start) $start = atomicFuse($n,2);
if (!$start) $start = atomicFuse($n,3);
if (!$start) $start = atomicFuse($n,4);
if (!$start) $start = atomicFuse($n,5);
if (!$start) $start = atomicFuse($n,6);
if (!$start) $start = atomicFuse($n,7);
if (!$start) $start = atomicFuse($n, false);
if (!$start) echo "<b>Atomicity failed.</b> ";
if ( $start )
{
echo "<b>Atomicity OK.</b> ";
/////////////////////////////
// CHECK FILESIZE VALIDITY //
/////////////////////////////
clearstatcache(); // needed for filesize and touch
$st = stat("$sname");
$original_size = $st['size'];
if ( $_DEBUG_ )
echo "; 1) prevAccess by ".$st['mtime']." fsize ".$st['size']."; ";
$fsize = filesize($sname);
if ( $original_size <> $fsize )
die("; fsize total FAILTURE; ");
if ($fsize === 0)
echo "! <b>The fsize is 0</b>: stat(): ".$st['size']." ;";
///////////////////
// OPEN THE FILE //
///////////////////
$fp = fopen($sname, "r");
$s = fread($fp, $fsize );
$success = fclose($fp);
if ( $success === false )
die("; fclose failed; ");
// 10 - loaded data, $p - browser
if ( $success )
{
$result = touch("$sname",strlen($s),$p);
if ( $_DEBUG_ )
echo "; TOUCH: $result;";
}
else
die("fclose FAIL.");
if ( strlen($s)<60 )
echo "*$s LENGTH:".strlen($s)."<br>";
}
}
if ( $start )
{
clearstatcache();
$st = stat("$tname");
if ( $_DEBUG_ )
echo "; 2) prevAccess by ".$st['mtime']." fsize is ".$fsize."; ";
// WRITE OPERATION WITH LOC_EX
$fp = fopen($tname, "w");
if ( true ) { // acquire an exclusive lock
$success = fwrite($fp, $s);
if ( $success === false)
echo "; w FAILED;";
else
if ( $_DEBUG_ )
echo " $success B written; ";
$success = fflush($fp);// flush output before releasing the lock
if ( $success === false )
echo "; flush FAILED; ";
if ( $success === false )
echo "; release FAILED; ";
$success = fclose($fp);
if ( $success === false )
echo "; fclose FAILED; ";
clearstatcache(); // needed for filesize and touch
$fsize = filesize($tname);
if ($original_size>$fsize)
{
echo "; <b>WRITE FAILED, restoring</b>;";
$original_fname = "$n";
$result = copy($original_fname, $tname);
if ($result == false )
die(" <b>TOTAL FAILTURE: copy failed.</b>");
else
echo " <b>RESTORED</b>;";
}
else
{
if ($fsize === 0)
echo "! THE FILE WAS NOT WRITTEN: data length: ".strlen($s)." fsize: $fsize RESOURCE: $fp<br>";
if ( $success )
touch("$tname",$fsize,$p);
}
} else {
echo "Couldn't get the lock!";
}
$success = rmdir("$n.t"); // remove atomic fuse
if ( $success )
echo "<h4>DIR REMOVED</h4>";
else
echo "<h4>DIR NOT REMOVED</h4>";
} // start
else
echo "skipped";
$time_elapsed_secs = microtime(true) - $start_time;
if ( $time_elapsed_secs === 0 )
echo " FAILED ";
echo "time: $time_elapsed_secs s<br>";
} // for
}
switch ( $_SERVER['HTTP_USER_AGENT'] ):
case "":
$p = 1; break;
case "":
$p = 2; break;
case "":
$p = 3; break;
endswitch;
copy("523","523.txt");
copy("948","948.txt");
copy("1371","1371.txt");
copy("1913","1913.txt");
copy("2701","2701.txt");
copy("4495","4495.txt");
copy("6758","6758.txt");
test("523",$p,$_DEBUG_);
test("948",$p,$_DEBUG_);
test("1371",$p,$_DEBUG_);
test("1913",$p,$_DEBUG_);
test("2701",$p,$_DEBUG_);
test("4495",$p,$_DEBUG_);
test("6758",$p,$_DEBUG_);
Примечание: T5-T7 - я не определил, были ли повреждения файла вызваны fflush или fwrite, но именно в этих тестах произошла такая ошибка.
Примечание: T8 - Особая проблема этого теста заключается в том, что он часто слишком долго ждет в начале блока тестирования (в начале функции тестирования). Там даже задержки как 7 секунд ожидания. Но я также попытался удалить эти цифры, и среднее значение не слишком изменилось, поэтому кривая T8 останется неизменной после этого изменения. Проблема здесь заключается в том, что использование задержки в цикле не является идеальным решением проблемы, оно делает вероятность отказа еще выше. Обратите внимание, что под "отказом" я не имею в виду повреждение файла, но пропущение данной атомарной задачи из-за истечения времени ожидания.