Уявіть, що у нас є система, в якій користувачі додають статті. Для кожного користувача ми відображаємо статистику щодо його статей на особистій панелі: кількість статей, середню кількість слів, частоту публікацій тощо. Щоб прискорити роботу системи, ми кешуємо ці дані. Для кожного звіту створюється унікальний ключ кешу.
Виникає питання: як зробити такі кеші недійсними при зміні даних? Один із підходів полягає в тому, щоб вручну очистити кеш для кожної події, наприклад, коли додається нова стаття:
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, ]) } }
Цей метод працює, але стає громіздким при роботі з великою кількістю звітів і ключів. Тут стане в нагоді кешування з тегами. Кешування з тегами дозволяє асоціювати дані не тільки з ключем, а й з масивом тегів. Згодом усі записи, пов’язані з певним тегом, можуть бути визнані недійсними, що значно спрощує процес.
Записування значення в кеш з тегами:
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' ] )
Недійсність кешу за тегами:
class UpdateCacheTagsOnArticleCreated { public function handle(event: ArticleCreatedEvent): void { this->taggedCacheService->updateTagsVersions([ 'user_article_cache_tag_' . user->id, ]) } }
Тут тег 'user_article_cache_tag_' . $user->id
представляє зміни в статтях користувача. Його можна використовувати для анулювання будь-яких кешів, що залежать від цих даних. Більш конкретний тег 'user_article_report_cache_tag_' . $user->id
дозволяє очищати лише звіти користувача, тоді як загальний тег 'user_article_report'
робить кеші звітів недійсними для всіх користувачів.
Якщо ваша бібліотека кешування не підтримує тегування, ви можете реалізувати це самостійно. Основна ідея полягає в тому, щоб зберегти значення поточної версії тегів, а також для кожного значення, позначеного тегами, щоб зберегти версії тегів, які були поточними на момент запису значення в кеш. Потім, під час отримання значення з кешу, також витягуються поточні версії тегів, і їх дійсність перевіряється шляхом їх порівняння.
Створення класу TaggedCache
class TaggedCache { private cacheService: cacheService }
Реалізація методу set
для запису в кеш з тегами. У цьому методі нам потрібно записати значення в кеш, а також отримати поточні версії наданих тегів і зберегти їх, пов’язані з певним ключем кешу. Це досягається за допомогою додаткового ключа з префіксом, доданим до наданого ключа.
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 }
Додавання методу get
для отримання позначених значень із кешу. Тут ми отримуємо значення за допомогою ключа, а також версії тегів, пов’язані з цим ключем. Потім перевіряємо валідність тегів. Якщо будь-який тег недійсний, значення видаляється з кешу та повертається null
. Якщо всі теги дійсні, повертається кешоване значення.
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 } }
Реалізація методу updateTagsVersions
для оновлення версій тегів. Тут ми повторюємо всі надані теги та оновлюємо їхні версії, використовуючи, наприклад, поточний час як версію.
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) } }
Такий підхід і зручний, і універсальний. Кешування з тегами позбавляє від необхідності вручну вказувати всі ключі для визнання недійсними, автоматизуючи процес. Однак для цього потрібні додаткові ресурси: зберігання даних версії тегів і перевірка їх дійсності з кожним запитом.
Якщо ваша служба кешування є швидкою та не має значних обмежень у розмірі, цей підхід не вплине істотно на продуктивність. Щоб мінімізувати навантаження, ви можете поєднати кешування з тегами з механізмами локального кешування.
Таким чином, кешування з тегами не тільки спрощує визнання недійсними, але й робить роботу з даними більш гнучкою та зрозумілою, особливо в складних системах із великими обсягами взаємопов’язаних даних.