Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Caché

La caché ([cache]) acelera su aplicación al guardar datos obtenidos con esfuerzo una vez para su uso futuro. Mostraremos:

  • cómo usar la caché
  • cómo cambiar el almacenamiento
  • cómo invalidar correctamente la caché

Usar la caché en Nette es muy fácil, pero cubre incluso necesidades muy avanzadas. Está diseñada para el rendimiento y una resistencia del 100%. En su núcleo, encontrará adaptadores para los almacenamientos backend más comunes. Permite la invalidación basada en etiquetas, la expiración por tiempo, tiene protección contra la estampida de caché, etc.

Instalación

Puede descargar e instalar la librería usando Composer:

composer require nette/caching

Uso básico

El centro del trabajo con la caché es el objeto Nette\Caching\Cache. Creamos su instancia y pasamos el llamado almacenamiento como parámetro al constructor. Este es un objeto que representa el lugar donde los datos se almacenarán físicamente (base de datos, Memcached, archivos en disco, …). Accedemos al almacenamiento pidiéndolo mediante inyección de dependencias con el tipo Nette\Caching\Storage. Aprenderá todo lo esencial en la sección Almacenamientos.

En la versión 3.0, la interfaz todavía tenía el prefijo I, por lo que el nombre era Nette\Caching\IStorage. Además, las constantes de la clase Cache estaban escritas en mayúsculas, así que, por ejemplo, Cache::EXPIRE en lugar de Cache::Expire.

Para los siguientes ejemplos, supongamos que tenemos un alias Cache creado y el almacenamiento en la variable $storage.

use Nette\Caching\Cache;

$storage = /* ... */; // instancia de Nette\Caching\Storage

La caché es básicamente un key-value store, lo que significa que leemos y escribimos datos bajo claves al igual que con los arrays asociativos. Las aplicaciones consisten en varias partes independientes, y si todas usaran un solo almacenamiento (imagine un directorio en el disco), tarde o temprano ocurriría una colisión de claves. Nette Framework resuelve este problema dividiendo todo el espacio en espacios de nombres (subdirectorios). Cada parte del programa luego usa su propio espacio con un nombre único, y no puede ocurrir ninguna colisión.

Especificamos el nombre del espacio como el segundo parámetro del constructor de la clase Cache:

$cache = new Cache($storage, 'Full Html Pages');

Ahora podemos usar el objeto $cache para leer y escribir en la caché. El método load() sirve para ambos propósitos. El primer argumento es la clave y el segundo es un callback de PHP, que se llama cuando la clave no se encuentra en la caché. El callback genera el valor, lo devuelve y se almacena en la caché:

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // cálculo costoso
	return $computedValue;
});

Si no especificamos el segundo parámetro $value = $cache->load($key), devuelve null si el elemento no está en la caché.

Lo bueno es que se pueden almacenar en la caché cualquier estructura serializable, no solo cadenas. Y lo mismo se aplica incluso a las claves.

Eliminamos un elemento de la caché usando el método remove():

$cache->remove($key);

También es posible guardar un elemento en la caché usando el método $cache->save($key, $value, array $dependencies = []). Sin embargo, se prefiere el método mencionado anteriormente usando load().

Memoización

La memoización significa almacenar en caché el resultado de una llamada a una función o método para que pueda usarlo la próxima vez sin calcular lo mismo una y otra vez.

Se pueden llamar a métodos y funciones de forma memoizada usando call(callable $callback, ...$args):

$result = $cache->call('gethostbyaddr', $ip);

La función gethostbyaddr() se llamará solo una vez para cada parámetro $ip, y la próxima vez se devolverá el valor de la caché.

También es posible crear un envoltorio memoizado sobre un método o función que se puede llamar más tarde:

function factorial($num)
{
	return /* ... */;
}

$memoizedFactorial = $cache->wrap('factorial');

$result = $memoizedFactorial(5); // calcula la primera vez
$result = $memoizedFactorial(5); // la segunda vez desde la caché

Expiración e Invalidación

Al almacenar en caché, es necesario abordar la cuestión de cuándo los datos previamente almacenados se vuelven inválidos. Nette Framework ofrece un mecanismo para limitar la validez de los datos o eliminarlos de forma controlada (en la terminología del framework, “invalidar”).

La validez de los datos se establece en el momento del almacenamiento utilizando el tercer parámetro del método save(), por ejemplo:

$cache->save($key, $value, [
	$cache::Expire => '20 minutes',
]);

O usando el parámetro $dependencies pasado por referencia al callback del método load(), por ejemplo:

$value = $cache->load($key, function (&$dependencies) {
	$dependencies[Cache::Expire] = '20 minutes';
	return /* ... */;
});

O usando el tercer parámetro en el método load(), por ejemplo:

$value = $cache->load($key, function () {
	return ...;
}, [Cache::Expire => '20 minutes']);

En los siguientes ejemplos, asumiremos la segunda variante y, por lo tanto, la existencia de la variable $dependencies.

Expiración

La expiración más simple es un límite de tiempo. Así es como almacenamos en caché datos válidos durante 20 minutos:

// también acepta número de segundos o timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';

Si quisiéramos extender el período de validez con cada lectura, se puede lograr de la siguiente manera, pero tenga cuidado, la sobrecarga de la caché aumentará:

$dependencies[Cache::Sliding] = true;

Una opción útil es dejar que los datos expiren cuando cambia un archivo o uno de varios archivos. Esto se puede usar, por ejemplo, al almacenar en caché datos resultantes del procesamiento de estos 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 otro elemento (o uno de varios otros) expire. Esto se puede usar cuando almacenamos en caché, por ejemplo, una página HTML completa y sus fragmentos bajo otras claves. Tan pronto como cambia un fragmento, toda la página se invalida. Si tenemos fragmentos almacenados bajo claves como frag1 y frag2, usamos:

$dependencies[Cache::Items] = ['frag1', 'frag2'];

La expiración también se puede controlar mediante funciones personalizadas o métodos estáticos, que deciden cada vez que se lee si el elemento sigue siendo válido. De esta manera, por ejemplo, podemos hacer que un elemento expire siempre que cambie la versión de PHP. Creamos una función que compara la versión actual con un parámetro, y al guardar, agregamos un array con el formato [nombre de la función, ...argumentos] entre las dependencias:

function checkPhpVersion($ver): bool
{
	return $ver === PHP_VERSION_ID;
}

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // expira cuando checkPhpVersion(...) === false
];

Por supuesto, todos los criterios se pueden combinar. La caché expirará cuando al menos un criterio no se cumpla.

$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';

Invalidación mediante etiquetas

Una herramienta de invalidación muy útil son las llamadas etiquetas. Podemos asignar una lista de etiquetas, que son cadenas arbitrarias, a cada elemento de la caché. Por ejemplo, tengamos una página HTML con un artículo y comentarios que almacenaremos en caché. Al guardar, especificamos las etiquetas:

$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];

Pasemos a la administración. Aquí encontramos un formulario para editar el artículo. Junto con guardar el artículo en la base de datos, llamamos al comando clean(), que elimina elementos de la caché según la etiqueta:

$cache->clean([
	$cache::Tags => ["article/$articleId"],
]);

Del mismo modo, en el lugar de agregar un nuevo comentario (o editar un comentario), no olvidemos invalidar la etiqueta correspondiente:

$cache->clean([
	$cache::Tags => ["comments/$articleId"],
]);

¿Qué hemos logrado con esto? Que nuestra caché HTML se invalidará (eliminará) cada vez que cambie el artículo o los comentarios. Cuando se edita un artículo con ID = 10, se fuerza la invalidación de la etiqueta article/10 y la página HTML que lleva esa etiqueta se elimina de la caché. Lo mismo ocurre al insertar un nuevo comentario bajo el artículo correspondiente.

Las etiquetas requieren el llamado Journal.

Invalidación mediante prioridad

Podemos establecer una prioridad para los elementos individuales en la caché, que se puede usar para eliminarlos cuando, por ejemplo, la caché exceda un cierto tamaño:

$dependencies[Cache::Priority] = 50;

Eliminaremos todos los elementos con una prioridad igual o menor que 100:

$cache->clean([
	$cache::Priority => 100,
]);

Las prioridades requieren el llamado Journal.

Limpiar la caché

El parámetro Cache::All elimina todo:

$cache->clean([
	$cache::All => true,
]);

Lectura masiva

Para la lectura y escritura masiva en la caché, se utiliza el método bulkLoad(), al que pasamos un array de claves y obtenemos un array de valores:

$values = $cache->bulkLoad($keys);

El método bulkLoad() funciona de manera similar a load() también con el segundo parámetro callback, al que se pasa la clave del elemento generado:

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // cálculo costoso
	return $computedValue;
});

Uso con PSR-16

Para usar Nette Cache con la interfaz PSR-16, puede utilizar el adaptador PsrCacheAdapter. Permite una integración perfecta entre Nette Cache y cualquier código o librería que espere una caché compatible con PSR-16.

$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);

Ahora puede usar $psrCache como una caché PSR-16:

$psrCache->set('key', 'value', 3600); // guarda el valor durante 1 hora
$value = $psrCache->get('key', 'default');

El adaptador admite todos los métodos definidos en PSR-16, incluidos getMultiple(), setMultiple() y deleteMultiple().

Almacenamiento en caché de la salida

La salida se puede capturar y almacenar en caché de forma muy elegante:

if ($capture = $cache->capture($key)) {

	echo ... // imprimimos datos

	$capture->end(); // guardamos la salida en la caché
}

Si la salida ya está almacenada en la caché, el método capture() la imprimirá y devolverá null, por lo que la condición no se ejecutará. De lo contrario, comenzará a capturar la salida y devolverá el objeto $capture, con el que finalmente guardaremos los datos impresos en la caché.

En la versión 3.0, el método se llamaba $cache->start().

Almacenamiento en caché en Latte

El almacenamiento en caché en las plantillas Latte es muy fácil, simplemente envuelva una parte de la plantilla con las etiquetas {cache}...{/cache}. La caché se invalida automáticamente cuando cambia la plantilla de origen (incluidas las plantillas incluidas dentro del bloque de caché). Las etiquetas {cache} se pueden anidar, y cuando un bloque anidado se invalida (por ejemplo, por una etiqueta), el bloque padre también se invalida.

En la etiqueta, es posible especificar claves a las que se vinculará la caché (aquí la variable $id) y establecer la expiración y las etiquetas para la invalidación.

{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
	...
{/cache}

Todos los elementos son opcionales, por lo que no tenemos que especificar ni la expiración, ni las etiquetas, ni siquiera las claves.

El uso de la caché también se puede condicionar usando if: el contenido se almacenará en caché solo si se cumple la condición:

{cache $id, if: !$form->isSubmitted()}
	{$form}
{/cache}

Almacenamientos

Un almacenamiento es un objeto que representa el lugar donde se almacenan físicamente los datos. Podemos usar una base de datos, un servidor Memcached o el almacenamiento más accesible, que son archivos en disco.

Almacenamiento Descripción
FileStorage almacenamiento predeterminado con guardado en archivos en disco
MemcachedStorage utiliza un servidor Memcached
MemoryStorage los datos están temporalmente en memoria
SQLiteStorage los datos se guardan en una base de datos SQLite
DevNullStorage los datos no se guardan, adecuado para pruebas

Accede al objeto de almacenamiento pidiéndolo mediante inyección de dependencias con el tipo Nette\Caching\Storage. Como almacenamiento predeterminado, Nette proporciona el objeto FileStorage que guarda los datos en la subcarpeta cache en el directorio para archivos temporales.

Puede cambiar el almacenamiento en la configuración:

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

FileStorage

Escribe la caché en archivos en disco. El almacenamiento Nette\Caching\Storages\FileStorage está muy bien optimizado para el rendimiento y, sobre todo, garantiza la atomicidad completa de las operaciones. ¿Qué significa eso? Que al usar la caché, no puede suceder que leamos un archivo que otro hilo aún no ha escrito por completo, o que alguien lo elimine “debajo de nuestras manos”. Por lo tanto, el uso de la caché es completamente seguro.

Este almacenamiento también tiene una función importante incorporada que evita un aumento extremo del uso de la CPU cuando se borra la caché o aún no se ha calentado (es decir, creado). Esta es una prevención contra la estampida de caché. Sucede que en un momento dado, un gran número de solicitudes concurrentes llegan queriendo lo mismo de la caché (por ejemplo, el resultado de una consulta SQL costosa) y como no está en la memoria caché, todos los procesos comienzan a ejecutar la misma consulta SQL. La carga se multiplica y puede incluso suceder que ningún hilo logre responder dentro del límite de tiempo, la caché no se crea y la aplicación colapsa. Afortunadamente, la caché en Nette funciona de tal manera que cuando hay múltiples solicitudes concurrentes para un elemento, solo el primer hilo lo genera, los demás esperan y luego usan el resultado generado.

Ejemplo de creación de FileStorage:

// el almacenamiento será el directorio '/path/to/temp' en el disco
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

El servidor Memcached es un sistema de almacenamiento en memoria distribuida de alto rendimiento, cuyo adaptador es Nette\Caching\Storages\MemcachedStorage. En la configuración, especificamos la dirección IP y el puerto, si es diferente del estándar 11211.

Requiere la extensión PHP memcached.

services:
	cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')

MemoryStorage

Nette\Caching\Storages\MemoryStorage es un almacenamiento que guarda los datos en un array PHP y, por lo tanto, se pierden al finalizar la solicitud.

SQLiteStorage

La base de datos SQLite y el adaptador Nette\Caching\Storages\SQLiteStorage ofrecen una forma de almacenar la caché en un solo archivo en el disco. En la configuración, especificamos la ruta a este archivo.

Requiere las extensiones 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 en realidad no guarda datos en absoluto. Por lo tanto, es adecuado para pruebas cuando queremos eliminar la influencia de la caché.

Uso de la caché en el código

Al usar la caché en el código, tenemos dos formas de hacerlo. La primera es que nos pasen el almacenamiento mediante inyección de dependencias y creemos un 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 que nos pasen directamente el objeto Cache:

class ClassTwo
{
	public function __construct(
		private Nette\Caching\Cache $cache,
	) {
	}
}

El objeto Cache se crea luego directamente en la configuración de esta manera:

services:
	- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )

Journal

Nette guarda las etiquetas y prioridades en el llamado journal. Por defecto, se utiliza SQLite y el archivo journal.s3db para esto, y se requieren las extensiones PHP pdo y pdo_sqlite.

Puede cambiar el journal en la configuración:

services:
	cache.journal: MyJournal

Servicios DI

Estos servicios se agregan al contenedor DI:

Nombre Tipo Descripción
cache.journal Nette\Caching\Storages\Journal journal
cache.storage Nette\Caching\Storage almacenamiento

Desactivar la caché

Una de las formas de desactivar la caché en la aplicación es establecer el almacenamiento en DevNullStorage:

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

Esta configuración no afecta el almacenamiento en caché de plantillas en Latte o el contenedor DI, ya que estas librerías no utilizan los servicios de nette/caching y gestionan su caché de forma independiente. Además, su caché no necesita ser desactivada en el modo de desarrollo.

versión: 3.x