Ir al contenido principal

Caché de datos propios para agilizar ejecución de scripts PHP

De vez en cuando viene bien ayudar a PHP a generar respuestas de forma mucho más rápida y eficiente de lo que ya es capaz por si mismo. En mi opinión, la mejor forma de hacerlo es implementar un sistema de caché propio o (una opción un tanto más aburrida) reutilizar alguno ya existente. En este artículo detallaremos la implementación de una clase que permita este cometido.

Aviso: Este artículo contiene ejemplos de programación en PHP aunque los conceptos explicados pueden ser aplicados a scripts realizados en cualquier otro lenguaje de programación.

Para ilustrar este proceso, veamos el siguiente caso no tan hipotético: Una aplicación web hace uso de una API del clima. El resultado de la consulta es la misma para todos los usuarios y cambia solamente cada hora. Sin embargo, cada consulta tarda un tiempo promedio de 15 segundos. Esto implica que cada usuario deberá esperar esos 15 segundos para ver el resultado (sumado al tiempo que tome la visualización y otros procesos que deba realizar la aplicación web). ¡Eso es mucho tiempo para esperar! Este tiempo puede reducirse si la respuesta a la consulta que haga el primer usuario se guarda en caché y los demás hacen uso de ese caché en lugar de realizar por si mismos la consulta a la API, después de todo, la respuesta no cambiará por un tiempo. De esta forma, solamente el primer usuario tendría que esperar esos +15 segundos y para los demás la respuesta sería mucho más rápida. Una hora después, se repite el proceso con un nuevo “primer usuario”.

Supongo que estás pensando en una opción para que incluso ese primer usuario no deba esperar tanto, ¿verdad? Algo así como tener una tarea que realice la consulta cada hora y que sea esta la que genere ese caché. Este enfoque aunque valido, haría que se realice el consumo de esa API haya o no una consulta de usuario que la requiera y esto implicaría un consumo de recursos no necesario en algunos casos, algo a considerar (especialmente si se debe pagar por cada consulta de la API). En cualquier caso, la aplicación web debería siempre estar en capacidad de consumir el API en la eventualidad que esa tarea remota falle y no se ejecute a tiempo.

Volviendo a nuestro caso, el código para la consulta de nuestra API con el uso de caché sería algo como lo siguiente:

$data_clima = false;
// Creación del objeto caché
$cache = new DataCache();
// Consulta si el archivo caché es valido
if ($cache->read('api-clima')) {
    // Recupera datos del caché
    $data_clima = $cache->getData();
}
else {
    // Realiza consulta a la API externa
    $data_clima = get_api_clima();
    // Genera archivo de caché para el siguiente uso
    $cache->save($data_clima);
}

Al terminar de ejecutarse el bloque, la variable $data_clima contendrá el valor retornado ya sea por la API externa (representada en la función get_api_clima() de este ejemplo) o por los datos almacenados en el caché, que previamente habría sido generado luego de recuperar los datos de la API externa una primera vez. Fácil, ¿verdad? Pero, ¿cómo controlar que se actualice cada hora, según el requerimiento previo? Para esto, adicionamos a nuestra clase una propiedad de vigencia de la caché antes de guardarla. Algo así como:

    $data_clima = get_api_clima();
    // Fija duración de la caché
    $cache->duration(3600);
    // Genera archivo de caché para el siguiente uso
    $cache->save($data_clima);

 De esta forma, le indicamos al caché la vigencia de los datos almacenados (para el ejemplo, una hora o 3600 segundos).

Otra mejora a considerar es la de prevenir el mantener en memoria dos copias de los datos de interés, esto es, una copia en el objeto $cache y otra en la variable $data_clima. Para corregir esta situación y reducir el riesgo de fallos por consumo innecesario de memoria (especialmente si se manejan en simultanea múltiples consultas de datos en caché), podemos implementar un método que recupere los datos y libere el espacio ocupado, así cambiamos el método getData() por exportInto(), así nos queda: 

if ($cache->read('api-clima')) {
    // Recupera datos del caché
    $cache->exportInto($data_clima);
}

Ahora si, ya con todas estas consideraciones, la implementación de la clase DataCache a usar sería la siguiente:

class DataCache {

    private string $filename = '';      // Nombre del archivo de caché
    private mixed $data = false;        // Datos de interés
    private int $maxtime = 0;           // Fecha de caducidad del caché

    /**
     * Lee archivo de caché.
     *
     * @param string $name Nombre asociado al caché
     * @return bool TRUE si pudo recuperar los datos de caché.
     **/
    public function read(string $name) {

        $this->maxtime = 0;
        $this->data = false;
        $this->filename = '';

        $result = false;

        if (trim($name) !== '') {
            // Automáticamente toma el directorio temporal del sistema.
            // Puede personalizar esta parte usando su propio directorio.
            // Usa md5() para "oscurecer" el nombre del archivo a usar y
            // prevenir problemas si el nombre contiene caracteres no
            // validos para nombres de archivo.
            $this->filename = sys_get_temp_dir() . 
                              DIRECTORY_SEPARATOR . 'cache-' .
                              md5(strtolower($name));
            // Lee datos almacenados
            if (file_exists($this->filename)) {
                $this->data = unserialize(
                                  file_get_contents($this->filename)
                                  );
                $result = (is_array($this->data) &&
                           array_key_exists('data', $this->data) &&
                           array_key_exists('maxtime', $this->data));
                if ($result && $this->data['maxtime'] > 0) {
                    $result = (time() <= $this->data['maxtime']);
                }
                if (!$result) {
                    // Falló al recuperar la data necesaria
                    $this->data = false;
                }
                else {
                    // Separa componentes
                    $this->maxtime = $this->data['maxtime'];
                    $this->data = $this->data['data'];
                }
            }
        }

        return $result;
    }

    /**
     * Retorna los datos recuperados del caché.
     *
     * @return mixed Datos recuperados o FALSE si no están disponibles.
     **/
    public function getData() {
        return $this->data;
    }

    /**
     * Asocia los datos recuperados del caché a una variable y luego los
     * elimina de la clase.
     *
     * @param mixed $data Variable en la que retorna los datos.
     **/
    public function exportInto(mixed &$data) {
        $data = $this->data;
        $this->data = false;
    }

    /**
     * Genera un nuevo archivo de caché con los datos indicados.
     *
     * @param mixed $data Datos a guardar en el archivo de caché.
     * @return bool TRUE si pudo guardar los datos en caché.
     **/
    public function save(mixed $data) {

        $result = false;

        if ($this->filename != '') {
            $bytes = file_put_contents(
                $this->filename,
                serialize(
                    array('data' => $data, 'maxtime' => $this->maxtime)
                    ));
            if ($bytes > 0) {
                $this->data = $data;
                $result = true;
            }
        }

        return $result;
    }

    /**
     * Tiempo en segundos para el que los datos en la caché son validos.
     * Debe ser mayor o igual a cero. Valor de cero remueve limite.
     *
     * @param int $seconds Segundos
     **/
    public function duration(int $seconds) {
        if ($seconds >= 0) {
            $this->maxtime = time() + $seconds;
            }
    }
}

Y con esto damos por terminada la implementación de nuestro propia clase administradora de datos en caché. Hemos de tener presente que este es un ejemplo básico y que deben considerarse situaciones a prevenir en un entorno de producción, como por ejemplo:

  • Fallos en la apertura de estos archivos de caché por parte de múltiples usuarios al mismo tiempo.
  • Validar el rendimiento en lectura/escritura de datos extremadamente grandes en los archivos de caché.
  • Proteger/encriptar el contenido de los archivos de caché cuando se manejen datos sensibles.
  • Remover archivos de caché no usados en mucho tiempo para mantener un espacio en disco saludable (sugerencia: implementar una tarea externa que realice esta actividad).

Espero que este artículo sea de utilidad y/o de inspiración para la implementación de tus propias soluciones. Te invito a compartir en los comentarios tus sugerencias para mejorar el código propuesto y los escenarios en que este uso de caché propietario puede ayudarte para reducir el consumo de recursos y agilizar tus scripts en PHP.

Por si acaso, también puedes encontrar este artículo en LinkedIn. y en medium.com.

Imagen de Dok Sev por cortesía de Pixabay

Comentarios

Entradas populares de este blog

Manejo de recursos HTML para tus páginas web con PHP

Déjame saber si te resulta familiar esta situación: páginas web que descargan el mismo recurso (sean estilos CSS o código Javascript) más de una vez o incluyen recursos remotos que tardan una eternidad en cada descarga. Yo lo he visto en más de una ocasión y no es difícil imaginar el porqué ocurre. Un desarrollador incluye el recurso de estilos que necesita su segmento de código y otro hace lo mismo, sin reparar (o sin que siquiera importe) que comparten el mismo recurso. En otro escenario muy común, acostumbran incluir muchos recursos remotos, con lo que el rendimiento de la página depende de lo rápido que responda dicho recurso. ¿Puede hacerse algo al respecto? Claro que si. Vamos a crear una clase en PHP que se encargue de administrar estos recursos y que nos facilite su despliegue en la página sin repeticiones . ¿Y respecto a la demora en la carga de recursos remotos? Atendamos una cosa por vez, porque como dicen por ahí: «Vísteme despacio, que tengo prisa». Administrando ...

Manejo de clases globales únicas en PHP

¿Cómo acceder desde cualquier script en tu proyecto a Clases y/o funciones de uso común? Este puede ser una de las primeras directrices a establecer para cualquier proyecto porque siempre, siempre , sea en  PHP  u otro lenguaje, será necesario usar recursos comunes. En PHP existen diferentes alternativas para su manejo, ya sea por medio de variables globales o de clases/objetos estáticos. A continuación consideraremos una propuesta para este manejo. Creación de recursos globales Para ilustrar esta solución, partimos de la necesidad de implementar una librería para manejo de servicios relacionados con el servidor Web, que de forma amigable nos permita disponer de información como: Valores almacenados de la variable superglobal $_SERVER de PHP. Valores asociados a la consulta realizada por el usuario, por Ej. la dirección IP del usuario o la URL ingresada. Valores asociados al servidor web usado, por Ej. la dirección IP del servidor o la ubicación del script que ej...

¿Qué tan bueno es realmente el “foreach” en PHP?

Como toda buena historia, esta comienza hace algún tiempo. El que fuera mi jefe por allá en la primera década del 2000, realmente odiaba (y mucho) el uso del foreach en el código PHP . Prefería que usáramos alguna alternativa diferente, alguna combinación del  for o del while . ¿Por qué? Ve tú a saber, nunca fue abierto respecto a las razones de su aprensión hacia ese constructor propio del lenguaje. Pero antes de continuar, veamos qué es y para qué nos puede servir. Arreglos, tenían que ser arreglos ¿Qué es foreach ? De acuerdo al manual de PHP , su definición es la siguiente: El constructor foreach proporciona un modo sencillo de iterar sobre arrays . foreach funciona sólo sobre arrays y objetos , y emitirá un error al intentar usarlo con una variable de un tipo diferente de datos o una variable no inicializada. Para su uso correcto existen dos sintaxis validas, a saber: foreach (expresión_array as $value) { ... } foreach (expresión_array as $key => $value) { ....