Cache con SimpleORM

En esta entrada escribiré detalladamente sobre una característica que me encanta SimpleORM y que, en mi humilde opinión, es fundamental para la escalabilidad, estoy hablado el Cacheo de los datos. Según su definición, el Cache es la duplicación de datos a un medio más rápido para así reducir el tiempo de acceso a dichos datos, y así optimizar el rendimiento del sistema. Dicha duplicación es volátil, es decir su eliminación o alteración no afecta a los datos originales.

El gran cuello de botella de los sistemas Webs son las bases de datos, como un amigo suele decir “the fastest database applications are those that avoid accessing the database as much as possible” (las mas aplicaciones mas rápidas utilizando bases de datos son las que la evitan lo mas posible, o algo así) lo cual tiene gran sentido, ya que las bases de datos bastante lentas, pero son necesarias. Son lentas ya que detras de una simple consulta SQL existen varios pasos, como ser (solo algunos pasos, no soy un experto esta área):

  1. Conexión a la base de datos (o utilizar una existente)
  2. Autenticación
  3. Compilación del SQL (o interpretación)
  4. Se planea y optimización lo generado/interpretado, buscando índices que agilicen la ejecución de la consulta
  5. Ejecución de la consulta, leyendo datos del I/O (generalmente el disco duro).
  6. Se devuelven los resultados.

Los arriba mencionados, son solo algunos pasos genéricos que utilizan los manejadores de bases de datos, pero puede ser menos o mas dependiendo de la implementación, peor lo que no varia nunca es el exceso de trabajo por cada consulta, haciendo de esta, en la mayoría de los casos, la parte mas lenta de cualquier sistema, incluyendo la Web donde es un factor crucial el tiempo de respuesta.

¿Y como ser podría evitar?, seria insano de mi parte a estas alturas recomendar escribir códigos para acceder a los datos utilizando algún tipo de ordenamiento, seria re-inventar la rueda. Una solución, por cierto la mas simple, es utilizar algún tipo de Cache para los resultados de consultas que no cambian frecuentemente, guardando dicho cache en un medio de rápido acceso, podría ser un directorio, memoria RAM, memcached.

Un error bastante común, que vi especialmente en CMS y similares, es que aunque el resultado a la consulta está en el cache, la conexión a la base de datos es siempre realizada, ya que esta es realizada automáticamente al inicio del código, es por este motivo que SimpleORM tiene conexión de forma implícita, solo es realizada si es estrictamente necesariamente, ejemplo:

require "DB.php";
DB::setUser("root");
DB::setPass("foobar");
DB::setDB("testing");
DB::setDriver("mysql");

Esto, a pesar de la gran utilidad, tiene un pequeño defecto, no se comprueban si los parámetros de conexión son correctos (usuario, contraseña, permisos, etc) en único lugar, en realidad nada imposible para un buen programador (utilizar transacciones siempre que se modifica algo, y utilizar siempre manejo de errores para cada operación).

En realidad, lo anterior citado solo es una pequeña acotación, lo más importante es determinar si los resultados de una consulta tienen que ser guardados en cache, y por cuanto tiempo. Después de mucho pensar en una manera sencilla, mantenible y eficiente de definir si una consulta puede ser cacheada, no se me vino nada mas simple que esto, definir un método (que por defecto en la clase padre DB retorna falso) que retorna true si la consulta actual puede ser cacheada. Los parámetros que el método recibe son los siguientes, $sql que contiene la consulta a ser ejecutada, $values que contienen las variables referenciadas en la consulta SQL, y $ttl donde se puede especificar el tiempo de vida del cacheo, en segundos.

class User
{
    public $user; /* unique index */
    public $pass;
    /* otras columnas */
    public $nombre;
    public $email;

    function isCacheable($sql, $values, &$ttl) {
        if (count($values) == 1 && isset($values['username'])) {
            $ttl = 7200; /* dos horas */
            return true;
        }
        return false;
    }
}

Como lo vieron, la definición del cacheo es sencilla, en este caso seran cacheadas (por dos horas) todas las consultas que tengan como variable username, (analizar el SQL en sí no muy lento, por eso utilizo solo las variables). Esto quiere decir realizamos una selección, donde username es “foobar”, la primera vez se conectará a la base de datos y realizará la búsqueda, luego el resultado será guardado en cache y por las siguientes dos horas, para la misma búsqueda (donde username sea “foobar”) el resultado será proveida por el cache.

$user = new User;
$user->username = "foobar";
$user->load();

Pero no todo es tan sencillo, pues existe un pequeñisimo problema que sería lo siguiente, si los datos de la tabla user para username es “foobar” cambia, digamos el nombre, nuestro cache mostraría datos no actualizado por dos horas (en el peor de los casos).

$user = new User;
$user->username = "foobar";
if ($user->load()->valid()) {
    $user->nombre = "nuevo nombre";
    $user->save();
}

La solución más eficiente que pude imaginarme fue la siguiente. Creé una función que es ejecutada cuando una actualización es realizada (a futuro lo haré para inserciones y datos borrados), y ahí proveo una simple manera de reconstruir el cache. Solo miren el siguiente código, que deberíamos agregar a la clase User.

    protected function onUpdate ($changes, $sql, $params)
    {
        /* obtener los parametros de la consulta original */
        $values = $this->getQueryParams();
        /* crear un objeto con del mismo tipo del objeto actual */
        $class   = get_class($this);
        $table   = new $class( $values );
        if (count($values) == 1 && isset($values['username'])) {
            /* deshabilitar el cacheo temporalmente y hacer la consulta */
            /* así, los datos serán traídos de la base de datos, y luego */
            /* será guardado en el cache, así actualizaremos nuestro cache */
            $table->disableQueryCache();
            $table->load();
        }
    }

Como vieron (el código es auto-explicado) , a cada cambio de los datos lo que hago es, previa verificación si la consulta es cacheada, deshabilitar temporalmente el cacheo para las selecciones y realizar la misma consulta, que posterior a su ejecución sobre-escribe el cache, actualizándolo de esta forma. No soluciona todos los casos, pero ayuda bastante, ya que esta soluciona combinada con la creatividad del programador puede hacer un sistema que dependa del cache lo más posible.

Finalmente, solo falta elegir cual método de cacheo que será el utilizado, lo cual se realizado simplemente incluyendo uno de los Cache drivers, en la distribución viene para simples archivos y memoria RAM o pero no obstante cualquier persona puede crear su propio Cache driver extendiendo esta clase base.

2 Comments

  1. Matías says:

    ¡Excelente!
    Todo se ve perfectamente flexible.

  2. Yoné says:

    Me encanta tu implementación de caché! Voy a estudiarlo para implementarlo en mi CMS, excelente trabajo Cesar!

Leave a Reply