Zapytanie, które trwa 200ms w twojej bazie danych, nadal trwa 200ms za milionowym razem, gdy ktoś je uruchamia, a każde takie uruchomienie rywalizuje o te same połączenia, ten sam I/O dysku i ten sam procesor. Redis trzyma odpowiedź w pamięci, na serwerze, który nie robi niczego innego, więc milionowy odczyt kosztuje tyle co jeden przeskok sieciowy, a nie plan zapytania.
Co Redis naprawdę robi jako cache
Redis to key-value store działający w pamięci. Wartości żyją w RAM, więc odczyty i zapisy mierzy się w czasie poniżej milisekundy zamiast dziesiątek czy setek milisekund, które kosztuje zapytanie relacyjne, gdy w grę wchodzą joiny, obciążone indeksy albo zajęty connection pool. Redis stoi obok twojej bazy danych, nie zamiast niej: aplikacja pyta najpierw Redis i sięga do bazy dopiero, gdy Redis nie ma odpowiedzi.
Wzorzec, który to opisuje, nazywa się cache-aside (też “lazy loading”) i to pierwszy, po który sięga większość aplikacji:
- Aplikacja pyta Redis o klucz.
- Cache hit — Redis ma odpowiedź, zwraca ją. Baza danych w ogóle nie widzi zapytania.
- Cache miss — Redis nie ma odpowiedzi. Odpytuje bazę danych, zapisuje wynik w Redis, a potem go zwraca.
| |
Baza danych pozostaje źródłem prawdy. Redis trzyma szybką, jednorazową kopię tych fragmentów, które czytasz często. Jeśli Redis wystartuje na pusto po restarcie, nic nie ginie: uzupełnia się sam przy kolejnej fali cache missów.
Jak uruchomić Redis w kontenerze Docker
Najszybszy sposób na instancję Redis to oficjalny obraz. Jeśli Docker jest ci nieznany, Co to jest Docker i do czego służy omawia podstawy, zanim pójdziesz dalej.
| |
To uruchamia Redis 8 w tle, publikując port 6379 na twojej maszynie. Połącz się przez redis-cli, zainstalowane lokalnie albo odpalone z drugiego kontenera w tej samej sieci:
| |
| |
TTL zwracające -1 oznacza, że klucz nie ma czasu wygaśnięcia: zostaje, dopóki go nie usuniesz albo Redis go nie wyeksmituje pod presją pamięci. Dla cache prawie zawsze chcesz mieć czas wygaśnięcia, żeby nieaktualne dane same się zestarzały.
| |
EX 300 sprawia, że klucz wygasa za 300 sekund. PX robi to samo w milisekundach, a EXPIRE user:42:name 300 ustawia TTL na kluczu, który już istnieje.
Dla wszystkiego, co wykracza poza szybkie ręczne sprawdzenie, uruchom Redis przez Compose razem z resztą swojego stacku — zobacz Czym Jest Docker Compose i Jak Go Używać:
| |
api dociera do Redis pod hostname redis, bo Compose stawia oba serwisy w tej samej sieci zdefiniowanej przez użytkownika — to mechanizm opisany w Co to Jest Sieć Docker i Jak Jej Używać. Wolumin redis-data ma mniejsze znaczenie dla czystego cache niż dla głównej bazy danych (zobacz Co to jest wolumin Docker i jak go używać): utrata cache oznacza tylko, że kolejne odczyty stają się cache missami. Mimo to zostaw wolumin, jeśli szybszy restart z rozgrzanym cache ma znaczenie dla twojego budżetu opóźnień.
Jak napisać logikę cache-aside w swojej aplikacji
Redis daje ci komendy; twoja aplikacja i tak musi wywołać je we właściwej kolejności. Owinięcie odczytu z bazy w cache-aside wygląda tak:
| |
Dwa szczegóły odpowiadają tu za większość roboty:
- TTL (
ex=300) nie jest opcjonalny. Bez niego wiersz, który zmienia się w bazie danych, wiecznie serwuje starą wartość z Redis. Ustaw TTL zależnie od tego, jak długo nieaktualne dane są akceptowalne dla tego konkretnego klucza, a nie na jedną globalną liczbę dla całej aplikacji. json.dumps/json.loads. Redis przechowuje stringi, plus garść bogatszych typów jak hashe i sorted sety — nigdy obiektów twojego języka. Wszystko, co strukturalne, trzeba zserializować na wejściu i sparsować na wyjściu.
Gdy wiersz w bazie danych się zmienia, usuń klucz zamiast czekać na TTL:
| |
Kolejny odczyt po aktualizacji to gwarantowany cache miss, który uzupełnia Redis świeżym wierszem. Cache-aside nigdy nie próbuje utrzymywać cache zsynchronizowanego w czasie rzeczywistym. Sprawia, że nieaktualne wpisy znikają szybko — przez TTL albo przez jawne usunięcie przy zapisie.
Eviction: co się dzieje, gdy Redis wyczerpie pamięć
Redis trzyma wszystko w RAM, więc pamięć jest skończona w sposób, w jaki dysk zwykle nie jest. Ustaw twardy limit przez maxmemory i powiedz Redis, co robić po jego osiągnięciu, przez maxmemory-policy:
| Polityka | Zachowanie | Do czego |
|---|---|---|
noeviction | Odrzuca nowe zapisy po zapełnieniu, zwraca błąd | Datastore, który nie może po cichu tracić danych |
allkeys-lru | Usuwa najdawniej używany klucz, dowolny | Czysty cache — sensowny wybór domyślny |
volatile-lru | Usuwa najdawniej używany klucz spośród tych z TTL | Mieszanie kluczy cache i kluczy stałych w jednej instancji |
volatile-ttl | Usuwa najpierw klucz z najkrótszym pozostałym TTL | Rate limitery, krótkotrwałe tokeny |
Dla dedykowanej instancji cache allkeys-lru prawie zawsze jest właściwa: każdy klucz jest w niej jednorazowy z definicji, więc pozwól Redis wyrzucać to, czego dotykałeś najdawniej. noeviction to zła polityka dla cache, bo zamienia “Redis jest pełny” w błędy aplikacji zamiast po cichu odrzucać zimne wpisy.
| |
Ustaw obie wartości w komendzie startowej, tak jak w pliku Compose powyżej, a nie tylko w runtime. Restart kontenera resetuje CONFIG SET ustawiony tylko w runtime do domyślnych wartości Redis (maxmemory 0, czyli bez limitu, ograniczonego tylko przez hosta).
Kiedy cache Redis to zły wybór
Cache ukrywa wolną ścieżkę; nie naprawia jej. Cztery sytuacje, w których sięgnięcie po Redis jako pierwsze rozwiązanie tylko pogarsza sprawę:
- Dane i tak zmieniają się przy każdym odczycie. Ticker giełdowy na żywo albo licznik zwiększany przy każdym żądaniu nic nie zyskuje na cache unieważnianym tak szybko, jak jest wypełniany. Dodałeś przeskok sieciowy bez żadnego hit rate.
- Potrzebujesz silnej spójności. Cache-aside jest z założenia eventually consistent: między zapisem a kolejnym cache missem istnieje okno, w którym wciąż może zostać zwrócona nieaktualna wartość. Dla salda konta czy stanu magazynowego, gdzie to okno powoduje realną szkodę, nie cachuj tego albo cachuj z TTL rzędu sekund i świadomie zaakceptuj kompromis.
- Prawdziwe wąskie gardło jest gdzie indziej. Jeśli twoja baza danych jest wolna z powodu brakującego indeksu albo zapytania N+1, napraw to najpierw. Cachowanie źle napisanego zapytania tylko przenosi tę samą złą odpowiedź do pamięci szybciej.
- Thundering herd przy wygaśnięciu. Gdy gorący klucz wygasa, wszystkie równoległe żądania mogą naraz nie trafić w cache i jednocześnie zaatakować bazę danych. Użyj
SET key value NX EX 30jako krótkotrwałej blokady, trzymanej przez to żądanie, które uzupełnia cache, żeby pozostałe czekały na nie zamiast walić w bazę.
Od czego zacząć z cache Redis
Postaw kontener redis:8 przed swoim najwolniejszym, najczęściej powtarzanym odczytem. Owiń go w cache-aside z TTL, który potrafisz uzasadnić, a nie z okrągłą liczbą wybraną na chybił trafił, i ustaw maxmemory-policy allkeys-lru, żeby pełny cache degradował zamiast rzucać błędy. Potem zmierz hit rate. Write-through caching, unieważnianie przez pub/sub i Redis Streams kosztują dodatkową złożoność i żadne z nich nie jest warte zachodu, dopóki prosty setup cache-aside nie pokaże ci dokładnie, gdzie mu brakuje.