Programiści często żartują, że programowanie wiąże się z dwoma głównymi wyzwaniami: nadawanie nazw zmiennym unieważnienie pamięci podręcznej Ten żart nie jest daleki od prawdy: zarządzanie pamięcią podręczną, zwłaszcza jej unieważnianie, może stać się poważnym zadaniem. W tym artykule wyjaśnię, jak łatwo wdrożyć funkcjonalność pamięci podręcznej z tagami w oparciu o istniejącą usługę buforowania. 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 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 pozwala wyczyścić tylko raporty użytkownika, podczas gdy ogólny tag unieważnia pamięć podręczną raportów dla wszystkich użytkowników. 'user_article_cache_tag_' . $user->id 'user_article_report_cache_tag_' . $user->id 'user_article_report' 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 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. 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 } Dodanie metody 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ść . Jeśli wszystkie tagi są prawidłowe, zwracana jest wartość z pamięci podręcznej. 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 } } Implementacja metody 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. 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) } } 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.