Imaginemos que tenemos un sistema en el que los usuarios añaden artículos. Para cada usuario, mostramos estadísticas sobre sus artículos en su panel personal: la cantidad de artículos, el recuento promedio de palabras, la frecuencia de publicación, etc. Para acelerar el sistema, almacenamos estos datos en caché. Se crea una clave de caché única para cada informe.
Surge la pregunta: ¿cómo invalidar dichos cachés cuando cambian los datos? Un enfoque es borrar manualmente el caché para cada evento, por ejemplo, cuando se agrega un nuevo artículo:
class InvalidateArticleReportCacheOnArticleCreated { public function handle(event: ArticleCreatedEvent): void { this->cacheService->deleteMultiple([ 'user_article_report_count_' . event->userId, 'user_article_report_word_avg_' . event->userId, 'user_article_report_freq_avg_' . event->userId, ]) } }
Este método funciona, pero resulta complicado cuando se trabaja con una gran cantidad de informes y claves. Aquí es donde resulta útil el almacenamiento en caché etiquetado. El almacenamiento en caché etiquetado permite asociar los datos no solo con una clave, sino también con una matriz de etiquetas. Posteriormente, se pueden invalidar todos los registros asociados con una etiqueta específica, lo que simplifica significativamente el proceso.
Escribir un valor en la caché con etiquetas:
this->taggedCacheService->set( key: 'user_article_report_count_' . user->id, value: value, tagNames: [ 'user_article_cache_tag_' . user->id, 'user_article_report_cache_tag_' . user->id, 'user_article_report' ] )
Invalidar la caché por etiquetas:
class UpdateCacheTagsOnArticleCreated { public function handle(event: ArticleCreatedEvent): void { this->taggedCacheService->updateTagsVersions([ 'user_article_cache_tag_' . user->id, ]) } }
Aquí, la etiqueta 'user_article_cache_tag_' . $user->id
representa los cambios en los artículos del usuario. Puede utilizarse para invalidar cualquier caché que dependa de estos datos. Una etiqueta más específica 'user_article_report_cache_tag_' . $user->id
permite borrar únicamente los informes del usuario, mientras que una etiqueta general 'user_article_report'
invalida los cachés de informes de todos los usuarios.
Si su biblioteca de almacenamiento en caché no admite el etiquetado, puede implementarlo usted mismo. La idea principal es almacenar los valores de la versión actual de las etiquetas y, para cada valor etiquetado, almacenar las versiones de las etiquetas que estaban vigentes en el momento en que el valor se escribió en la memoria caché. Luego, al recuperar un valor de la memoria caché, también se recuperan las versiones actuales de las etiquetas y se verifica su validez comparándolas.
Creación de una clase TaggedCache
class TaggedCache { private cacheService: cacheService }
Implementación del método set
para escribir en la memoria caché con etiquetas. En este método, necesitamos escribir el valor en la memoria caché, así como recuperar las versiones actuales de las etiquetas proporcionadas y guardarlas asociadas con la clave de caché específica. Esto se logra utilizando una clave adicional con un prefijo agregado a la clave proporcionada.
class TaggedCache { private cacheService: cacheService public function set( key: string, value: mixed, tagNames: string[], ttl: int ): bool { if (empty(tagNames)) { return false } tagVersions = this->getTagsVersions(tagNames) tagsCacheKey = this->getTagsCacheKey(key) return this->cacheService->setMultiple( [ key => value, tagsCacheKey => tagVersions, ], ttl ) } private function getTagsVersions(tagNames: string[]): array<string, string> { tagVersions = [] tagVersionKeys = [] foreach (tagNames as tagName) { tagVersionKeys[tagName] = this->getTagVersionKey(tagName) } if (empty(tagVersionKeys)) { return tagVersions } tagVersionsCache = this->cacheService->getMultiple(tagVersionKeys) foreach (tagVersionKeys as tagName => tagVersionKey) { if (empty(tagVersionsCache[tagVersionKey])) { tagVersionsCache[tagVersionKey] = this->updateTagVersion(tagName) } tagVersions[$tagName] = tagVersionsCache[tagVersionKey] } return tagVersions } private function getTagVersionKey(tagName: string): string { return 'tag_version_' . tagName } private function getTagsCacheKey(key: string): string { return 'cache_tags_tagskeys_' . key }
Añadiendo el método get
para recuperar valores etiquetados de la memoria caché. Aquí, recuperamos el valor utilizando la clave, así como las versiones de etiqueta asociadas con esa clave. Luego, comprobamos la validez de las etiquetas. Si alguna etiqueta no es válida, el valor se elimina de la memoria caché y se devuelve un valor null
. Si todas las etiquetas son válidas, se devuelve el valor almacenado en caché.
class TaggedCache { private cacheService: cacheService public function get(key: string): mixed { tagsCacheKey = this->getTagsCacheKey(key) values = this->cacheService->getMultiple([key, tagsCacheKey]) if (empty(values[key]) || empty(values[tagsCacheKey])) { return null } value = values[key] tagVersions = values[tagsCacheKey] if (! this->isTagVersionsValid(tagVersions)) { this->cacheService->deleteMultiple([key, tagsCacheKey]) return null } return value } private function isTagVersionsValid(tagVersions: array<string, string>): bool { tagNames = array_keys(tagVersions) actualTagVersions = this->getTagsVersions(tagNames) foreach (tagVersions as tagName => tagVersion) { if (empty(actualTagVersions[tagName])) { return false } if (actualTagVersions[tagName] !== tagVersion) { return false } } return true } }
Implementación del método updateTagsVersions
para actualizar las versiones de las etiquetas. Aquí, iteramos sobre todas las etiquetas proporcionadas y actualizamos sus versiones utilizando, por ejemplo, la hora actual como versión.
class TaggedCache { private cacheService: cacheService public function updateTagsVersions(tagNames: string[]): void { foreach (tagNames as tagName) { this->updateTagVersion(tagName) } } private function updateTagVersion(tagName: string): string { tagKey = this->getTagVersionKey(tagName) tagVersion = this->generateTagVersion() return this->cacheService->set(tagKey, tagVersion) ? tagVersion : '' } private function generateTagVersion(): string { return (string) hrtime(true) } }
Este enfoque es práctico y universal. El almacenamiento en caché con etiquetas elimina la necesidad de especificar manualmente todas las claves para invalidarlas, lo que automatiza el proceso. Sin embargo, requiere recursos adicionales: almacenar datos de versiones de etiquetas y verificar su validez con cada solicitud.
Si su servicio de almacenamiento en caché es rápido y no tiene limitaciones de tamaño, este enfoque no afectará significativamente el rendimiento. Para minimizar la carga, puede combinar el almacenamiento en caché etiquetado con mecanismos de almacenamiento en caché local.
De este modo, el almacenamiento en caché etiquetado no solo simplifica la invalidación, sino que también hace que trabajar con datos sea más flexible y comprensible, especialmente en sistemas complejos con grandes cantidades de datos interconectados.