Nette Caching
La caché acelera su aplicación guardando datos cuya obtención resultó costosa en su momento, lo que permite acceder a ellos más rápido en el futuro. Veremos:
- cómo usar la caché
- cómo cambiar el almacenamiento
- cómo invalidar correctamente la caché
Usar la caché en Nette es muy sencillo y, aun así, cubre necesidades de cacheo sofisticadas. Está diseñada para el rendimiento y una durabilidad del 100 %. Incluye adaptadores para los almacenamientos más habituales. Soporta la invalidación por etiquetas, la expiración por tiempo, la protección contra la estampida de caché y más.
Instalación
Descargue e instale el paquete con Composer:
composer require nette/caching
Uso básico
El elemento central para trabajar con la caché es el objeto Nette\Caching\Cache. Creamos una instancia suya y le
pasamos al constructor un objeto de almacenamiento. Ese objeto de almacenamiento representa el lugar físico donde se guardarán
los datos (base de datos, Memcached, archivos en disco, etc.). El objeto de almacenamiento lo obtiene normalmente mediante dependency injection pidiendo el tipo
Nette\Caching\Storage. Lo esencial lo aprenderá en la sección
Almacenamientos.
En la versión 3.0 la interfaz todavía llevaba el prefijo I, así que el nombre era
Nette\Caching\IStorage. Además, las constantes de la clase Cache se escribían en mayúsculas, p. ej.
Cache::EXPIRE en lugar de Cache::Expire.
En los ejemplos siguientes damos por hecho que tenemos un alias Cache y una instancia de almacenamiento en la
variable $storage.
use Nette\Caching\Cache;
$storage = /* ... */; // instancia de Nette\Caching\Storage
La caché es, en esencia, un almacén clave-valor, es decir, leemos y escribimos los datos con claves, de forma parecida a los arrays asociativos. Las aplicaciones constan de varias partes independientes. Si todas usaran un único almacenamiento (imagine un solo directorio en el disco), tarde o temprano se producirían colisiones de claves. Nette Framework lo resuelve dividiendo el espacio de almacenamiento en espacios de nombres (conceptualmente, como subdirectorios). Cada parte de la aplicación trabaja entonces dentro de su propio espacio de nombres con un nombre único, lo que evita cualquier colisión.
Indique el nombre del espacio de nombres como segundo argumento del constructor de la clase Cache:
$cache = new Cache($storage, 'Full Html Pages');
Si hace falta, de una instancia existente puede derivar una caché nueva acotada a un subespacio de nombres con el método
derive():
$subCache = $cache->derive('Images');
Ahora podemos usar el objeto $cache para leer de la caché y escribir en ella. Para ambas cosas sirve el método
load(). El primer argumento es la clave y el segundo es un callback de PHP que se invoca si la clave no se encuentra
en la caché. El callback genera el valor, lo devuelve, y el método load() lo guarda en la caché:
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // cálculo costoso
return $computedValue;
});
Si se omite el segundo parámetro ($value = $cache->load($key)), load() devuelve null
si el elemento no se encuentra en la caché.
Lo estupendo es que se pueden cachear todas las estructuras serializables, no solo cadenas. Lo mismo vale para las claves.
Para borrar un elemento de la caché, use el método remove():
$cache->remove($key);
También puede guardar un elemento en la caché con el método
$cache->save($key, $data, ?array $dependencies = null). Pero, por lo general, es preferible el enfoque con
load() mostrado arriba.
Memoización
La memoización consiste en cachear el resultado de la llamada a una función o método, de modo que la próxima vez que se llame con los mismos argumentos se devuelva el resultado cacheado en lugar de volver a calcularlo.
Los métodos y las funciones se pueden llamar de forma memoizada con call(callable $callback, ...$args):
$result = $cache->call('gethostbyaddr', $ip);
Así, la función gethostbyaddr() se llama solo una vez por cada argumento $ip único. Las llamadas
siguientes con el mismo $ip devolverán el valor cacheado.
También es posible crear un envoltorio memoizado de un método o una función y llamarlo más tarde:
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // la primera vez lo calcula
$result = $memoizedFactorial(5); // la segunda vez lo devuelve de la caché
Expiración e invalidación
Al usar la caché hay que resolver la cuestión de cuándo dejan de ser válidos los datos guardados anteriormente. Nette Framework proporciona mecanismos para limitar la validez de los datos o para borrarlos explícitamente (en la terminología del framework, “invalidación”).
La validez de los datos se establece al guardarlos, normalmente con el tercer parámetro del método save(),
p. ej.:
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
Como alternativa se puede establecer con el parámetro $dependencies que se pasa por referencia al callback del
método load(), p. ej.:
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
O con el tercer parámetro del propio método load(), p. ej.:
$value = $cache->load($key, function () {
return /* ... */;
}, [Cache::Expire => '20 minutes']);
En los ejemplos siguientes usaremos la segunda variante, con la variable $dependencies dentro del callback.
Expiración
La forma más sencilla de expiración es un límite de tiempo. Esto cachea los datos con una validez de 20 minutos:
// acepta también un número de segundos o una marca de tiempo UNIX
$dependencies[Cache::Expire] = '20 minutes';
Si quiere que el periodo de validez se prolongue con cada lectura (expiración deslizante), lo consigue así, pero tenga en cuenta que eso aumenta la sobrecarga de la caché:
$dependencies[Cache::Sliding] = true;
Una opción útil es hacer que los datos expiren cuando se modifique un archivo concreto o alguno de varios archivos. Es útil, por ejemplo, al cachear datos derivados de procesar esos archivos. Use rutas absolutas.
$dependencies[Cache::Files] = '/path/to/data.yaml';
// o
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
Podemos hacer que un elemento de la caché expire cuando expire otro elemento concreto (o alguno de varios). Es útil al
cachear, por ejemplo, una página HTML entera y sus fragmentos bajo claves distintas. Cuando cambia un fragmento, hay que
invalidar toda la página. Si los fragmentos están guardados bajo claves como frag1 y frag2, use:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
La expiración también se puede controlar con funciones propias o métodos estáticos. Se llaman en cada lectura para
determinar si el elemento sigue siendo válido. Por ejemplo, podemos hacer que un elemento expire siempre que cambie la versión
de PHP. Cree una función que compare la versión actual con un parámetro y, al guardar, añada a las dependencias un array con
el formato [nombre de la función, ...argumentos]:
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [
['checkPhpVersion', PHP_VERSION_ID] // expira cuando checkPhpVersion(...) === false
];
Naturalmente, todos estos criterios se pueden combinar. El elemento de la caché expira si al menos uno de los criterios deja de cumplirse.
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
Invalidación con etiquetas
Las etiquetas proporcionan un mecanismo de invalidación muy útil. A cada elemento guardado en la caché le podemos asignar una lista de etiquetas (cadenas arbitrarias). Por ejemplo, supongamos que tenemos una página HTML que muestra un artículo y sus comentarios, y que queremos cachear. Al guardarla indicamos las etiquetas pertinentes:
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Pasemos ahora a la sección de administración. Ahí tenemos un formulario para editar los artículos. Junto con guardar el
artículo en la base de datos, llamamos al método clean() para borrar los elementos cacheados según su
etiqueta:
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
Del mismo modo, al añadir un comentario nuevo (o al editarlo) tenemos que acordarnos de invalidar la etiqueta correspondiente:
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
¿Qué hemos conseguido? Que nuestra caché HTML se invalide (se borre) siempre que cambie el artículo asociado o sus
comentarios. Al editar el artículo con ID = 10 se invalida la etiqueta article/10 y se borra la página HTML
cacheada que lleva esa etiqueta. Lo mismo ocurre al añadir un comentario nuevo bajo el artículo correspondiente.
Las etiquetas requieren un Journal.
Invalidación por prioridad
A los distintos elementos de la caché les podemos asignar prioridades. Eso permite un borrado controlado, por ejemplo cuando la caché supera cierto límite de tamaño:
$dependencies[Cache::Priority] = 50;
Para borrar todos los elementos con una prioridad igual o menor que 100:
$cache->clean([
$cache::Priority => 100,
]);
Las prioridades requieren el llamado Journal.
Vaciar la caché
El parámetro Cache::All lo borra todo:
$cache->clean([
$cache::All => true,
]);
Lectura masiva
Para leer y escribir en la caché de forma masiva, use el método bulkLoad(). Pásele un array de claves y le
devolverá un array con los valores correspondientes:
$values = $cache->bulkLoad($keys);
El método bulkLoad() funciona de forma parecida a load() y también acepta un segundo parámetro con
un callback. Ese callback recibe la clave del elemento que se está generando:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // cálculo costoso
return $computedValue;
});
Al revés, para escribir varios elementos a la vez use el método bulkSave(), que toma un array de pares
clave => valor y, opcionalmente, las dependencias:
$cache->bulkSave([
$key1 => $value1,
$key2 => $value2,
], [Cache::Expire => '20 minutes']);
Uso con PSR-16
Para usar Nette Cache con una interfaz PSR-16 puede aprovechar PsrCacheAdapter. Permite una integración fluida
entre Nette Cache y cualquier código o biblioteca que espere una implementación de caché compatible con PSR-16.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Ahora puede usar $psrCache como una caché PSR-16 estándar:
$psrCache->set('key', 'value', 3600); // guarda el valor durante 1 hora
$value = $psrCache->get('key', 'default');
El adaptador soporta todos los métodos definidos en PSR-16, incluidos getMultiple(), setMultiple() y
deleteMultiple().
Cachear la salida
La salida se puede capturar y cachear de forma muy elegante:
if ($capture = $cache->capture($key)) {
// echo ... imprime algunos datos
$capture->end(); // guarda la salida en la caché
}
Si la salida ya está en la caché, el método capture() la imprime y devuelve null, así que el
bloque de la condición if se salta. En caso contrario empieza a bufferizar la salida y devuelve un objeto
$capture, que usa para guardar por fin los datos capturados en la caché con su método end().
En la versión 3.0 este método se llamaba $cache->start().
Cachear en Latte
Cachear en las plantillas de Latte es muy sencillo. Basta con envolver la parte de la
plantilla que quiere cachear con las etiquetas {cache}...{/cache}. La caché se invalida automáticamente siempre que
cambia el archivo fuente de la plantilla (incluidas las plantillas incluidas dentro del bloque cacheado). Las etiquetas
{cache} se pueden anidar. Cuando se invalida un bloque anidado (p. ej. mediante una etiqueta), también se invalida
su bloque padre.
Dentro de la etiqueta puede indicar las claves a las que se vinculará la entrada de la caché (aquí, la variable
$id), establecer un tiempo de expiración y definir etiquetas de
invalidación.
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
Todos estos parámetros son opcionales, así que no tiene que indicar la expiración, las etiquetas ni siquiera las claves.
El uso de la caché también se puede condicionar con if: el contenido se cacheará solo si se cumple la
condición:
{cache $id, if: !$form->isSubmitted()}
{$form}
{/cache}
Almacenamientos
Un almacenamiento es un objeto que representa el lugar físico donde se guardan los datos. Podemos usar una base de datos, un servidor Memcached o el almacenamiento más a mano: archivos en disco.
| Almacenamiento | Descripción |
|---|---|
| FileStorage | Almacenamiento predeterminado, guarda la caché en archivos en disco. |
| MemcachedStorage | Usa un servidor Memcached para guardar los datos. |
| MemoryStorage | Los datos se guardan temporalmente en memoria (se pierden al terminar la petición). |
| SQLiteStorage | Los datos se guardan en un archivo de base de datos SQLite. |
| DevNullStorage | Los datos no se guardan realmente; útil para hacer pruebas. |
El objeto de almacenamiento lo obtiene mediante dependency injection pidiendo el tipo
Nette\Caching\Storage. De forma predeterminada, Nette proporciona un objeto FileStorage que guarda los
datos en el subdirectorio cache dentro del directorio de archivos temporales.
Puede cambiar el almacenamiento predeterminado en la configuración:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Escribe las entradas de la caché en archivos en disco. El almacenamiento Nette\Caching\Storages\FileStorage está
muy optimizado para el rendimiento y, sobre todo, garantiza la atomicidad plena de las operaciones. ¿Qué significa eso? Que al
usar la caché no puede ocurrir que lea un archivo que otro hilo todavía no ha terminado de escribir, ni que alguien lo borre
mientras lo está leyendo. Por tanto, usar este almacenamiento de caché es completamente seguro.
Este almacenamiento incluye además una función integrada importante que evita un aumento extremo del uso de CPU cuando la caché se vacía o todavía está “fría” (es decir, aún no creada). Se conoce como prevención de la estampida de caché. Ocurre cuando varias peticiones concurrentes piden a la vez el mismo elemento cacheado (p. ej. el resultado de una consulta SQL costosa). Si el elemento no está en la caché en ese momento, todos esos procesos podrían empezar a ejecutar la misma operación costosa (como la consulta SQL). Eso multiplica la carga del servidor, e incluso puede ocurrir que ningún hilo consiga responder dentro del límite de tiempo, la caché no llegue a crearse y la aplicación se caiga. Por suerte, la caché de Nette se ocupa de eso: cuando hay varias peticiones concurrentes del mismo elemento, solo el primer hilo lo genera. Los demás hilos esperan y usan después el resultado generado por el primero.
Ejemplo de creación de un FileStorage:
// el almacenamiento será el directorio '/path/to/temp' del disco
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
El servidor Memcached es un sistema distribuido de alto rendimiento para cachear objetos en
memoria. Su adaptador en Nette es Nette\Caching\Storages\MemcachedStorage. En la configuración indique la dirección
IP del servidor y el puerto si difiere del estándar 11211.
Requiere la extensión de PHP memcached.
services:
cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
Nette\Caching\Storages\MemoryStorage es un almacenamiento que mantiene los datos dentro de un array de PHP. Por
consiguiente, los datos se pierden al terminar la petición.
SQLiteStorage
La base de datos SQLite, junto con el adaptador Nette\Caching\Storages\SQLiteStorage, proporciona una forma de
cachear los datos dentro de un único archivo en disco. La configuración indica la ruta a ese archivo de base de datos.
Requiere las extensiones de PHP pdo y pdo_sqlite.
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
Una implementación especial de almacenamiento es Nette\Caching\Storages\DevNullStorage, que no guarda ningún
dato. Por eso resulta adecuada para hacer pruebas cuando quiere eliminar los efectos del cacheo.
Usar la caché en el código
Al usar la caché en su código hay dos enfoques principales. El primero es obtener el objeto de almacenamiento mediante dependency injection y crear después usted mismo el
objeto Cache:
use Nette;
class ClassOne
{
private Nette\Caching\Cache $cache;
public function __construct(Nette\Caching\Storage $storage)
{
$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
}
}
La segunda opción es pedir directamente el objeto Cache:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
El objeto Cache hay que definirlo entonces en la configuración, por ejemplo así:
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette guarda la información sobre las etiquetas y las prioridades en el llamado journal. De forma predeterminada, para ello se
usa SQLite mediante el archivo journal.s3db, y se requieren las extensiones de PHP pdo y
pdo_sqlite.
Puede cambiar la implementación del journal en la configuración:
services:
cache.journal: MyJournal
Servicios DI
Estos servicios se añaden al contenedor DI:
| Nombre | Tipo | Descripción |
|---|---|---|
cache.journal |
Nette\Caching\Storages\Journal | El almacenamiento del journal de la caché |
cache.storage |
Nette\Caching\Storage | El almacenamiento principal de la caché |
Desactivar la caché
Una forma de desactivar el cacheo en su aplicación es establecer el almacenamiento a DevNullStorage:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
Este ajuste no afecta al cacheo de las plantillas de Latte ni del contenedor DI, ya que esas bibliotecas no usan los servicios
de nette/caching y gestionan sus cachés por su cuenta. Además, sus cachés normalmente no hace falta
desactivarlas en modo de desarrollo.
Si está actualizando a una versión más reciente, vea la página de actualización.