- Подробности
-
Категория: PHP. Файлы
Суть проблемы такова:
Есть база данных, используемая на сайте (например, база для регистрации пользователей, куда записывается их имя и email), она лежит в текстовом файле построчно (в дальнейшем, "file_base.dat".). Два пользователя активизируют сервер через командную строку в броузере, для ввода свох имен и email. Сервер отсылает их к скрипту. Оба пользователя "начинают движение" по скриптовому потоку (тексту php файла) сверху вниз, причем, Первый "бежит" на долю секунды быстрее Второго. Когда они достигают того места, где скрипт исполняет их запрос, движение по потоку останавливается, в их броузер выводится сгенерированная скриптом страница в виде html. Чтобы из file_base.dat прочитать данные, этот файл надо открыть на чтение (функция — @file), чтобы записать что-то в него, надо открыть на запись (функция — @fopen). В скрипте это выглядит так:
< ?
……
// читаем данные из файла-базы
$f = @file ("file_base.dat", "r"); // здесь находится 2 пользователь
// здесь идет текст скрипта
// открываем файл-базу на запись
$fp = @fopen ("file_base.dat", "w"); // здесь находится 1 пользователь
// записываем в файл-базу данные из выше
// прочитанного file_base.dat — переменная $f
// и добавляем еще одну строку с данными нового пользователя
// закрываем файл-базу
@fclose ($fp);
……
? >
То есть, чтобы новый пользователь появился в нашей file_base.dat, мы считываем оттуда информацию, которую кладем в переменную $f, затем записываем в этот же файл эту переменную $f (при этом, file_base.dat переписываем полностью) и внизу дописываем еще одну строку с данными нашего нового пользователя. Мы видим, что на чтение и на запись file_base.dat открывается в разных местах нашего скрипта. Если оба пользователя, одновременно, достигли "своего" места в этом скрипте, но один из file_base.dat только начал читать данные, а другой уже их прочитал, продвинулся чуть ниже и, в этот момент, находится на отметке записи в файл file_base.dat своих данных. То, в этом случае, от нашего файла file_base.dat ничего не останется. В связи с этим, в php была введена функция совместного доступа @flock, которая призвана не допускать совместный доступ к файлу на чтение и запись.
< ?
……
// читаем данные из файла файла-базы
$f = @file ("file_base.dat", "r"); // здесь находится 2 пользователь
// здесь идет текст скрипта
// открываем файл-базу на запись
$fp = @fopen ("file_base.dat", "w"); // здесь находится 1 пользователь
// блокируем файл-базу на чтение
@flock ($fp, lock_ex)
// записываем в файл-базу данные из выше
// прочитанного file_base.dat — переменная $f
// и добавляем еще одну строку с данными нового пользователя
// снимаем блокировку
@flock ($fp, lock_un)
// закрываем файл-базу
@fclose ($fp);
……
? >
Теперь, вроде бы все нормально, файл блокируется на чтение, когда в него что-то пишут. Но, что произойдет в нашем случае, когда, 1 пользователь прочитал данные файла и пришел к отметке открытия file_base.dat на запись, оркыл его и заблокировал на чтение функцией @flock, а 2 пользователь, именно в этот момент пришел к точке, где данные из файла считываются функцией @file? 2 пользователь ничего не прочитает из этого файла, потому что file_base.dat блокирован 1 пользователем командой @flock на чтение. 2 пользователь просто "пробежит" дальше по потоку скрипта ничего из файла file_base.dat не прочитав, то есть наша переменная $f будет пустой. Но, дальше-то он будет записывать в этот файл, то что ранее с него считал. В итоге, в наш file_base.dat запишется только одна строка с его именем и email. То есть, file_base.dat будет потерян.
В связи с чем, была придумана функция: read_file
function read_file($path)
{
if(!is_file($path)) {return false; }
elseif(!filesize($path)) {return array(); }
elseif($array=file($path)) {return $array; }
else { while(!$array=file($path)){sleep(1);} return $array; }
}
Суть ее такова: пока файл блокирован на чтение, пользователь, который хочет считать из него информацию, находится в цикле, то есть, как бы "стоит" на месте, не "бежит" дальше по тексту скрипта, ожидая, когда файл разблокируется на чтение. В итоге наш скрипт принимает такой вид:
< ?
function read_file($path)
{
if(!is_file($path)) {return false; }
elseif(!filesize($path)) {return array(); }
elseif($array=file($path)) {return $array; }
else { while(!$array=file($path)){sleep(1);} return $array; }
}
……
// проверяем заблокирован ли файл на чтение,
// если заблокирован, назначаем цикл с остановкой,
// пока блокировка не будет снята, после снятия блокировки,
// читаем данные из файла файла-базы
$f = read_file ("file_base.dat", "r"); // здесь находится 2 пользователь
// здесь идет текст скрипта
// открываем файл-базу на запись
$fp = @fopen ("file_base.dat", "w"); // здесь находится 1 пользователь
// блокируем файл-базу на чтение
@flock ($fp, lock_ex)
// записываем в файл-базу данные из выше
// прочитанного file_base.dat — переменная $f
// и добавляем еще одну строку с данными нового пользователя
// снимаем блокировку
@flock ($fp, lock_un)
// закрываем файл-базу
@fclose ($fp);
……
? >
Вроде бы все нормально. Когда 1 пользователь начал записывать свои данные в file_base.dat, 2 "спит" одну секунду, ожидая разблокировки file_base.dat, когда file_base.dat разблокирован, он считывает из него информацию и начинает движение дальше. Но, есть одно "но". Если 2 пользователь чуть быстрее 1 пользователя "добежал" до "своего" места, прочитал половину данных из нашего file_base.dat, который еще не был блокирован, и, именно, в этот момент, 1 "добежал" по тексту скрипта до @flock ($fp, lock_ex), то есть заблокировал наш файл, то переменная $fp будет иметь только половину данных из нашего file_base.dat, потому что, именно, в этот момент file_base.dat был заблокирован. В итоге, в наш file_base.dat будет записана половина информации из нашей базы + одна сторка нового пользователя. То есть, опять, file_base.dat мы потеряли.
Суть подхода, который предлагаем мы для решения этой проблемы такой:
Во время считывания из файла или записи в него данных, на сайте появляется текстовая строка с названием этого файла, когда из файла информация прочиталась или записалась, эта текстовая строка удаляется. Пока она есть, значит кто-то уже использует базу данных на чтение или запись, пользователь, который хочет обратиться к этой базе на чтение или запись, находится в цикле, ожидая пока текстовая строка исчезнет. Как только она исчезает, он читает или пишет в базу данных, сам при этом создавая такую же текстовую строку, тем самым препятствуя доступу двух и более пользователей к базе.
Для чего, на сайте создадим папку для хранения текстовых строк. Например, lock. Наш файл на сайте лежит в папке database, значит в папке lock надо создать папку database. Теперь, когда пользователь обращается к file_base.dat, для чтения или записи в него, абсолютный путь http://наш_сайт.ru/database/file_base.dat, в папке http://наш_сайт.ru/lock/database/ появляется файл-строка file_base.dat.tmp, абсолютный путь — http://наш_сайт.ru/lock/database/file_base.dat.tmp, закрывая доступ к file_base.dat, как только пользователь считал или записал информацию в наш файл-базу, текстовая строка file_base.dat.tmp удаляется, открывая доступ другим пользователям к file_base.dat.
< ?
// объявляем директорию для временных файлов
$lock_dir = "lock";
// функция для создания временных текстовых файлов
function touchstring($file) {
global $lock_dir;
$tmp = "$lock_dir/".$file.".tmp";
while(1) {
if (is_file($tmp))
{
while(file_exists($tmp))
{
$file_exist++;
if($file_exist > 10){break;}
clearstatcache();
sleep(1);
}
}
return touch($tmp);
}
}
// функция удаляющая временные текстовые файлы
function delstring($file) {
global $lock_dir;
$tmp = "$lock_dir/".$file.".tmp";
return unlink($tmp);
}
// альтернатива функци @file
function filearray($file) {
if (!is_readable($file)) return false;
touchstring($file);
$bufer = file($file);
delstring($file);
return $bufer;
}
// альтернатива функци @fopen
function openfile($file, $mode) {
touchstring($file);
return fopen($file, $mode);
}
// альтернатива функци @fclose
function closefile($fido, $file) {
$sito = fclose($fido);
delstring($file);
return $sito;
}
……
// проверяем есть ли временный файл, запрещающий
// чтение и запись в файл-базу, если он есть, останавливаем
// пользователя в цикле, где он ожидает пока временный файл
// исчезнет, как толко он исчезает, сами создаем такой же временный
// файл, запрещая доступ к файлу-базе, только после этого читаем данные из
// файла-базы, удаляем временный файл, открывая доступ к базе
// другим пользователям
$f = filearray ("database/file_base.dat"); // здесь находится 2 пользователь
// здесь идет текст скрипта
// проверяем есть ли временный файл, запрещающий
// чтение и запись в файл-базу, если он есть, останавливаем
// пользователя в цикле, где он ожидает пока временный файл
// исчезнет, как толко он исчезает, сами создаем такой же временный
// файл, запрещая доступ к файлу-базе, только после этого открываем
// файл-базу на запись, временный файл не удаляется, закрывая доступ к базе
// другим пользователям
$fp = openfile ("database/file_base.dat", "w"); // здесь находится 1 пользователь
// записываем в файл-базу данные из выше
// прочитанного database/file_base.dat — переменная $f
// и добавляем еще одну строку с данными нового пользователя
// закрываем файл-базу, удаляем временный файл, открывая доступ к базе
// другим пользователям
closefile ($fp, "database/file_base.dat");
// обратите внимание, что в closefile два аргумента: $fp и
// database/file_base.dat
……
? >
В этой ситуации мы видим, что когда один пользователь достиг точки считывания с file_base.dat или, наоборот, другой пытается записать в file_base.dat информацию, нам ничего не страшно. Потому что, как в одном, так и в другом случае, появился маленький текстовый файл file_base.dat.tmp, который не дает ни одному ни другому совместно читать или писать в file_base.dat.
В нашем варианте есть одна "дыра", если вдруг пользователь обратился к базе данных, создав текстовую строку блокировки, незакончил как-бы цикл (свет погас и компьютер выключился), и эта текстовая блокирующая строка осталась лежать в папке lock, то наша программа "подвиснет", не давая никому пройти, из-за этого оставшегося флага.
Мы не стали усложнять функции проверкой на удаление этого файла-флага, а, просто ввели в функции его создающей, предел "подвисания" компьютера 10 секунд ($file_exist++; if($file_exist > 10){break;}), через 10 секунд он автоматически выйдет из цикла и сотрет временный файл. В этом кроется опасность, но она ничтожна мала по сравнению с теми, которые были описаны выше. Считаем, что подход описанный выше, защитит от обвала базы данных, которая лежит в текстовом файле и не будет особо заметно флагов блокировки, когда на сайте ее, одновременно использует до 10 человек. Мы знаем, что наш скрипт сервером исполняется 0,5 сек, в этом случае его пропускная способность в час составит до 1800 человек. Уменьшим ее вдвое, 900 человек, за сутки 21600 человек. Согласитесь, и без mysql можно обойтись.
Для проверки работоспособности этих функций запишите вручную в папку lock этот самый блокирующий файл file_base.dat.tmp, в нашем случае, это: http://наш_сайт.ru/lock/database/file_base.dat.tmp, тем самым, блокируя доступ к базе http://наш_сайт.ru/database/file_base.dat, запустите программу, обратитесь к базе, вы увидите, что флаг работает и броузер "стоит" на месте, удалите file_base.dat.tmp файл (разблокируя) и подвисание закончится.
Теперь у нас есть функции:
1. filearray , заменяющая @file
2. openfile , заменяющая @fopen
3. closefile , заменяющая @fclose