Мое предложение - использовать 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()
в результате запроса БД false
из-за сбоя include
- илипустая строка ( условие гонки )
Состояние гонки происходит, если первый посетитель выполняет эту строку:
$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 ().