[ Поиск ] - [ Пользователи ] - [ Календарь ]
Полная Версия: Функционал мемкэша на простых файлах
AlexanderC
Кэширование нужная вещь, и очень часто для этого используются такие штуки как APC, xCache, Memcache итд....

Вот написали приложение которое тесно связанно с мемкэшом, а на хостинге нет данного расширения(memcache)- массовость не удалась!
Исходя из этого написал простенький класс повторяющий API расширения Memcache из пыха.

Какие данные можно хранить??? --- все которые могут быть сериализованы !!!

АПИ полностью повторено !!!, т.е. поведение тоже самое...



<?php if(__FILE__ == $_SERVER['SCRIPT_FILENAME']) exit('Are you hacker?');

/**
*
@author AlexanderC
*
@edited 05.08.2011
*
@todo Improve performance
*
@modified
* @note Memcache extension interface implementation based on flat files,
* can be stored any data supported by serialization function [see PHP documentation]
*
* -> Errors are returned by "GetErrors" function (see fn desc)
*
* -> Implemented such function as:
* Flush - clean cache data
* Get - get cached value by key
* Set - store a value in cache(modify if exists)
* Add - store a value in cache
* Delete - delete stored value(can be set an timeout)
* Replace - replace an existing value
* Increment - increment a numeric value
* Decrement - decrement a numeric value
*
*
@example ---------------------------------------------------------
*
* $val1 = 'my value stored here';
* $val2 = array('here some values', 3, null);
*
* $toBeCached = array($val1, $val2); // this will be cached
*
* // store variable in cache for a day
* if ( Filecache::Add('myKey', $toBeCached, true, 60*60*24) )
* exit('The variable was successfully stored in cache');
*
* // if the values already exists
* exit(
* var_export(
* Filecache::Get('myKey')
* )
* );
*
* ------------------------------------------------------------------
*/


final class Filecache
{
/* --- start config --- */

// cache file extension

const CacheExt = 'acc';

// cache directory with slash at the end
const CacheDir = 'temp/';

/* --- end config --- */

// full year in seconds

const SecYear = 31536000;

// data dump
private static $dump;

// errors dump
private static $errDump = array();

/**
* get errors
*
*
@param bool (clean error dump)
*
@return array
*/

public static function GetErrors( $clean = false )
{
$err = self::$errDump;
// clean dump
if ($clean == true)
self::$errDump = array();
return $err;
}

/**
* get current store folder(create if not exists)
*
*
@return string(folder name)/false on error
* Note: the folder is the same as the first character of provided key(to lower case if letter)
*/

private static function getStore( $key )
{
// get first char
$fl = substr($key, 0, 1);
// check if key is alphanumeric
if ( !ctype_alnum($fl) )
{
// create number range
$nr = range( 0, 9 );
// create letter range
$lr = range( 'a', 'z' );
// merge arrays
$r = array_merge( $nr, $lr );
// get random char
$rc = rand( 0, count($r) );
$lr = $r[$rc];
}
// set to lower if letter
if ( preg_match( '/^[a-z]$/i', $fl ) )
$fl = strtolower($fl);

// define required dir
$cd = self::CacheDir.$fl.'/';
// create directory if doesn't exist
if ( !is_dir( $cd ) )
if ( !mkdir($cd, 0755) )
{
self::$errDump[] = 'Failed to create cache directory -> '.$cd;
return false;
}
return $cd;
}


/**
* check if cache isn't expired and write cache to the local $dump var
*
*
@param str, bool
*
@access private
*
@return bool
*/

private static function CheckData( $key, $returnExp = false )
{
// get cache directory
$cd = self::getStore($key);
// on error
if ( !$cd )
return false;

// check if file exists
if ( !file_exists($cd.$key.'.'.self::CacheExt) )
return false;

// unserialize cache file
$data = unserialize( file_get_contents( $cd.$key.'.'.self::CacheExt ) );

// check if lifetime expired
if ( time() > (int)$data['exp'] )
{
// delete old cache
unlink($cd.$key.'.'.self::CacheExt);
return false;
}

// if must be written to local dump var
if ( !$returnExp )
{
// check if data was compressed
if ( (bool)$data['gz'] == true )
{
// uncompress data than unserialize
$data['data'] = gzuncompress($data['data']);
self::$dump = unserialize($data['data']);
}
// esleif not compresses
elseif( (bool) $data['gz'] == false )
self::$dump = $data['data'];
else
{
// on error/wrong file format/broken data delete cache file
unlink($cd.$key.'.'.self::CacheExt);
return false;
}
}

// if data must be returned (increment, decrement...)
elseif ( $returnExp == true )
{
// check if data was compressed
if ( (bool)$data['gz'] == true )
{
// uncompress than unserialize data
$data['data'] = gzuncompress($data['data']);
return array(unserialize($data['data']), $data['exp']);
}
// if no compression
elseif( (bool)$data['gz'] == false )
return array($data['data'], $data['exp']);
// on error/wrong file format/broken data delete cache file
else
{
unlink($cd.$key.'.'.self::CacheExt);
return false;
}
}

return true;
}


/**
* compress data after check
*
@param mixed
*
@return mixed
*/

private static function Compress($var)
{
// check if is required caching with compression,
// and don't cache if unnecessary,to exclude overload

if ( !is_bool($var) && !is_int($var) && !is_float($var) )
if ( function_exists('gzcompress') )
{
// prevent notice if array as param
$var = serialize($var);
return gzcompress( $var, 9); // o_O
}
return $var;
}

/**
* Clear the cache
*
*
@return bool
*/

public static function Flush()
{
// open directory
$dir = opendir(self::CacheDir);
if ( !$dir )
{
self::$errDump[] = 'Can\'t open cache directory '.self::CacheDir;
return false;
}

// loop throuth cache dirs
while ( false !== ($file = readdir($dir)) )
{
// get dirs
if ( $file != '.' && $file != '..' && is_dir(self::CacheDir.$file) )
{
// check if they are cache dirs
if ( preg_match('/^[a-z0-9]{1,1}$/i', $file) )
{
// open child dir
$dh = opendir( self::CacheDir.$file.'/' );
if ( !$dh )
{
self::$errDump[] = 'Can\'t open cache directory '.self::CacheDir.$file;
continue;
}
// delete cache files
while( false !== ($cf = readdir($dh)) )
if ( $cf != '.' && $cf != '..' )
unlink( self::CacheDir.$file.'/'.$cf );

closedir($dh);
// delete directory
rmdir(self::CacheDir.$file.'/');
}
}
}

return true;
}

/**
* Returns the value by it's key
*
*
@param string $key
*
@return mix/false
*/

public static function Get( $key )
{
// check if array of keys required
if ( is_array($key) )
{
$temp = array();
foreach ( $key as &$k )
{
// get each key ant put it into temp var/false if not found or error
if ( !self::Get($k) )
$temp[] = false;
else
$temp[] = self::$dump;
}
return $temp;
}
// get cache (f*kin exclamation, 20 min of debugging)
if ( !self::CheckData( $key ) )
return false;

return self::$dump;
}

/**
* Store the value (overwrite if key exists)
*
*
@param string $key
*
@param mix $var
*
@param bool $compress
*
@param int $expire (seconds before item expires)
*
@return bool
*/

public static function Set( $key, $var, $compress = false, $expire = 0 )
{
// get cache directory
$cd = self::getStore($key);
// on error
if ( !$cd )
return false;

// if expire is not set- set it for an year, must be enought
if ( $expire == 0 )
$expire = self::SecYear;

// calculate expire time
$expire = time() + (int) $expire;

// compres if required
if ( $compress != false )
$var = self::Compress($var);

// create cache file content
$data = serialize( array('exp' => $expire, 'data' => $var, 'gz' => ($compress==0?0:1) ) );
// try to write to file
if ( !file_put_contents( $cd.$key.'.'.self::CacheExt, $data, LOCK_EX | LOCK_NB ) )
return false;
return true;
}

/**
* Set the value, if the value does not exist; returns FALSE if value exists
*
*
@param sting $key
*
@param mix $var
*
@param bool $compress
*
@param int $expire
*
@return bool
*/

public static function Add( $key, $var, $compress = false, $expire = 0 )
{
// get cache directory
$cd = self::getStore($key);
// on error
if ( !$cd )
return false;

// check if file exists
if ( file_exists($cd.$key.'.'.self::CacheExt) )
return false; // that's all magic between Set and Add methods

// if expire is not set- set it for an year, must be enought

if ( $expire == 0 )
$expire = self::SecYear;

// calculate expire time
$expire = time() + (int) $expire;

// compres if required
if ( $compress != false )
$var = self::Compress($var);

// create cache file content
$data = serialize( array('exp' => $expire, 'data' => $var, 'gz' => ($compress==0?0:1) ) );
// try to write to file
if ( !file_put_contents( $cd.$key.'.'.self::CacheExt, $data, LOCK_EX | LOCK_NB ) )
return false;
return true;
}

/**
* Replace an existing value
*
*
@param string $key
*
@param mix $var
*
@param bool $compress
*
@param int $expire
*
@return bool
*/

public static function Replace( $key, $var, $compress = false, $expire=0 )
{
// get cache directory
$cd = self::getStore($key);
// on error
if ( !$cd )
return false;

// check if file exists
if ( file_exists($cd.$key.self::CacheExt) )
{
// if expire is not set- set it for an year, must be enought
if ( $expire == 0 )
$expire = self::SecYear;

// calculate expire time
$expire = time() + (int) $expire;

// compres if required
if ( $compress != false )
$var = self::Compress($var);

// create cache file content
$data = serialize( array('exp' => $expire, 'data' => $var, 'gz' => ($compress==0?0:1) ) );
// try to write to file
if ( !file_put_contents( $cd.$key.'.'.self::CacheExt, $data, LOCK_EX | LOCK_NB ) )
return false;
return true;
}
// if not exists
else
return
false;
}

/**
* Delete a record or set a timeout
*
*
@param string $key
*
@param int $timeout
*
@return bool
*/

public static function Delete( $key, $timeout = 0 )
{
// get cache directory
$cd = self::getStore($key);
// on error
if ( !$cd )
return false;

// check if file exists
if ( !file_exists($cd.$key.'.'.self::CacheExt) )
return true;

// try to delete file
if ( (int)$timeout == 0 )
{
if ( unlink($cd.$key.'.'.self::CacheExt) )
return true;
else
return
false;
}
// set timeout for deleting (expiration time)
elseif ( $timeout != 0 )
{
// get contents of cache file
$data = unserialize( file_get_contents($cd.$key.'.'.self::CacheExt) );
// set timeout
$data['exp'] = time() + (int) $timeout;
// try to put contents back
if ( file_put_contents($cd.$key.'.'.self::CacheExt, serialize($data), LOCK_EX ) )
return true;
}
return false;
}

/**
* Increment an existing integer value
*
*
@param string $key
*
@param mix $value
*
@return bool
*/

public static function Increment( $key, $value = 1 )
{
// get cache directory
$cd = self::getStore($key);
// on error
if ( !$cd )
return false;

// check if file exists
if ( !file_exists($cd.$key.'.'.self::CacheExt) )
return false;
// get cached data
$data = self::CheckData( $key, true );
// if failed or unexpected format
if ( !$data || !is_array($data) )
return false;
// if value is not numeric set it to 1
if ( !is_numeric($value) )
$value = 1;
// set increment counter
$counter = is_numeric($data[0])?(int)$data[0] + $value:$value;
// create cache file contents
$data = serialize( array('exp' => $data[1], 'data' => $counter, 'gz' => 0) );
// try to write to file
if ( file_put_contents($cd.$key.'.'.self::CacheExt, $data , LOCK_EX | LOCK_NB) )
return true;
return false;
}

/**
* Decrement an existing value
*
*
@param string $key
*
@param mix $value
*
@return bool
*/

public static function Decrement( $key, $value = 1 )
{
// get cache directory
$cd = self::getStore($key);
// on error
if ( !$cd )
return false;
// check if file exists
if ( !file_exists($cd.$key.'.'.self::CacheExt) )
return false;
// get cached data
$data = self::CheckData( $key, true );
// if failed or unexpected format
if ( !$data || !is_array($data) )
return false;
// if value is not numeric set it to 1
if ( !is_numeric($value) )
$value = 1;
// set increment counter
$counter = is_numeric($data[0])?(int)$data[0] - $value:0;
// create cache file contents
$data = serialize( array('exp' => $data[1], 'data' => $counter, 'gz' => 0) );
// try to write to file
if ( file_put_contents($cd.$key.'.'.self::CacheExt, $data , LOCK_EX | LOCK_NB) )
return true;
return false;
}
}

?>




Спустя 1 минута, 38 секунд (4.08.2011 - 17:45) AlexanderC написал(а):
П.С. замечания и дополнения приветствуются !!!!!

...
перевёл камменты к классу по пожеланиям форумчан

Спустя 2 часа, 32 минуты, 17 секунд (4.08.2011 - 20:18) ИНСИ написал(а):
AlexanderC Буду тестить, чуть позже как освобожусь. А пока тему закрепил.

Спустя 5 минут, 22 секунды (4.08.2011 - 20:23) AlexanderC написал(а):
Цитата (INSIDIOUS @ 4.08.2011 - 17:18)
AlexanderC Буду тестить, чуть позже как освобожусь. А пока тему закрепил.

СПС, вроде люди сказали что достаточно шустро работает... проверял в реале, вроде ничё так... Мемкэш конечно быстрее ;D, но данные легко теряются )

Спустя 1 час, 12 минут, 48 секунд (4.08.2011 - 21:36) Семён написал(а):
AlexanderC
Файловый кеш принято тестировать на tmpfs, проверь сильно удивишься )))

Спустя 1 час, 12 минут, 50 секунд (4.08.2011 - 22:49) AlexanderC написал(а):
Удивлюсь то что он быстрее... да, он быстрее, ибо данный класс записывает в файлы, а Tmpfs в виртуальную память... или мне придумывать велосипед? для этого мемкэш(правда в RAM держит), а если нет, то без редактирования кода можно использовать данный класс... от безисходности!!! и так далее...

А для хайлод проектов я и не делал данное дело, там всё должно работать как часы, и его не зальют на дешёвый хост!!!(потому как нужны выделенные сервера... чё я буду объяснять то, сами знаете)

Спустя 10 часов, 33 минуты, 7 секунд (5.08.2011 - 09:22) Семён написал(а):
AlexanderC
tmpfs это лишь файловая система расположенная в ОЗУ вместо ПЗУ.
на highload-e можно свободно использовать вместо memcache.

Спустя 48 минут, 52 секунды (5.08.2011 - 10:11) linker написал(а):
Семён
smile.gif дисковые накопители, а также флэхи и т.п. не принято называть ПЗУ - Постоянное Запоминающее Устройство (то, на которое один раз записал и всё, CD-R диски можно назвать ПЗУ). Но это так, чтобы кто-нибудь не попутал чего.

Спустя 37 минут, 58 секунд (5.08.2011 - 10:49) Семён написал(а):
linker
Согласен) спс. за уточнение

Спустя 49 минут, 5 секунд (5.08.2011 - 11:38) killer8080 написал(а):
Цитата (linker @ 5.08.2011 - 10:11)
CD-R диски можно назвать ПЗУ

Если уж придираться к буквам, то CD-R - это скорее ППЗУ smile.gif

Спустя 57 минут, 4 секунды (5.08.2011 - 12:35) AlexanderC написал(а):
Ага, можно, приведите пример ресурса реального, который изпользует данную файловую систему ВМЕСТО МЕМКЭША, или других из этой серии?

я ещё раз повторюсь- этот класс для тех МАССОВЫХ И НЕ СИЛЬНО НАГРУЖЕННЫХ приложений которые были разработаны с использованием мемкэша, но по каким-то причинам нет там такого(даже расширения...).

ДЛЯ МАССОВОСТИ.... +++ это скорее концепт, хотя спокойно можно использовать для хостов с урезанным пыхом и без баз данных...
Я не думаю что стоит спорит с пеной у рта... я знаю что такое Tmpfs, и что изначально создавалось для хранения временных файлов...

ИМХО- мемкэш более "взрослый" и масштабируемые

Спустя 6 минут, 41 секунда (5.08.2011 - 12:42) linker написал(а):
AlexanderC
Да никто не спорит. Будет время посмотрю класс.

Спустя 16 минут, 21 секунда (5.08.2011 - 12:58) Семён написал(а):
AlexanderC
iccup.com

Спустя 31 секунда (5.08.2011 - 12:58) AlexanderC написал(а):
Цитата (Семён @ 4.08.2011 - 18:36)
AlexanderC
Файловый кеш принято тестировать на tmpfs, проверь сильно удивишься )))

Просто посчитал данное высказывание неуместным... мне бы норм критику и улучшения кода...

Спустя 4 минуты, 37 секунд (5.08.2011 - 13:03) AlexanderC написал(а):
Верю- если один сервер под это дело...
+ конечно шустро статику отдаёт... видно позитивное влияние Ngix-а )))

Спустя 1 минута, 20 секунд (5.08.2011 - 13:04) Семён написал(а):
AlexanderC
Всё ну тебя нафиг, прочитай что такое tmpfs, что такое ОЗУ, почему принято использовать memcache, узнай в чём отличие memcache от tmpfs.
tmpfs это не отдельный инструмент, это файловая система расположенная в ОЗУ!!!!!!!!! туда ты блин свой долбанный кеш будешь класть!!!!!!!!!!! mad.gif mad.gif mad.gif

Спустя 26 минут, 32 секунды (5.08.2011 - 13:31) linker написал(а):
Для не нагруженного сойдут и файлы на обычной ФС, чего вы так ругаетесь.

Спустя 8 минут, 31 секунда (5.08.2011 - 13:39) AlexanderC написал(а):
)))) так и да, видно что не понял меня)))) я не ссорится пришёл, а продуктивную критику слушать ;D
... я и не мог понять при чём здесь tmpfs был... если просто так сказано, то извиняйте....

Спустя 2 минуты, 30 секунд (5.08.2011 - 13:42) AlexanderC написал(а):
П.С. а кто хочет
mount -t tmpfs -o size=500M,mode=0755 tmpfs /var/www/www.forSemen.ua/tmp
пожалуйста... обычные юзверя не будут заморачиваться ;D

Спустя 1 час, 36 минут, 44 секунды (5.08.2011 - 15:19) twin написал(а):
AlexanderC
Цитата
я не ссорится пришёл, а продуктивную критику слушать ;D


Объем проведенной работы впечатляет. И уровень исполнения тоже.

Но алгоритм мне не очень понравился. Зачем столько обращений к ФС? Есть же более щадящие способы генерации уникальных имен, нежели постоянные проверки наличия файлов и директорий.

Сама идея хранения данных в сериализованном виде чет не радует. Это достаточно затратные операции - (де)сериализация.

Имена файлам дает ужасно кривые. Если ключ - кирилица.

Есть что пооптимизировать. Это место допустим навскидку.
      // проходимся циклом по файлам
while ( false !== ($file = readdir($dir)) )
{
// только если директория идём дальше
if ( $file != '.' && $file != '..' && is_dir(self::CacheDir.$file) )
{
// проверяем название чтобы не позволять лишних циклов
if ( preg_match('/^[a-z0-9]{1,1}$/i', $file) )
{
// открываем директорию
$dh = opendir( self::CacheDir.$file.'/' );
if ( !$dh )
{
self::$errDump[] = 'Не могу открыть директорию '.self::CacheDir.$file;
continue;
}
// удаляем файлы
while( false !== ($cf = readdir($dh)) )
if ( $cf != '.' && $cf != '..' )
unlink( self::CacheDir.$file.'/'.$cf );


Посмотри в сторону scandir() хотя бы, а лучше glob()


Или вот еще:
     if ( !is_bool($var) && !is_int($var) && !is_float($var) )
if ( $compress != false )
if ( function_exists('gzcompress') )
{
// сериализуем перед сжатием, чтоб не получить ошибку
$var = serialize($var);
$var = gzcompress( $var, 9); // o_O
}

Код повторяется в трех методах без изменений. Ну дык и вынуть его отдельным методом.

Вобщем и целом - респект. Но еще работать и работать.
А нишу свою класс найдет.

Спустя 1 час, 35 минут, 57 секунд (5.08.2011 - 16:55) AlexanderC написал(а):
Да, повторения нужно будет вывести... но эт как-бы последнее...

В начале хотелось получить сравнительно норм. быстродействие....


Don't use glob() if you try to list files in a directory where very much files are stored (>100.000). You get an "Allowed memory size of XYZ bytes exhausted ..." error.
You may try to increase the memory_limit variable in php.ini. Mine has 128MB set and the script will still reach this limit while glob()ing over 500.000 files.
-- это меня остановило от глоб...

прег_матч наверное нужно убрать из флуш()

Спустя 6 минут, 21 секунда (5.08.2011 - 17:01) AlexanderC написал(а):
Очень конечно интересно найти лучший формат для хранения нежели сериализация... плюсом в ней то что можно разные типы данных хранить...

Проверял- быстрее json_encode или допустим var_export...

Насчёт имён, то я оставил это на совести вебмастера, т.к. ключи норм. хранить на латинском, решил не делать лишних операций... Ключи не проверяются вообще...

И интересно узнать более интересный алгоритм по распределению по папкам...
по сути тут папки будут проименованы только: a-z и 0-9...
Кто может подсказать более удобный(и так чтоб не было слишком много папок и файлов в них)?

Спустя 19 минут, 13 секунд (5.08.2011 - 17:20) AlexanderC написал(а):
Загвоздка в данном случае что при получения файла нужно получать дату(таймаоут) и состояние данных(сжаты или нет)
Быстрый ответ:

 Графические смайлики |  Показывать подпись
Здесь расположена полная версия этой страницы.
Invision Power Board © 2001-2024 Invision Power Services, Inc.