Contenido

Cacheando contenido con PHP

1 sep

+ 32

Hace unas semanas tuve que implementar una Class para cachear elementos de mi PHP en ficheros estáticos. Inicialmente usé un sistema similar al que había usado siempre basado en la función filemtime() que me devolvía la fecha de modificación del archivo estático y la contrastaba con la actual, si el resultado era mayor a un número de segundos especificado volvía a generar el fichero estático.

Código filemtime()

class Cache {
 function fileName($key){
 return 'cache/'.md5($key);
 }

 function put($key, $datos){
 $f = fopen($this->fileName($key), 'w');
 if (!$f) die('No se puede leer el fichero de caché');

 $data = serialize($data);

 if (fwrite($f,$data)===false) {
 die('No se puede escribir el fichero de caché');
 }
 fclose($f);
 }

 function get($key){
 $filename = $this->fileName($key);
 if (!file_exists($filename) || !is_readable($filename)) return false;
 if ((filemtime($filename) + 3600) < time()) {
 return file_get_contents($filename);
 }
 return false;
 }
}

Esta versión, me permite cachear en fichero cualquier contenido que yo le indique mediante put(). Veamos un ejemplo.

$cache = new Cache();
if (!$data = $cache->get('misdatos')) {
 ....
 $cache->put('misdatos', $misdatos);
}

Problemas

Tras unas horas usándolo me encontré con un problema, la poca flexibilidad de este sistema. Necesitaba que ciertos datos, se cachearan una hora y otros lo hicieran 24. De esta forma me era dificil montar un sistema basado en filemtime() que me permitiera indicar una caché para tiempos diferentes.

Las soluciones pasaba por comprobar el tipo de contenido y dependiendo de que fuera tomar un tiempo u otro, o crear métodos específicos para cachear por tiempos (put60(), put24(),…). Ninguna solución me parecía elegante.

Serialize()

Entonce recordé un artículo de hace ya unos años en los que mediante el uso de serialize() permitía hacer lo que estaba buscando.

serialize() es una función de PHP que permite convertir un objeto que puede ser fácilmente almacenable. Algo que me venía ideal para hacer que mi caché de ficheros estática fuera más flexible.

Código serialize()

>class Cache {
 var $cacheDir;

 function __construct($cacheDir = './'){
 $this->cacheDir = $cacheDir;
 }

 // Indicamos el directorio donde queremos alojar los ficheros de caché
 function put( $key , $data ,$time = 600) {

 $h = fopen( $this->getFileName( $key ) , 'w' );
 if (!$h) throw new Error('No se puede leer el fichero de caché');

 $data = serialize( array( time() + $time , $data ) );

 if ( fwrite( $h , $data ) === false ) {
 throw new Error('No se puede escribir el fichero de caché');
 }
 fclose($h);

 }

 // Obtenemos el nombre del fichero codificado
 private function getFileName($key) {
 return  $this->cacheDir.md5($key);
 }

 // Recuperamos de caché
 function get($key) {

 $filename = $this->getFileName($key);
 if (!file_exists($filename) || !is_readable($filename)) return false;

 $data = file_get_contents($filename);

 $data = @unserialize($data);
 if (!$data) {
 unlink($filename);
 return false;
 }
 if (time() > $data[0]) {
 unlink($filename);
 return false;
 }
 return $data[1];
 }
}

Esta clase ya me permite especificiar de una forma cómoda el tiempo que quiero que se cachee un contenido determinado.

$cache = new Cache('cache/');
if (!$data = $cache->get('misdatos')) {
 ....
 $cache->put('misdatos', $misdatos, 60); // 60 seg
}

Rendimiento

Este sistema es muy cómodo, pero hay que ser conscientes de los problemas de rendimiento que presenta. Hemos de pensar que cada comprobación pasa por leer el contenido del fichero, convertir el objeto serializado y comprobar la fecha de este último.

Esto, frente a la simple consulta de la última fecha de modificación hace que este método sea considerablemente más lento. Pero por contra es bastante más flexible.

Mejoras

Bueno, despues de todo el rollo os propongo un juego. ¿Como lo podemos mejorar?

Mi opción

La opción que estaba barajando era la de generar un fichero con la fecha de caducidad del fichero y el nombre del fichero. De esta forma, únicamente tendría que hacer una lectura de fichero para cargar la fecha de caducidad asociada al fichero y despues mediante filemtime() comprobar si esta es superior a la especificada.

Ejemplo

<?php
...

// Leer
$content = file_get_content($file);
$times = unserialize($content);

 if (filemtime($cache_file) > $times[$cache_file]) {
 ....
 // Generamos de nuevo la caché
 $times[$cache_file] = time() + 60; // 60 seg.
 }

// Grabar
serialize(array(
 md5("tmp".time()).".cache" => '123456789'
 ....
));

...
?>

¿Alguna idea más?

  • yo propongo guardar el archivo estatico con la siguiente estructura

    time() + $tiempoensegundosDEcaducidad . “-” . md5(,,,

    nos quedamos con la parte numerica con la funcion implode, y luego comparamos la el time() actual con el implode[0]

  • Casualmente estuve investigando técnicas de cacheo porque en uno de mis sitios (ConEfecto.com) era necesario para evitar hits al servidor de base de datos para consultas cuyo resultado no cambiaba en (por poner un tiempo) 20 minutos. El sitio está desarrollado con Java, y hay varias opciones de caché similares a las que planteas en tu post, pero al momento de probarlo vi que el servidor de aplicaciones siempre recibe un hit y tiene que verificar si el dato existe en la cache, si hay que renovarlo, etc.

    Por eso me decidí poner un servidor proxy cache que resuelva esa situación, y dejar lo del cacheo fuera de la lógica de tu negocio. En el proxy caché puedes definir las peticiones que van a cachearse, por cuánto tiempo, etc.

    En mi caso utilicé squid como proxy.

    Saludos.

  • Yo hago exactamente lo mismo que el caso 1, sólo un pelín de nada más elaborado, por eso no entiendo del todo el problema del primer método, simplemente pasa el 3600 como parámetros y $cache->put('misdatos', $misdatos, 60); o $cache->put('misdatos', $misdatos, 600); según quieras menos o más caducidad.

    Si quieres datos en crudo, como textos, etc..

     $cache->put('misdatos', "All my loving", 60);
    

    Si quieres guardar un array por dos minutos…

    $arr=Array("1","otro");
    $cache->put('miarray', serialize($arr), 120);
    

    And that’s all, no ? :D

  • Comentar de paso que esto está bien para un servidor compartido / hosting “pobre”, si tienes una máquina dedicada, Zend tiene un buen cacheador de objetos que hace este trabajo por ti, y ya como solución “ideal”, memcached (danga.com) al rescate ! :D

    Si nos sirve generar estático total (pagina completa) en lugar de sólo parte estáticas con otras dinámicas, la opción ideal para mi es un Squid/nginx/Apache con mod_proxy , aunque unas reglas htaccess + un poco de PHP pueden hacer un trabajo MUY bueno, casi igualando un proxy incluso en hosting compartido (sobre eso tengo pendiente un post yo mismo, de estos eternos xE )

  • @3 pero el que se muestra tiene problemas de perfomance

    bueno se me olvido añadir (aunque sea obvio) que para hacer las peticiones usar expresiones regulares, es decir, la primera parte [0-9], el guion y luego el hash md5 del fichero.

  • no entendi bien el tema … uds quieren guardar un cierto contenido estatico q se pasa como variable en un archivo estatico un X tiempo ?
    o quieren cachear todo lo q sucede en el php ?

  • Imagino que tendrás motivos de peso para utilizar ficheros como backend de la cache. Ni que decir tiene que utilizando Memcached o APC obtendrías resultados mucho mejores.

    La única vez que he probado a hacer una cache en ficheros en un sitio con bastante carga y millones de páginas a almacenar en cache, el resultado era peor que sin utilizar cache (llegaba a colapsarse por sobrecarga de accesos a disco).

    Lo único que se me ocurre sería definir unos tipos de cache con una longevidad fija, incluirlos en la clave que utilizas para identificar la cache e interpretarlos al leer la cache contra la fecha de creación del fichero.

    Dicho de forma más clara…
    Definimos diferentes tiempos de cache…

    $cacheTimes = array(
    	'low'=>600,
    	'med',1200,
    	'hig',1800
    );

    Cuando vamos a generar la clave para guardar la cache utilizamos estos tipos como prefijo.

    $cacheKey = 'low' . md5($fileName);
    $cache->save($cacheKey,$data);

    En el load de $cache la comparación sería la siguiente…

    $timeType = substr($key,3);
    if (filemtime($cache_file)+$cacheTimes[$timeType] > time()) 

    Reconozco que es bastante “lioso” y se pierde flexibilidad, pero nos cargamos un serialize y un unserialize (que son lentisimas) y reducimos a la mitad los accesos al disco.

  • Personamente uso memcached para estas cosas. Es facil de instalar y de usar (nunca me ha dado ni medio problema). Implementa TTL (time to live) de una foma muy fácil es muy rápido y escala muy bien. En definitiva una maravilla.

    La puestos a rizar el rizo te recominedo que, si usas linux, los archivos de cache los tengas en la particion tmpfs. De esta forma sin hacer nada tienes implementado un cache en RAM (con unas velocidades de lectura y escritura mucho mejores)

    Y ya de paso (esto ya son manías personales mias, lo reconozco) usar un singleton en la clase para ahorrarte instaciar la clase de forma explícita:

    Cache::singleton()->get($key);
    Cache::singleton()->put($key, $content, $ttl);
    

    En lugar de

    $cache = new Cache();
    $cache->get($key);
    $cache->put($key, $content, $ttl);
  • Hola, una posible solución, que aunque más compleja a mi me parece bastante potente es usar un paquete ya conocido de caché (yo uso Cache_Lite: http://pear.php.net/package/Cache_Lite) y crear una clase que se encargue de la gestión y puedas diferenciar todos los tipos de caché que quieras.

    Nosotros en el curro (biko2.com) lo usamos en nuestro CMS ya que diferenciamos entre distintos tipos de elementos a cachear, cada uno con su propio tiempo.

    El uso de Cache_Lite es bastante sencillo (un par de ejemplos rápidos: http://pear.php.net/manual/en/package.caching.cache-lite.intro.php) y lo que haría esta nueva clase que propongo es establecer los distintos tipos que necesitas, usando serialize() cuando te hace falta

    Ventajas:
    – Coges todas las ventajas y lógica de Cache_Lite, incluido el borrado de elementos “viejos”, hashed de ficheros y de niveles de directorios, etc
    – Organización de la cache y escalado fácil si necesitas nuevas cachés (por tiempo o por tipo)

    Inconvenientes:
    – Es más complicado de montar

    Es un tema que se le puede dar muchas vueltas, es todo un mundo. Yo partiría de algo conocido y probado como Cache_Lite y no reinventar la rueda.

  • Sigo sin entender, lo juro, el problema del time() al gusto, yo lo veo asi (pseudocódigo)

    $datos=Cache->get($clave,$frescura_en_segundos);
    // En este paso si el fichero correspondiente a $clave es más antiguo de $segundos, devuelve false con una simple comprobación en disco, si no devuelve su contenido.
    
    if ($datos) { 
      echo $datos; // Por ejemplo
    }
    else
    {
      $datos=funcion_que_regenera_esos_datos();
      Cache->put($clave,$datos);
      echo $datos
    }

    ¿ Cual es el problema para fijar el número de segundos que queramos en cada caso ? Creo que estais (por favor corregidme si me euivoco) intentando guardar la caducidad en disco, y eso os complica mucho, cuando por lo general la caducidad se debe contemplar en la propia lógica de la aplicación, porque será diferente para unos cachés u otros, no ?

    Obviamente, como decía antes y recalca Borja, esto es trabajo para APC o memcached, de libro, pero incluso en el caso de estar en un hosting compartido donde no te lo puedas permitir, sigo sin verle el fallo a este tipo de uso; de hecho yo mismo tengo una web antigüa hecha exactamente asi, que en su dia estaba en un hosting compartido, nunca se ha actualizado, y con una clase de 12 lineas sirve partes statics con 10000 visitas únicas al dia, sin inmutarse.

    Obviamente, para usos mayores en visitas, millones como dice Borja, o incluso unas cuantas decenas de miles, el coste de acceso a disco hace esta solución inútil, y hay que tirar por algo más estilo “ramdisk” (tmpfs) o memcached.

    Just my 2 cents (de Euro, eso si) :)

  • Una pregunta de novato: ¿Puede memcached almacenar “ficheros”, CSS por ejemplo?

    Sobre la solución: ¿Sabes de antemano qué ficheros quieres para cada duración? ¿Porque no creas un subdirectorio del directorio cache para cada duración? cache/60 cache/180 cache/86400 Claro que eso solo sirve si sabes que los CSS (por ejemplo) tienen una cache de un día, los XML de 1 minuto, etc..

  • Jose Alberto, memcached almacena “objetos”, sean Textos, variables, arrays, imágenes en blob, CSS… cualquier cosa. De todas formas, no se me ocurre una utilidad para el CSS, la verdad, que debería servirse siempre que sea posible como estático, no desde php (salvo rarísimas necesidades, vamos) :D

  • mi propuesta sería algo como el objeto application del ASP, tengo un código hecho, se podría modificar para generar un fichero por cada variable que guardas y quizás como dicen mas arriba guardar el tiempo de caducidad en el nombre de fichero. Ejemplo: 3600–nombrefichero

    El código que comentaba antes: http://www.deambulando.com/2009/04/08/simple-cache-para-php/

  • No has probado con Zend_Cache de Zend Framework, tambien funciona perfectamente fuera del framework.
    Ya lo he utilizado en un par de proyectos y anda de maravillas.

  • @MarcosBL: El problema está… ¿donde guardo ese parámetro? Si se lo paso, perfecto, pero la función filemtime() usa la fecha del sistema para comprobar la última modificación.

    No puedo hacer nada con ese parámetro :(
    @MarcosBL: Si, la tecnología no me preocupa es más la metodología de uso.
    @anonimo: Sería una buena opción, fácil y cómoda. Me gusta :D
    @Borja: Está claro que con Memcached o APC, obtendría mejores resultados. Pero simplemente son recipientes en donde alojar los datos cacheados.

    Al fin y al cabo, viene a ser lo mismo cambiando la tecnología donde almacenamos los datos. Mi idea es dar con una metodología optima, independientemente de la tecnología.
    @MarcosBL: Ahora me va gustando más. Y esto implica un cambio de concepto, no guardar cuando va a caducar, sinó obtener los datos y pasar por parámetro el tiempo que deberían estar vivos.

    Fijate en el código que has puesto con el que hay en primer comentario tuyo. Son diferentes, y esta solución me gusta mucho :D
    @Jose Alberto: No está nada mal, me gusta hacer directorios y más directorios. Así que esta me encanta :D
    @daniel: No, me lo apunto :D
    @chema: Este ejemplo viene a ser como el segundo código del post. Y me encuentro con el mismo problema, tener que abrir y leer cada fichero.

  • @Tatai: Uff, esto es bueno… eso si, quizás voy a matar moscas a cañonazos :D

  • @aNieto2k: quizás no me expliqué bien, la idea luego es mirar en el directorio todos los que estén caducados y borrarlos o regenerarlos según. Lo del acceso a disco claro de alguna manera tendrás que almacenar el dato.

    Otra opción que comentan arriba a es que cuando guardas la variable, le puedas pasar cuando caduca está claro que cargas el contenido :S

  • Particularmente, soy un “enamorado” del framework Symfony.. Que tiene la grandísima ventaja de que es totalmente desacoplado (vamos, que puedes usar las clases/paquetes que necesites) gracias a que los request se atienden mediante una cadena de filtros y eventos.

    En concreto, para el tema del caché, tiene unas cuántas clases:

    http://www.symfony-project.org/api/1_2/cache

    Aparecen, como veis, varias.. En función de cómo queráis gestionar la caché (APC, memcached, ficheros,….), en un fichero dónde se definen qué clase ha de utilizarse para cada uno de los “filtros” -el de caché es uno de los filtros estándar que se ejecutan, aunque puede deshabilitarse)

    Como veis, hay una clase abstracta, sfCache, de la cual extienden cada una de las posibles implementaciones de cacheo..

    Yo no seguiría planteándome nada, sin echarle un _buen_ vistazo a cómo se ha implementado en Symfony.

    http://www.symfony-project.org/api/1_2/sfCache
    http://www.symfony-project.org/api/1_2/sfFileCache
    http://trac.symfony-project.org/browser/branches/1.2/lib/cache/sfCache.class.php
    http://trac.symfony-project.org/browser/branches/1.2/lib/cache/sfFileCache.class.php

  • Te recomiendo que utilices Zend_Cache (http://framework.zend.com/manual/en/zend.cache.html), es básicamente una interfaz de caché que a través de métodos get y put simples te permite guardar en muchos backends diferentes (ficheros, memcached, APC, etc). Zend_Cache ya gestiona los TTL de la caché, los filelocks y más cosas por lo que no te lo tendrás que currar tu desde cero

  • @MarcosBL Gracias por la respuesta. Hago cache de CSS porque para cada usuario es diferente, la pueden personalizar. Aunque realmente no es una cache al uso, se genera un CSS cada vez que el usuario guarda su configuración :D Estoy estudiando la posibilidad de utilizar caché para otras cosas.

    @aNieto2k Me alegra que te guste mi propuesta

  • @Jose Alberto: Jose Alberto, entiendo la necesidad en tu caso; de todas formas casi que sigo pensando que un fichero estático /usercss/iduser/style.css que actualizes en cada miodificación por parte del usuario sería mejor idea, ya que seguirías sirviendo estáticos, que siempre es infinitamente más ligero que lanzar un proceso PHP por petición CSS :D

  • se vale montar esos archivos sobre una particion RAM ?

  • Renato Amaya mira el comentario de ‘gonzalo’ que me ha parecido muy interesante :)

  • @Jose Alberto quizá CssDispatcher te sea útil. La salida se puede cachear con cualquier sistema de caché, preferentemente memcache o mod_mem_cache (ojo, son cosas distintas).

  • @Israel Viana: Muchas gracias Israel, voy corriendo a echar un vistazo.

  • Hola

    Yo he utilizado la librería de cachés de Symfony, porque tal y como dice el compañero, está desacoplada y va de maravilla.
    La verdad es que la solución que aporta Andrés es muy… como diría, burda, y me temo que acarrea problemas de rendimiento.

    El tema de las cachés es delicado y si lo haces más vale dejarlo en manos de una librería especializada, ya sea Symfony, Memcache, Zend, APD o la que sea, pero que esté probada y hecha a conciencia para su cometida.

    Librerías caché de una clase aún no las he conocido ;·)

  • Supongo que si en la solución que usa anieto en el post los ficheros se guardasen en tmpfs que ya mencionaron arriba no habría muchos problemas de rendimiento, no? Porque se estaría accediendo directamente a memoria y nunca a disco?

  • Pero en un host compartido, ¿vas a tener acceso a la carpeta tmpfs de linux?
    @kassu: Pero en un host compartido, ¿vas a tener acceso a la carpeta tmpfs de linux?

  • @David Vale, en un host compartido está claro que no, pero me refería en el caso de que fuese posible usar tmpfs.

  • Incluso de podría meter toda la aplicacion en una carpeta de tipo tmpfs y tenerlo sincronizado a una carpeta fisica del sistema con rsync, o incluso sincronizado con otra máquina :)

Comentar

#

Me reservo el derecho de eliminar y/o modificar los comentarios que contengan lenguaje inapropiado, spam u otras conductas no apropiadas en una comunidad civilizada. Si tu comentario no aparece, puede ser que akismet lo haya capturado, cada día lo reviso y lo coloco en su lugar. Siento las molestias.