css, html, php, javascript, jQuery, ajax … – решения, примеры, рецепты
13 Янв
Чтение и запись в текстовый файл казалось бы простейшая операция. Блокировал, открыл, записал, закрыл, снял блокировку с файла. Однако просто это только на первый взгляд….
Давайте подумаем, что произойдет, если к одному и тому же файлу одновременно обратятся несколько процессов, с целью записать туда какую-либо информацию? Могу сразу ответить: часть, либо вся информация, хранящаяся в файле будет безвозвратно потеряна. И просто блокировкой файла здесь не обойтись.
Такая задача встала передо мной при написании скрипта каталога для обмена ссылками LinkExchanger. Информация там должна была храниться именно в файлах. В итоге получилась вот такая функция, код которой приведен ниже…
function WriteToFile ($path_to_file,$data) {
$lock = fopen(PATH_BLOCKFILE,"a");
if(flock($lock, LOCK_EX)) {
$tmp=fopen(PATH_TEMPFILE,"w");
for($i=0;$i<count($data);$i++) {
fputs($tmp, "$data[$i]\n");
}
fclose($tmp);
unlink("$path_to_file");
rename(PATH_TEMPFILE, "$path_to_file");
flock($lock, LOCK_UN);
fclose($lock);
}
}
Давайте разберем очень подробно, как это работает.
Функция WriteToFile принимает два аргумента: путь к файлу, в который будет записана информация и собственно информация, представленная в виде массива. Нам понадобятся еще два файла, пути к которым определяются константами PATH_BLOCKFILE и PATH_TEMPFILE. Собственно из названий понятно, что первый – это некий блокирующий файл, а второй – файл для временного хранения информации.
Первое, что мы делаем – открываем на запись блокирующий файл. Далее ставим на него блокировку, и если эта операция прошла успешно, открываем на запись временный файл, в который и пишем нашу информацию.
Закрываем временный файл, удаляем исходный файл и переименовываем временный файл, давая ему имя исходного. Далее снимаем блокировку с блокирующего файла и закрываем его.
Это все. Проверено годами работы в условиях массового использования и на самых различных хостингах.
Единственное, за чем надо следить, чтобы при добавлении информации не было превышено допустимое дисковое пространство.
Отзывов (36) на «PHP: запись информации в текстовый файл»
Я так понял, имя временного файла постоянно, и во избежание конфликтов используется блокирующий файл.
А не лучше ли имя временного файла задавать случайным уникальным числом? Например функцией uniqid(). И удалять его по окончании работы скрипта.
Только папку периодически подчищать (довольно редко) если случаличь ошибки создания/переименования (чтоб мусора не накапливалось)?
Можно. Это наверное дело вкуса. А собственно вся фишка в блокирующем файле, который существует изначально под своим конкретным именем. Он служит как бы «оберткой» для реального файла. Для того, чтобы попасть «внутрь» и работать с реальным файлом, процесс должен заблокировать «блокирующий» файл. Если файл уже заблокирован, процесс будет ожидать пока он не освободится. Таким образом гарантируется, что уже с реальным файлом одновременно может работать только один процесс, что и обеспечивает высокую надежность.
Я так понимаю, что временный файл многопоточности никак не помогает, а служит только для того, чтоб не потерять старую информацию во время записи новой при сбоях (выключение света, молоток по винту
).
Я все таки не понял, почему недостаточно обычной блокировки?
To: Илья Колесников (Я все таки не понял, почему недостаточно обычной блокировки?)
Есть ряд недостатков у блокировки.
Гланое – возможность создания неполного файла. Порядок дейсивий: открять файл -> заблокировать -> записать -> разблокировать -> закрыть. Все это требует времени в течении которого файл недоступен. И главное: при ошибки в записи файл будет содержать ошибочные данные.
При подмене файла: основной файл не трогается, работа идет со временным и только при удачном(!) создании временного файла – вызов крайне быстрых функций переименования.
Выше надежность и скорость.
А не может случиться так, что к файлу обратятся сразу, скажем, двое человек. Запуститься 2 процеса? Создадуться 2 временных файла? А потом они по очереди заменят собой основной? Но ведь тогда информация только с одного временного дополнит предидущий оригинал, з того, который будет последним?
Этот метод и написан как раз для того, чтобы предотвратить такую возможность. См. внимательно исходный код и почитайте немного….
в теории всё вроде понятно и красиво. Попытался запустить под Denver’ом (WinXP) – пока не поставил блокировку на исходный файл на время чтения с него, он периодически обнулялся.
(тут можно вставлять код?)
Здорово! Я уже много раз сталкивался с подобной проблемой и очень рад, что именно Геннадий помог мне во многих вещах основанных на этой фишки. Эта статейка очень полезная для небольших скриптов (типа гостевых, скрипты голосования и тому подобные), поэтому предлагаю добавить больше информации по этой теме. К примеру, как добавить строчку в текстовой базе, как ее удлить или изменить и т.д.
Функция всегда переписывает исходный файл. А как быть, если в исходном файле нужно найти и отредактировать какую-то строку или просто добавить несколько строк к уже имеющимся? Если я буду использовать такой метод при записи сообщений в гостевой книге, как остальные сообщения, поступившие в момент, когда был заблокирован исходный файл, запишутся в него? Надо формировать какую-то очередь? И как это реализовать, какой-то демон должен постоянно проверять, есть ли в очереди записи?
Найти и отредактировать – практически тот же самый алгоритм. Только в цикле надо проверять условие и если запись не та которая нужна, ее переписать как есть. Если же нужная – отредактировать.
Только обратите внимание – все таки это не для больших объемов данных. Пара-тройка тысяч строк в файле по опыту еще тянут, а далее становится некомфортно работать…
Да, по второй части вопроса – ничего не надо делать более. Не надо отслеживать никаких очередей. Как только файл освободится – в него будет записана следующие данные.
Весь смысл в том, чтобы не дать возможности одновременной записи в файл нескольким процессам – ведь это приведет к потере информации.
Я сталивался с такой проблемой пару лет назад, интересная мысль, но как быть если одновременно несколько пользоватлей хотят сделать запись в файл? Тот кто первее блокирует файл и пишет в него, а тот кто не успел? Ждёт? Получает отказ? Предлагаю просто дополнить этот код циклом.
for ($i=1; $i<=10;$i++){
если файл заблокирован, то
sleep(1); //засыпаем на секунду и
повторяем цикл и так 10 секунд, а если доступ есть,
то пишем данные
}
Вы это серьезно что ли? Это решение и предназначено для того, чтобы не потерять информацию при попытке одновременной (близкой по времени) записи в файл несколькими процессами…. Даже если таких процессов будет несколько, запись пройдет в порядке очереди. Ну, естественно на разумные размеры файлов данных – 3-4 МБ вполне тянет…
Кстати, это не теоретические изыскания – подтверждено на практике. Скрипт, в котором использовано это решение работает на тысячах самых разных сайтов. И работает устойчиво.
Т.е. не нужно заботиться об очередях в PHP?.. Где вообще можно почитать о многопоточности в PHP? На сколько я знаю, в Java это нужно обязательно делать, есть специальный метод synchronized()
О многопоточности в РНР почитать нельзя нигде). Он не поддерживает многопоточности, и как вариант обойти этот недостаток и была написана эта статья)
2Gennady: Вы меня не совсем правильно поняли, я понял для чего это решение, просто я впервый раз слышу, что если файл заблокирован, то другие пользователи, желающие сделать в него запись будут допущены в порядке очереди. Поэтому написал цикл в теле которого, мы пытаемся достучаться до файла в течении 10 секунд.
To Андриан:
Я чуть ранее писал:
… не потерять информацию при попытке одновременной (близкой по времени) записи в файл несколькими процессами…
Мы наверное действительно немного друг друга недопонимаем – я веду речь исключительно о предотвращении потери информации… и наверное не очень удачно применил термин очередь…
На практике – получается, что действительно пишет в файл данные от одновременных (близких по времени – единицы-десятки миллисекунд) процессов
без искажений. Но опять же если – если файл относительно небольшой.
А к файлу, который будет лежать на сервере, нужно открывать общий доступ?
Или же скрипт может открыть и файл который не доступен всем?
Если файл создан скриптом (в приведенном фрагменте кода так и получается, ведь там по сути каждый раз новый файл создается), то все работает с правами 0644.
Поясните пожалуйста, если «блокирующий» файл служит своеобразным флагом показывающим, что идет запись в файл и гарантирует работу только одного процесса записи, то почему не рекомендуете записывать напрямую в файл без использования временного, раз второй процесс записи возникнуть уже не может пока не будет снята блокировка «блокирующего» файла? Или это может вызвать те же последствия, что и в случае «блокировал, открыл, записал, закрыл, снял блокировку с файла»? Ведь чтобы дописать сточку к файлу при большом его объеме процесс перезаписи файла во временный может несколько тормозить программу, поэтому целесообразна ли такая перестраховка при наличии «блокирующего» файла.
function paranoid_safe_write($file_path, $data){
$uid = uniqid(rand());
$backup_file_path = APP_DIR.files::BACKUPDIR.$uid;
#путь к бэкапу файла
$temp_file_path = APP_DIR.files::TEMPDIR.$uid;
#путь к временному
$block_file_path = APP_DIR.files::BLOCKDIR.hash(‘md5′, realpath($file_path));
#хеш пути к блок- файлу
if(
$lock = fopen($block_file_path,’a')
#открываем БЛОКИРУЮЩИЙ
and flock($lock, LOCK_EX)
#БЛОКИРУЕМ его
and $tmp=fopen($temp_file_path,»w»)
#открываем ВРЕМЕННЫЙ
and is_string($data)?fputs($tmp, $data):TRUE
#ПИШЕМ туда
and fclose($tmp)
#закрываем
and is_writable($file_path)?rename($file_path, $backup_file_path):TRUE
#делаем БЭКАП существующего
and rename($temp_file_path, $file_path)
#кидаем временный на место существуюющего
and flock($lock, LOCK_UN)
#анлок
and fclose($lock)
and unlink($backup_file_path)
#стираем бэкап
) return TRUE;
if(is_writable($file_path)) unlink($file_path);
if(is_writable($backup_file_path))
rename($backup_file_path, $file_path);
return FALSE;
}
только что состряпал для системных файлов
комменты к верхней строчке
упс, лажанулся в последних четырех строчках
если фэйл будет раньше бэкапа, то мы теряем файл. гг.
в общем идея в том чтобы иметь свои собственные временные и блокирующие для любого файла + бэкап файл который будет возвращаться на место в случае фэйла
Странно как-то : только последняя записанная информация хранится в файле? Удаляем, переименовываем. После чего в файле снова остается только последняя…
ВОПРОС: и чего мы таким способом защищаем???
PS: Это вопрос to Gennady
Сергей, для меня это было очень целесообразно…
Во-первых решение это не для файлов больших размеров (3-5 тысяч строк в файле не более того…)
Во-вторых – это оказался единственный действительно надежный вариант, который работал на любых, даже самых дохлых хостингах (если не обратили внимание – это решение было для одного проекта, причем массового использования).
Собственно я даже не особо обсуждать все это хотел – просто поделился. Нравится – пользуйся, не нравится – не пользуйся
Mel, на самом деле можно делать все что угодно – дописывать, изменять одну или несколько записей, удалять – что в цикле будете делать с информацией, то на выходе и получите…
Вопрос то GENNADY
1. Вы говорите что работает на большом колличестве хостигах, а будет ли работать функция блокирования flock в файловых системах NTFS?
В документации PHP помоему написано про что функция блокирования flock работать небудет. И как тогда быть?
2. Вы говорите что бы текстовый файл был небольших размеров, если больше «становится некомфортно работать…» Поясните пожалуйста что это означает? Будет зависать страница при загрузке или всетаки будут происходить ошибки связанные с записью?
По первому вопросу, не буду обманывать, ничего не скажу, надо смотреть документацию.
По второму: некомфортно – приходится ждать, но это уже было связано с конкретно моей задачей. Приходилось не только отыскать нужную строку, но и разбить ее в массив по разделителю, произвести какие-либо операции, и «положить» на место. Иногда и не с одной строкой, а с группой. Поэтому….
Очень помогли, спасибо!
только как проверить что ввел юзер, ну к примеру в путь к файлу он дописал ../../etc
Сергей, если говорить о примере, то путь к файлу – это константа, которую определяете Вы, а не юзер. Если же говорить вообще – то статья не посвящена вопросам безопасности. Информации по обеспечению безопасности достаточно, нужно только поискать…
flock() не будет работать на NFS и многих других сетевых файловых системах.
В некоторых операционных системах flock() реализован на уровне процессов. При использовании многопоточных серверных API, таких как ISAPI, нельзя полагаться на flock() для защиты ваших файлов от дугих PHP-скриптов, которые работают в параллельном потоке на том же сервере!
flock() не поддерживается на старых файловых системах вроде FAT и его производных, так что всегда будет возвращать FALSE в этом окружении (это особенно касается пользователей Windows 98).
Здорово уметь читать доки и приводить из них цитаты…
Скрипт судя по всему замечательный, но есть одно но.
Ваша логика понятна, сохранить в целости и сохранности данные при одновременном доступе нескольких пользователей, путем сохранения данных во временный файл, блокировки и т.д…
Это на самом деле не ново – некоторые движки баз данных примерно так и поступают, например Access, из-за того что NTFS или FAT не позволяет блокировать файл по сети напрямую, но они нашли оригинальный выход, создали дополнительный файл (назовем его lock файл) НО! ВНИМАНИЕ! они не пишут в этот lock файл данные базы данных, а пишут информацию о блокировке файла БД.
Таким образом программа проверяет lock файл и если там есть какие-либо данные о блокировке другими процессами, то она ждет пока не появится запись о том что основной файл БД свободен для записи. А потом уже процесс пишет в lock файл свои данные, проверяет что все записано успешно, и уже обращается в основному файлу БД.
Кстати, этот метод и позволяет оперировать с файлом БД даже большого размера. Мне сейчас необходимо написание подобного скрипта, но как всегда нехватает времени. Ставлю эту страничку в закладки, интересно как разовьется дальше эта дискуссия.
Возникла необходимость поработать с файлами, и вновь обратился к Вашему методу.
Два вопроса.
1. Вы открываете блокировочный файл PATH_BLOCKFILE до оператора IF (2я строка), а закрываете его внутри условного оператора (12стр). Правильно ли будет вынести закрытие файла за пределы IF?
2. Если добавить последней строкой удаление блокировочного файла
unlink(«PATH_BLOCKFILE»);
- это не помешает надёжности работы скрипта ?
___
Спасибо!
1. Да.
2. Не помешает.
вот так правильно делать запись/чтение в файл, дабы не порушить его
в этом случае доступ контролируется на уровне движка php и блокировок
function read_status() {
flock($f, LOCK_EX);
rewind($f);
$curr = trim(fgets($f));
$full = trim(fgets($f));
flock($f, LOCK_UN);
fclose($f);
return array(
‘curr’ => $curr,
‘full’ => $full,
);
}
function write_status($curr, $full) {
$f = fopen(STATUS_FILE,’a');
flock($f, LOCK_EX);
ftruncate($f, 0);
fputs($f, $curr.»\n»);
fputs($f, $full);
fflush($f);
flock($f, LOCK_UN);
fclose($f);
}