Wyobraźmy sobie, że mamy system, w którym użytkownicy dodają artykuły. Dla każdego użytkownika wyświetlamy statystyki dotyczące jego artykułów na jego osobistym pulpicie: liczbę artykułów, średnią liczbę słów, częstotliwość publikacji itp. Aby przyspieszyć system, buforujemy te dane. Dla każdego raportu tworzony jest unikalny klucz bufora.
Powstaje pytanie: jak unieważnić takie cache, gdy dane się zmieniają? Jednym ze sposobów jest ręczne czyszczenie cache dla każdego zdarzenia, na przykład, gdy dodawany jest nowy artykuł:
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, ]) } }
Ta metoda działa, ale staje się uciążliwa, gdy mamy do czynienia z dużą liczbą raportów i kluczy. W tym miejscu przydaje się buforowanie tagów. Buforowanie tagów pozwala na skojarzenie danych nie tylko z kluczem, ale także z tablicą tagów. Następnie wszystkie rekordy skojarzone z konkretnym tagiem mogą zostać unieważnione, co znacznie upraszcza proces.
Zapisywanie wartości do pamięci podręcznej za pomocą tagów:
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' ] )
Unieważnianie pamięci podręcznej według tagów:
class UpdateCacheTagsOnArticleCreated { public function handle(event: ArticleCreatedEvent): void { this->taggedCacheService->updateTagsVersions([ 'user_article_cache_tag_' . user->id, ]) } }
Tutaj tag 'user_article_cache_tag_' . $user->id
reprezentuje zmiany w artykułach użytkownika. Może być użyty do unieważnienia pamięci podręcznej zależnej od tych danych. Bardziej szczegółowy tag 'user_article_report_cache_tag_' . $user->id
pozwala wyczyścić tylko raporty użytkownika, podczas gdy ogólny tag 'user_article_report'
unieważnia pamięć podręczną raportów dla wszystkich użytkowników.
Jeśli Twoja biblioteka buforowania nie obsługuje tagowania, możesz zaimplementować je samodzielnie. Głównym pomysłem jest przechowywanie bieżących wartości wersji tagów, a także dla każdej oznaczonej wartości przechowywanie wersji tagów, które były aktualne w momencie zapisania wartości w pamięci podręcznej. Następnie, podczas pobierania wartości z pamięci podręcznej, pobierane są również bieżące wersje tagów, a ich ważność jest sprawdzana poprzez ich porównanie.
Tworzenie klasy TaggedCache
class TaggedCache { private cacheService: cacheService }
Implementacja metody set
do zapisu do pamięci podręcznej za pomocą tagów. W tej metodzie musimy zapisać wartość do pamięci podręcznej, a także pobrać bieżące wersje dostarczonych tagów i zapisać je w powiązaniu ze specyficznym kluczem pamięci podręcznej. Osiąga się to poprzez użycie dodatkowego klucza z prefiksem dodanym do dostarczonego klucza.
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 }
Dodanie metody get
w celu pobrania oznaczonych wartości z pamięci podręcznej. Tutaj pobieramy wartość przy użyciu klucza, a także wersji tagów powiązanych z tym kluczem. Następnie sprawdzamy poprawność tagów. Jeśli którykolwiek tag jest nieprawidłowy, wartość jest usuwana z pamięci podręcznej i zwracana jest wartość null
. Jeśli wszystkie tagi są prawidłowe, zwracana jest wartość z pamięci podręcznej.
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 } }
Implementacja metody updateTagsVersions
w celu aktualizacji wersji tagów. Tutaj iterujemy po wszystkich dostarczonych tagach i aktualizujemy ich wersje, używając na przykład bieżącego czasu jako wersji.
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) } }
To podejście jest zarówno wygodne, jak i uniwersalne. Buforowanie tagów eliminuje potrzebę ręcznego określania wszystkich kluczy do unieważnienia, automatyzując proces. Wymaga jednak dodatkowych zasobów: przechowywania danych wersji tagów i sprawdzania ich ważności przy każdym żądaniu.
Jeśli Twoja usługa buforowania jest szybka i nie jest mocno ograniczona rozmiarem, to podejście nie wpłynie znacząco na wydajność. Aby zminimalizować obciążenie, możesz połączyć buforowanie tagowane z lokalnymi mechanizmami buforowania.
W ten sposób buforowanie tagów nie tylko upraszcza unieważnianie, ale także sprawia, że praca z danymi staje się bardziej elastyczna i zrozumiała, zwłaszcza w złożonych systemach z dużą ilością połączonych ze sobą danych.